Merge m-c to inbound.
Merge m-c to inbound.
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -877,8 +877,11 @@ pref("apz.pan_repaint_interval", 40);
//
// Using a software canvas can save memory when JS calls getImageData()
// on the canvas frequently. See bug 884226.
pref("gfx.canvas.willReadFrequently.enable", true);
// Disable autofocus until we can have it not bring up the keyboard.
// https://bugzilla.mozilla.org/show_bug.cgi?id=965763
pref("browser.autofocus", false);
+
+// Enable wakelock
+pref("dom.wakelock.enabled", true);
--- 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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a314508e397c8f1814228d36259ea8708034444e"/>
<!-- 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="317f25e0a4cb3e8e86e2b76c37a14081372f0307">
<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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a314508e397c8f1814228d36259ea8708034444e"/>
<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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a314508e397c8f1814228d36259ea8708034444e"/>
<!-- 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": "01b437206d143e1632ee5a5fd49cc649aee2e970",
+ "revision": "9da6724d2e1af0c60e5fd64c8a79c9c9c0106c9f",
"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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<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="a314508e397c8f1814228d36259ea8708034444e"/>
<!-- 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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<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="a314508e397c8f1814228d36259ea8708034444e"/>
<!-- 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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<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="a314508e397c8f1814228d36259ea8708034444e"/>
<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="317f25e0a4cb3e8e86e2b76c37a14081372f0307">
<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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a314508e397c8f1814228d36259ea8708034444e"/>
<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="4c6b5142d3b716f1c4ea502eeb92d3119f2b01c6"/>
+ <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e546129dd7860ee3e62b3f86ab222647dfcaae4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="78b908b493bfe0b477e3d4f6edec8c46a2c0d096"/>
<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="a314508e397c8f1814228d36259ea8708034444e"/>
<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-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -15,17 +15,17 @@
<stringbundle id="bundle_preferences" src="chrome://browser/locale/preferences/preferences.properties"/>
</stringbundleset>
<commandset id="mainCommandSet">
<command id="cmd_newNavigator" oncommand="OpenBrowserWindow()"/>
<command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" />
<command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" />
- <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/>
+ <command id="cmd_newNavigatorTab" oncommand="BrowserOpenNewTabOrWindow(event);"/>
<command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/>
<command id="Browser:SavePage" oncommand="saveDocument(window.content.document);"/>
<command id="Browser:SendLink"
oncommand="MailIntegration.sendLinkForWindow(window.content);"/>
<command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/>
<command id="cmd_print" oncommand="PrintUtils.print();"/>
--- a/browser/base/content/browser-tabPreviews.js
+++ b/browser/base/content/browser-tabPreviews.js
@@ -197,24 +197,19 @@ var ctrlTab = {
// Rotate the list until the selected tab is first
while (!list[0].selected)
list.push(list.shift());
list = list.filter(function (tab) !tab.closing);
if (this.recentlyUsedLimit != 0) {
- let recentlyUsedTabs = [];
- for (let tab of this._recentlyUsedTabs) {
- if (!tab.hidden && !tab.closing) {
- recentlyUsedTabs.push(tab);
- if (this.recentlyUsedLimit > 0 && recentlyUsedTabs.length >= this.recentlyUsedLimit)
- break;
- }
- }
+ let recentlyUsedTabs = this._recentlyUsedTabs;
+ if (this.recentlyUsedLimit > 0)
+ recentlyUsedTabs = this._recentlyUsedTabs.slice(0, this.recentlyUsedLimit);
for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) {
list.splice(list.indexOf(recentlyUsedTabs[i]), 1);
list.unshift(recentlyUsedTabs[i]);
}
}
return this._tabList = list;
},
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7231,8 +7231,16 @@ let BrowserChromeTest = {
},
runWhenReady: function (cb) {
if (this._ready)
cb();
else
this._cb = cb;
}
};
+
+function BrowserOpenNewTabOrWindow(event) {
+ if (event.shiftKey) {
+ OpenBrowserWindow();
+ } else {
+ BrowserOpenTab();
+ }
+}
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -115,17 +115,16 @@
class="subviewbutton"
oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/>
<toolbarbutton id="panelMenu_unsortedBookmarks"
label="&unsortedBookmarksCmd.label;"
class="subviewbutton"
oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
<toolbarseparator/>
<toolbaritem id="panelMenu_bookmarksMenu"
- flex="1"
orient="vertical"
smoothscroll="false"
onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
flatList="true"
tooltip="bhTooltip">
<!-- bookmarks menu items will go here -->
</toolbaritem>
@@ -140,17 +139,17 @@
<panelview id="PanelUI-socialapi" flex="1"/>
<panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
<label value="&feedsMenu.label;" class="panel-subview-header"/>
</panelview>
<panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
<label value="&helpMenu.label;" class="panel-subview-header"/>
- <vbox id="PanelUI-helpItems"/>
+ <vbox id="PanelUI-helpItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-developer" flex="1">
<label value="&webDeveloperMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-developerItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-characterEncodingView" flex="1">
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -298,16 +298,17 @@ const PanelUI = {
tempPanel.setAttribute("context", "");
document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
// If the view has a footer, set a convenience class on the panel.
tempPanel.classList.toggle("cui-widget-panelWithFooter",
viewNode.querySelector(".panel-subview-footer"));
let multiView = document.createElement("panelmultiview");
tempPanel.appendChild(multiView);
+ multiView.setAttribute("mainViewIsSubView", "true");
multiView.setMainView(viewNode);
viewNode.classList.add("cui-widget-panelview");
CustomizableUI.addPanelCloseListeners(tempPanel);
let panelRemover = function() {
tempPanel.removeEventListener("popuphidden", panelRemover);
viewNode.classList.remove("cui-widget-panelview");
CustomizableUI.removePanelCloseListeners(tempPanel);
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -61,16 +61,18 @@
<field name="__transitioning">false</field>
<field name="_ignoreMutations">false</field>
<property name="showingSubView" readonly="true"
onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/>
<property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
<property name="_mainView" readonly="true"
onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
+ <property name="showingSubViewAsMainView" readonly="true"
+ onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
<property name="ignoreMutations">
<getter>
return this._ignoreMutations;
</getter>
<setter><![CDATA[
this._ignoreMutations = val;
if (!val && this._panel.state == "open") {
@@ -317,18 +319,23 @@
let newHeight = this._heightOfSubview(this._currentSubView);
this._viewContainer.style.height = newHeight + "px";
}
]]></body>
</method>
<method name="_syncContainerWithMainView">
<body><![CDATA[
if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
- this._viewContainer.style.height =
- this._mainView.scrollHeight + "px";
+ let height;
+ if (this.showingSubViewAsMainView) {
+ height = this._heightOfSubview(this._mainView);
+ } else {
+ height = this._mainView.scrollHeight;
+ }
+ this._viewContainer.style.height = height + "px";
}
]]></body>
</method>
<method name="_heightOfSubview">
<parameter name="aSubview"/>
<body><![CDATA[
let body = aSubview.querySelector(".panel-subview-body");
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -3134,17 +3134,17 @@ function WidgetSingleWrapper(aWidget, aN
this.__defineGetter__("disabled", function() aNode.disabled);
this.__defineSetter__("disabled", function(aValue) {
aNode.disabled = !!aValue;
});
this.__defineGetter__("anchor", function() {
let anchorId;
// First check for an anchor for the area:
- let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
+ let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
if (placement) {
anchorId = gAreas.get(placement.area).get("anchor");
}
if (!anchorId) {
anchorId = aNode.getAttribute("cui-anchorid");
}
return anchorId ? aNode.ownerDocument.getElementById(anchorId)
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -849,16 +849,17 @@ CustomizeMode.prototype = {
CustomizableUI.reset();
yield this._wrapToolbarItems();
yield this.populatePalette();
this.persistCurrentSets(true);
this._updateResetButton();
+ this._updateEmptyPaletteNotice();
this._showPanelCustomizationPlaceholders();
this.resetting = false;
}.bind(this)).then(null, ERROR);
},
_onToolbarVisibilityChange: function(aEvent) {
let toolbar = aEvent.target;
if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
@@ -950,18 +951,20 @@ CustomizeMode.prototype = {
let widget = CustomizableUI.getWidget(aWidgetId);
this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
}
}
},
_onUIChange: function() {
this._changed = true;
- this._updateResetButton();
- this._updateEmptyPaletteNotice();
+ if (!this.resetting) {
+ this._updateResetButton();
+ this._updateEmptyPaletteNotice();
+ }
this.dispatchToolboxEvent("customizationchange");
},
_updateEmptyPaletteNotice: function() {
let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
this.paletteEmptyNotice.hidden = !!paletteItems.length;
},
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -24,16 +24,19 @@ const EVENTS = {
// When new sources are received from the debugger server.
NEW_SOURCE: "Debugger:NewSource",
SOURCES_ADDED: "Debugger:SourcesAdded",
// When a source is shown in the source editor.
SOURCE_SHOWN: "Debugger:EditorSourceShown",
SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
+ // When the editor has shown a source and set the line / column position
+ EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
+
// When scopes, variables, properties and watch expressions are fetched and
// displayed in the variables view.
FETCHED_SCOPES: "Debugger:FetchedScopes",
FETCHED_VARIABLES: "Debugger:FetchedVariables",
FETCHED_PROPERTIES: "Debugger:FetchedProperties",
FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
@@ -1295,25 +1298,25 @@ SourceScripts.prototype = {
if (textPromise && textPromise.pretty === wantPretty) {
return textPromise;
}
const deferred = promise.defer();
deferred.promise.pretty = wantPretty;
this._cache.set(aSource.url, deferred.promise);
- const afterToggle = ({ error, message, source: text }) => {
+ const afterToggle = ({ error, message, source: text, contentType }) => {
if (error) {
// Revert the rejected promise from the cache, so that the original
// source's text may be shown when the source is selected.
this._cache.set(aSource.url, textPromise);
deferred.reject([aSource, message || error]);
return;
}
- deferred.resolve([aSource, text]);
+ deferred.resolve([aSource, text, contentType]);
};
if (wantPretty) {
sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle);
} else {
sourceClient.disablePrettyPrint(afterToggle);
}
@@ -1355,24 +1358,25 @@ SourceScripts.prototype = {
this._cache.set(aSource.url, deferred.promise);
// If the source text takes a long time to fetch, invoke a callback.
if (aOnTimeout) {
var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
}
// Get the source text from the active thread.
- this.activeThread.source(aSource).source(({ error, message, source: text }) => {
+ this.activeThread.source(aSource)
+ .source(({ error, message, source: text, contentType }) => {
if (aOnTimeout) {
window.clearTimeout(fetchTimeout);
}
if (error) {
deferred.reject([aSource, message || error]);
} else {
- deferred.resolve([aSource, text]);
+ deferred.resolve([aSource, text, contentType]);
}
});
return deferred.promise;
},
/**
* Starts fetching all the sources, silently.
@@ -1401,23 +1405,23 @@ SourceScripts.prototype = {
}
/* Called if fetching a source takes too long. */
function onTimeout(aSource) {
onError([aSource]);
}
/* Called if fetching a source finishes successfully. */
- function onFetch([aSource, aText]) {
+ function onFetch([aSource, aText, aContentType]) {
// If fetching the source has previously timed out, discard it this time.
if (!pending.has(aSource.url)) {
return;
}
pending.delete(aSource.url);
- fetched.push([aSource.url, aText]);
+ fetched.push([aSource.url, aText, aContentType]);
maybeFinish();
}
/* Called if fetching a source failed because of an error. */
function onError([aSource, aError]) {
pending.delete(aSource.url);
maybeFinish();
}
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -383,35 +383,36 @@ let DebuggerView = {
let histogram = Services.telemetry.getHistogramById(histogramId);
let startTime = Date.now();
let deferred = promise.defer();
this._setEditorText(L10N.getStr("loadingText"));
this._editorSource = { url: aSource.url, promise: deferred.promise };
- DebuggerController.SourceScripts.getText(aSource).then(([, aText]) => {
+ DebuggerController.SourceScripts.getText(aSource)
+ .then(([, aText, aContentType]) => {
// Avoid setting an unexpected source. This may happen when switching
// very fast between sources that haven't been fetched yet.
if (this._editorSource.url != aSource.url) {
return;
}
this._setEditorText(aText);
- this._setEditorMode(aSource.url, aSource.contentType, aText);
+ this._setEditorMode(aSource.url, aContentType, aText);
// Synchronize any other components with the currently displayed source.
DebuggerView.Sources.selectedValue = aSource.url;
DebuggerController.Breakpoints.updateEditorBreakpoints();
histogram.add(Date.now() - startTime);
// Resolve and notify that a source file was shown.
window.emit(EVENTS.SOURCE_SHOWN, aSource);
- deferred.resolve([aSource, aText]);
+ deferred.resolve([aSource, aText, aContentType]);
},
([, aError]) => {
let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError);
this._setEditorText(msg);
Cu.reportError(msg);
dumpn(msg);
// Reject and notify that there was an error showing the source file.
@@ -461,35 +462,40 @@ let DebuggerView = {
}
}
let sourceItem = this.Sources.getItemByValue(aUrl);
let sourceForm = sourceItem.attachment.source;
// Make sure the requested source client is shown in the editor, then
// update the source editor's caret position and debug location.
- return this._setEditorSource(sourceForm, aFlags).then(() => {
+ return this._setEditorSource(sourceForm, aFlags)
+ .then(([,, aContentType]) => {
+ // Record the contentType learned from fetching
+ sourceForm.contentType = aContentType;
// Line numbers in the source editor should start from 1. If invalid
// or not specified, then don't do anything.
if (aLine < 1) {
+ window.emit(EVENTS.EDITOR_LOCATION_SET);
return;
}
if (aFlags.charOffset) {
aLine += this.editor.getPosition(aFlags.charOffset).line;
}
if (aFlags.lineOffset) {
aLine += aFlags.lineOffset;
}
if (!aFlags.noCaret) {
let location = { line: aLine -1, ch: aFlags.columnOffset || 0 };
this.editor.setCursor(location, aFlags.align);
}
if (!aFlags.noDebug) {
this.editor.setDebugLocation(aLine - 1);
}
+ window.emit(EVENTS.EDITOR_LOCATION_SET);
}).then(null, console.error);
},
/**
* Gets the visibility state of the instruments pane.
* @return boolean
*/
get instrumentsPaneHidden()
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -23,16 +23,18 @@ support-files =
code_tracing-01.js
code_ugly.js
code_ugly-2.js
code_ugly-3.js
code_ugly-4.js
code_ugly-5.js
code_ugly-6.js
code_ugly-7.js
+ code_ugly-8
+ code_ugly-8^headers^
doc_auto-pretty-print-01.html
doc_auto-pretty-print-02.html
doc_binary_search.html
doc_blackboxing.html
doc_closures.html
doc_cmd-break.html
doc_cmd-dbg.html
doc_conditional-breakpoints.html
@@ -51,16 +53,17 @@ support-files =
doc_inline-script.html
doc_large-array-buffer.html
doc_minified.html
doc_minified_bogus_map.html
doc_no-page-sources.html
doc_pause-exceptions.html
doc_pretty-print.html
doc_pretty-print-2.html
+ doc_pretty-print-3.html
doc_random-javascript.html
doc_recursion-stack.html
doc_scope-variable.html
doc_scope-variable-2.html
doc_scope-variable-3.html
doc_script-switching-01.html
doc_script-switching-02.html
doc_step-out.html
@@ -156,16 +159,17 @@ support-files =
[browser_dbg_pretty-print-05.js]
[browser_dbg_pretty-print-06.js]
[browser_dbg_pretty-print-07.js]
[browser_dbg_pretty-print-08.js]
[browser_dbg_pretty-print-09.js]
[browser_dbg_pretty-print-10.js]
[browser_dbg_pretty-print-11.js]
[browser_dbg_pretty-print-12.js]
+[browser_dbg_pretty-print-13.js]
[browser_dbg_progress-listener-bug.js]
[browser_dbg_reload-preferred-script-01.js]
[browser_dbg_reload-preferred-script-02.js]
[browser_dbg_reload-preferred-script-03.js]
[browser_dbg_reload-same-script.js]
[browser_dbg_scripts-switching-01.js]
[browser_dbg_scripts-switching-02.js]
[browser_dbg_scripts-switching-03.js]
copy from browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
copy to browser/devtools/debugger/test/browser_dbg_pretty-print-13.js
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-13.js
@@ -1,33 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
- * Make sure that clicking the pretty print button prettifies the source.
+ * Make sure that clicking the pretty print button prettifies the source, even
+ * when the source URL does not end in ".js", but the content type is
+ * JavaScript.
*/
-const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print-3.html";
let gTab, gDebuggee, gPanel, gDebugger;
let gEditor, gSources;
function test() {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gEditor = gDebugger.DebuggerView.editor;
gSources = gDebugger.DebuggerView.Sources;
- waitForSourceShown(gPanel, "code_ugly.js")
+ promise.all([waitForSourceShown(gPanel, "code_ugly-8"),
+ waitForEditorLocationSet(gPanel)])
.then(testSourceIsUgly)
.then(() => {
- const finished = waitForSourceShown(gPanel, "code_ugly.js");
+ const finished = waitForSourceShown(gPanel, "code_ugly-8");
clickPrettyPrintButton();
testProgressBarShown();
return finished;
})
.then(testSourceIsPretty)
.then(testEditorShown)
.then(testSourceIsStillPretty)
.then(() => closeDebuggerAndFinish(gPanel))
copy from browser/devtools/debugger/test/code_ugly.js
copy to browser/devtools/debugger/test/code_ugly-8
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/code_ugly-8^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/javascript
copy from browser/devtools/debugger/test/doc_pretty-print.html
copy to browser/devtools/debugger/test/doc_pretty-print-3.html
--- a/browser/devtools/debugger/test/doc_pretty-print.html
+++ b/browser/devtools/debugger/test/doc_pretty-print-3.html
@@ -1,8 +1,8 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<title>Debugger Pretty Printing Test Page</title>
</head>
-<script src="code_ugly.js"></script>
+<script src="code_ugly-8"></script>
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -231,16 +231,21 @@ function waitForSourceShown(aPanel, aUrl
if (!sourceUrl.contains(aUrl)) {
return waitForSourceShown(aPanel, aUrl);
} else {
ok(true, "The correct source has been shown.");
}
});
}
+function waitForEditorLocationSet(aPanel) {
+ return waitForDebuggerEvents(aPanel,
+ aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
+}
+
function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) {
if (aPanel.panelWin.DebuggerView.Sources.selectedValue.contains(aUrl)) {
ok(true, "Expected source is shown: " + aUrl);
return promise.resolve(null);
}
if (aWaitFlag) {
return waitForSourceShown(aPanel, aUrl);
}
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -200,17 +200,18 @@ StyleEditorUI.prototype = {
line: line,
col: ch
};
}
// remember saved file locations
for (let editor of this.editors) {
if (editor.savedFile) {
- this.savedLocations[editor.styleSheet.href] = editor.savedFile;
+ let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
+ this.savedLocations[identifier] = editor.savedFile;
}
}
this._clearStyleSheetEditors();
this._view.removeAll();
this.selectedEditor = null;
@@ -252,17 +253,18 @@ StyleEditorUI.prototype = {
* Object representing stylesheet
* @param {nsIfile} file
* Optional file object that sheet was imported from
* @param {Boolean} isNew
* Optional if stylesheet is a new sheet created by user
*/
_addStyleSheetEditor: function(styleSheet, file, isNew) {
// recall location of saved file for this sheet after page reload
- let savedFile = this.savedLocations[styleSheet.href];
+ let identifier = this.getStyleSheetIdentifier(styleSheet);
+ let savedFile = this.savedLocations[identifier];
if (savedFile && !file) {
file = savedFile;
}
let editor =
new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker);
editor.on("property-change", this._summaryChange.bind(this, editor));
@@ -522,16 +524,28 @@ StyleEditorUI.prototype = {
deferred.resolve(editor.summary);
}
});
return deferred.promise;
},
/**
+ * Returns an identifier for the given style sheet.
+ *
+ * @param {StyleSheet} aStyleSheet
+ * The style sheet to be identified.
+ */
+ getStyleSheetIdentifier: function (aStyleSheet) {
+ // Identify inline style sheets by their host page URI and index at the page.
+ return aStyleSheet.href ? aStyleSheet.href :
+ "inline-" + aStyleSheet.styleSheetIndex + "-at-" + aStyleSheet.nodeHref;
+ },
+
+ /**
* selects a stylesheet and optionally moves the cursor to a selected line
*
* @param {string} [href]
* Href of stylesheet that should be selected. If a stylesheet is not passed
* and the editor is not initialized we focus the first stylesheet. If
* a stylesheet is not passed and the editor is initialized we ignore
* the call.
* @param {Number} [line]
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -2,16 +2,18 @@
support-files =
autocomplete.html
browser_styleeditor_cmd_edit.html
four.html
head.js
import.css
import.html
import2.css
+ inline-1.html
+ inline-2.html
longload.html
media-small.css
media.html
minified.html
nostyle.html
resources_inpage.jsi
resources_inpage1.css
resources_inpage2.css
@@ -32,16 +34,17 @@ support-files =
[browser_styleeditor_bug_851132_middle_click.js]
[browser_styleeditor_bug_870339.js]
[browser_styleeditor_cmd_edit.js]
[browser_styleeditor_enabled.js]
[browser_styleeditor_filesave.js]
[browser_styleeditor_import.js]
[browser_styleeditor_import_rule.js]
[browser_styleeditor_init.js]
+[browser_styleeditor_inline_friendly_names.js]
[browser_styleeditor_loading.js]
[browser_styleeditor_new.js]
[browser_styleeditor_nostyle.js]
[browser_styleeditor_pretty.js]
# Disabled because of intermittent failures - See Bug 942473
skip-if = true
[browser_styleeditor_private_perwindowpb.js]
[browser_styleeditor_reload.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_inline_friendly_names.js
@@ -0,0 +1,150 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gUI;
+
+const FIRST_TEST_PAGE = TEST_BASE + "inline-1.html"
+const SECOND_TEST_PAGE = TEST_BASE + "inline-2.html"
+const SAVE_PATH = "test.css";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+
+ // First test that identifiers are correcly generated. If not other tests
+ // are likely to fail.
+ testIndentifierGeneration();
+
+ waitForEditors(2)
+ .then(saveFirstInlineStyleSheet)
+ .then(testFriendlyNamesAfterSave)
+ .then(reloadPage)
+ .then(testFriendlyNamesAfterSave)
+ .then(navigateToAnotherPage)
+ .then(testFriendlyNamesAfterNavigation)
+ .then(finishTests);
+ });
+
+ content.location = FIRST_TEST_PAGE;
+}
+
+function testIndentifierGeneration() {
+ let fakeStyleSheetFile = {
+ "href": "http://example.com/test.css",
+ "nodeHref": "http://example.com/",
+ "styleSheetIndex": 1
+ }
+
+ let fakeInlineStyleSheet = {
+ "href": null,
+ "nodeHref": "http://example.com/",
+ "styleSheetIndex": 2
+ }
+
+ is(gUI.getStyleSheetIdentifier(fakeStyleSheetFile), "http://example.com/test.css",
+ "URI is the identifier of style sheet file.");
+
+ is(gUI.getStyleSheetIdentifier(fakeInlineStyleSheet), "inline-2-at-http://example.com/",
+ "Inline style sheets are identified by their page and position at that page.");
+}
+
+function saveFirstInlineStyleSheet() {
+ let deferred = promise.defer();
+ let editor = gUI.editors[0];
+
+ let destFile = FileUtils.getFile("ProfD", [SAVE_PATH]);
+
+ editor.saveToFile(destFile, function (file) {
+ ok(file, "File was correctly saved.");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function testFriendlyNamesAfterSave() {
+ let firstEditor = gUI.editors[0];
+ let secondEditor = gUI.editors[1];
+
+ // The friendly name of first sheet should've been remembered, the second should
+ // not be the same (bug 969900).
+ is(firstEditor.friendlyName, SAVE_PATH,
+ "Friendly name is correct for the saved inline style sheet.");
+ isnot(secondEditor.friendlyName, SAVE_PATH,
+ "Friendly name is for the second inline style sheet is not the same as first.");
+
+ return promise.resolve(null);
+}
+
+function reloadPage() {
+ info("Reloading page.");
+ content.location.reload();
+ return waitForEditors(2);
+}
+
+function navigateToAnotherPage() {
+ info("Navigating to another page.");
+ let deferred = promise.defer();
+ gBrowser.removeCurrentTab();
+
+ gUI = null;
+
+ addTabAndOpenStyleEditor(function(panel) {
+ gUI = panel.UI;
+
+ waitForEditors(2).then(deferred.resolve);
+ });
+
+ content.location = SECOND_TEST_PAGE;
+ return deferred.promise;
+}
+
+function testFriendlyNamesAfterNavigation() {
+ let firstEditor = gUI.editors[0];
+ let secondEditor = gUI.editors[1];
+
+ // Inline style sheets shouldn't have the name of previously saved file as the
+ // page is different.
+ isnot(firstEditor.friendlyName, SAVE_PATH,
+ "The first editor doesn't have the save path as a friendly name.");
+ isnot(secondEditor.friendlyName, SAVE_PATH,
+ "The second editor doesn't have the save path as a friendly name.");
+
+ return promise.resolve(null);
+}
+
+function finishTests() {
+ gUI = null;
+ finish();
+}
+
+/**
+ * Waits for all editors to be added.
+ *
+ * @param {int} aNumberOfEditors
+ * The number of editors to wait until proceeding.
+ *
+ * Returns a promise that's resolved once all editors are added.
+ */
+function waitForEditors(aNumberOfEditors) {
+ let deferred = promise.defer();
+ let count = 0;
+
+ info("Waiting for " + aNumberOfEditors + " editors to be added");
+ gUI.on("editor-added", function editorAdded(event, editor) {
+ if (++count == aNumberOfEditors) {
+ info("All editors added. Resolving promise.");
+ gUI.off("editor-added", editorAdded);
+ gUI.editors[0].getSourceEditor().then(deferred.resolve);
+ }
+ else {
+ info ("Editor " + count + " of " + aNumberOfEditors + " added.");
+ }
+ });
+
+ return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/inline-1.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #1</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="first">
+ Inline test page #1
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/inline-2.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #2</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="second">
+ Inline test page #2
+ </body>
+</html>
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
@@ -1,59 +1,71 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- *
- * Contributor(s):
- * Mihai Șucan <mihai.sucan@gmail.com>
- *
- * ***** END LICENSE BLOCK ***** */
+/* 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/. */
+
+// Test that the console output scrolls to JS eval results when there are many
+// messages displayed. See bug 601352.
+
+function test() {
+ Task.spawn(runner).then(finishTest);
-function consoleOpened(HUD) {
- HUD.jsterm.clearOutput();
+ function* runner() {
+ let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
+ let hud = yield openConsole(tab);
+ hud.jsterm.clearOutput();
+
+ let longMessage = "";
+ for (let i = 0; i < 50; i++) {
+ longMessage += "LongNonwrappingMessage";
+ }
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test1 message " + i);
+ }
- let longMessage = "";
- for (let i = 0; i < 50; i++) {
- longMessage += "LongNonwrappingMessage";
- }
+ content.console.log(longMessage);
- for (let i = 0; i < 50; i++) {
- content.console.log("test message " + i);
- }
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test2 message " + i);
+ }
- content.console.log(longMessage);
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test1 message 0",
+ }, {
+ text: "test1 message 49",
+ }, {
+ text: "LongNonwrappingMessage",
+ }, {
+ text: "test2 message 0",
+ }, {
+ text: "test2 message 49",
+ }],
+ });
- for (let i = 0; i < 50; i++) {
- content.console.log("test message " + i);
- }
+ let nodeDeferred = promise.defer();
+ hud.jsterm.execute("1+1", (node) => { nodeDeferred.resolve(node); });
+ let node = yield nodeDeferred.promise;
- HUD.jsterm.execute("1+1", performTest);
-
- function performTest(node) {
- let scrollNode = HUD.outputNode.parentNode;
+ let scrollNode = hud.outputNode.parentNode;
let rectNode = node.getBoundingClientRect();
let rectOutput = scrollNode.getBoundingClientRect();
+ console.debug("rectNode", rectNode, "rectOutput", rectOutput);
+ console.log("scrollNode scrollHeight", scrollNode.scrollHeight, "scrollTop", scrollNode.scrollTop, "clientHeight", scrollNode.clientHeight);
isnot(scrollNode.scrollTop, 0, "scroll location is not at the top");
- // Visible scroll viewport.
- let height = scrollNode.scrollHeight - scrollNode.scrollTop;
+ // The bounding client rect .top/left coordinates are relative to the
+ // console iframe.
- // Top position of the last message node, relative to the outputNode.
- let top = rectNode.top + scrollNode.scrollTop;
- let bottom = top + node.clientHeight;
- info("output height " + height + " node top " + top + " node bottom " + bottom + " node height " + node.clientHeight);
+ // Visible scroll viewport.
+ let height = rectOutput.height;
+
+ // Top and bottom coordinates of the last message node, relative to the outputNode.
+ let top = rectNode.top - rectOutput.top;
+ let bottom = top + rectNode.height;
+ info("node top " + top + " node bottom " + bottom + " node clientHeight " + node.clientHeight);
ok(top >= 0 && bottom <= height, "last message is visible");
-
- finishTest();
- };
+ }
}
-
-function test() {
- addTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
- browser.addEventListener("load", function tabLoad(aEvent) {
- browser.removeEventListener(aEvent.type, tabLoad, true);
- openConsole(null, consoleOpened);
- }, true);
-}
-
--- a/browser/metro/base/content/bindings/bindings.xml
+++ b/browser/metro/base/content/bindings/bindings.xml
@@ -191,17 +191,17 @@
box.showContextMenu(this, event, false);
]]>
</handler>
<handler event="click" phase="capturing">
<![CDATA[
if (event.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
if (typeof SelectionHelperUI != 'undefined') {
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
- event.clientX, event.clientY);
+ event.clientX, event.clientY, this);
} else {
// If we don't have access to SelectionHelperUI then we are using this
// binding for browser content (e.g. about:config)
Services.obs.notifyObservers(event, "attach_edit_session_to_content", "");
}
}
]]>
</handler>
@@ -290,17 +290,17 @@
</method>
</implementation>
<handlers>
<handler event="click" phase="capturing">
<![CDATA[
if (event.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
if (typeof SelectionHelperUI != 'undefined') {
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
- event.clientX, event.clientY);
+ event.clientX, event.clientY, this);
} else {
// If we don't have access to SelectionHelperUI then we are using this
// binding for browser content (e.g. about:config)
Services.obs.notifyObservers(event, "attach_edit_session_to_content", "");
}
}
]]>
</handler>
--- a/browser/metro/base/content/bindings/urlbar.xml
+++ b/browser/metro/base/content/bindings/urlbar.xml
@@ -287,16 +287,22 @@
if (!this.focused)
this.focus();
this._clearFormatting();
this.select();
if (aShouldDismiss)
ContextUI.dismissTabs();
+
+ if (!InputSourceHelper.isPrecise && this.textLength) {
+ let inputRectangle = this.inputField.getBoundingClientRect();
+ SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
+ inputRectangle.left, inputRectangle.top, this);
+ }
]]>
</body>
</method>
<method name="endEditing">
<parameter name="aShouldRevert"/>
<body>
<![CDATA[
@@ -443,16 +449,25 @@
// and displays the half-height autocomplete popup.
this.beginEditing();
this.openPopup();
]]>
</handler>
<handler event="click" phase="capturing">
<![CDATA[
+ // workaround for bug 925457: taping browser chrome resets last tap
+ // co-ordinates to 'undefined' so that we know not to shift the
+ // browser when the keyboard is up in SelectionHandler's
+ // _calcNewContentPosition().
+ Browser.selectedTab.browser.messageManager.sendAsyncMessage(
+ "Browser:ResetLastPos", {
+ xPos: null,
+ yPos: null
+ });
this.beginEditing(true);
]]>
</handler>
<!-- Editing mode behaviors -->
<handler event="dblclick" phase="capturing">
<![CDATA[
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -477,18 +477,17 @@ Desktop browser's sync prefs.
<textbox id="urlbar-edit"
type="url"
flex="1"
autocompletesearch="history"
autocompletepopup="urlbar-autocomplete"
completeselectedindex="true"
placeholder="&urlbar.emptytext;"
- tabscrolling="true"
- onclick="SelectionHelperUI.urlbarTextboxClick(this);"/>
+ tabscrolling="true" />
<toolbarbutton id="go-button"
class="urlbar-button"
command="cmd_go"/>
<toolbarbutton id="reload-button"
class="urlbar-button"
oncommand="CommandUpdater.doCommand(
event.shiftKey ? 'cmd_forceReload'
--- a/browser/metro/base/content/helperui/ChromeSelectionHandler.js
+++ b/browser/metro/base/content/helperui/ChromeSelectionHandler.js
@@ -28,17 +28,17 @@ var ChromeSelectionHandler = {
*/
/*
* General selection start method for both caret and selection mode.
*/
_onSelectionAttach: function _onSelectionAttach(aJson) {
this._domWinUtils = Util.getWindowUtils(window);
this._contentWindow = window;
- this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
+ this._targetElement = aJson.target;
this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
if (!this._targetIsEditable) {
this._onFail("not an editable?", this._targetElement);
return;
}
let selection = this._getSelection();
if (!selection) {
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -430,60 +430,69 @@ var SelectionHelperUI = {
});
},
/*
* attachEditSession
*
* Attaches to existing selection and begins editing.
*
- * @param aMsgTarget - Browser or chrome message target
- * @param aX, aY - Browser relative client coordinates.
+ * @param aMsgTarget - Browser or chrome message target.
+ * @param aX Tap browser relative client X coordinate.
+ * @param aY Tap browser relative client Y coordinate.
+ * @param aTarget Actual tap target (optional).
*/
- attachEditSession: function attachEditSession(aMsgTarget, aX, aY) {
+ attachEditSession: function attachEditSession(aMsgTarget, aX, aY, aTarget) {
if (!aMsgTarget || this.isActive)
return;
this._init(aMsgTarget);
this._setupDebugOptions();
// Send this over to SelectionHandler in content, they'll message us
// back with information on the current selection. SelectionAttach
// takes client coordinates.
this._sendAsyncMessage("Browser:SelectionAttach", {
+ target: aTarget,
xPos: aX,
yPos: aY
});
},
/*
* attachToCaret
*
* Initiates a touch caret selection session for a text input.
* Can be called multiple times to move the caret marker around.
*
* Note the caret marker is pretty limited in functionality. The
* only thing is can do is be displayed at the caret position.
* Once the user starts a drag, the caret marker is hidden, and
* the start and end markers take over.
*
- * @param aMsgTarget - Browser or chrome message target
- * @param aX, aY - Browser relative client coordinates of the tap
- * that initiated the session.
+ * @param aMsgTarget - Browser or chrome message target.
+ * @param aX Tap browser relative client X coordinate.
+ * @param aY Tap browser relative client Y coordinate.
+ * @param aTarget Actual tap target (optional).
*/
- attachToCaret: function attachToCaret(aMsgTarget, aX, aY) {
+ attachToCaret: function attachToCaret(aMsgTarget, aX, aY, aTarget) {
if (!this.isActive) {
this._init(aMsgTarget);
this._setupDebugOptions();
} else {
this._hideMonocles();
}
- this._lastPoint = { xPos: aX, yPos: aY };
+ this._lastCaretAttachment = {
+ target: aTarget,
+ xPos: aX,
+ yPos: aY
+ };
this._sendAsyncMessage("Browser:CaretAttach", {
+ target: aTarget,
xPos: aX,
yPos: aY
});
},
/*
* canHandleContextMenuMsg
*
@@ -512,47 +521,23 @@ var SelectionHelperUI = {
// which we will call _shutdown().
let clearSelection = aClearSelection || false;
this._sendAsyncMessage("Browser:SelectionClose", {
clearSelection: clearSelection
});
},
/*
- * Event handler on the navbar text input. Called from navbar bindings
- * when focus is applied to the edit.
- */
- urlbarTextboxClick: function(aEdit) {
- // workaround for bug 925457: taping browser chrome resets last tap
- // co-ordinates to 'undefined' so that we know not to shift the browser
- // when the keyboard is up in SelectionHandler's _calcNewContentPosition().
- Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
- xPos: null,
- yPos: null
- });
-
- if (InputSourceHelper.isPrecise || !aEdit.textLength) {
- return;
- }
-
- // Enable selection when there's text in the control
- let innerRect = aEdit.inputField.getBoundingClientRect();
- this.attachEditSession(ChromeSelectionHandler,
- innerRect.left,
- innerRect.top);
- },
-
- /*
* Click handler for chrome pages loaded into the browser (about:config).
* Called from the text input bindings via the attach_edit_session_to_content
* observer.
*/
chromeTextboxClick: function (aEvent) {
- this.attachEditSession(Browser.selectedTab.browser,
- aEvent.clientX, aEvent.clientY);
+ this.attachEditSession(Browser.selectedTab.browser, aEvent.clientX,
+ aEvent.clientY, aEvent.target);
},
/*
* Handy debug routines that work independent of selection. They
* make use of the selection overlay for drawing points.
*/
debugDisplayDebugPoint: function (aLeft, aTop, aSize, aCssColorStr, aFill) {
@@ -870,22 +855,23 @@ var SelectionHelperUI = {
},
/*
* Event handlers for document events
*/
/*
* Handles taps that move the current caret around in text edits,
- * clear active selection and focus when neccessary, or change
- * modes. Only active afer SelectionHandlerUI is initialized.
+ * clear active selection and focus when necessary, or change
+ * modes. Only active after SelectionHandlerUI is initialized.
*/
_onClick: function(aEvent) {
if (this.layerMode == kChromeLayer && this._targetIsEditable) {
- this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY);
+ this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY,
+ aEvent.target);
}
},
_onKeypress: function _onKeypress() {
this.closeEditSession();
},
_onResize: function _onResize() {
@@ -904,17 +890,18 @@ var SelectionHelperUI = {
/*
* _onDeckOffsetChanged - fired by ContentAreaObserver after the browser
* deck is shifted for form input access in response to a soft keyboard
* display.
*/
_onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) {
// Update the monocle position and display
- this.attachToCaret(null, this._lastPoint.xPos, this._lastPoint.yPos);
+ this.attachToCaret(null, this._lastCaretAttachment.xPos,
+ this._lastCaretAttachment.yPos, this._lastCaretAttachment.target);
},
/*
* Detects when the nav bar transitions, so we can enable selection at the
* appropriate location once the transition is complete, or shutdown
* selection down when the nav bar is hidden.
*/
_onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) {
--- a/browser/metro/base/tests/mochitest/browser_selection_urlbar.js
+++ b/browser/metro/base/tests/mochitest/browser_selection_urlbar.js
@@ -102,17 +102,18 @@ gTests.push({
edit.select();
let editCoords = logicalCoordsForElement(edit);
// wait for popup animation to complete, it interferes with edit selection testing
let autocompletePopup = document.getElementById("urlbar-autocomplete-scroll");
yield waitForEvent(autocompletePopup, "transitionend");
- SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x, editCoords.y);
+ SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x,
+ editCoords.y, edit);
ok(SelectionHelperUI.isSelectionUIVisible, "selection enabled");
let selection = edit.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
.editor.selection;
let rects = selection.getRangeAt(0).getClientRects();
let midX = Math.ceil(((rects[0].right - rects[0].left) * .5) + rects[0].left);
let midY = Math.ceil(((rects[0].bottom - rects[0].top) * .5) + rects[0].top);
@@ -131,17 +132,18 @@ gTests.push({
desc: "bug 894713 - blur shuts down selection handling",
run: function() {
gWindow = window;
yield showNavBar();
let edit = document.getElementById("urlbar-edit");
edit.value = "wikipedia.org";
edit.select();
let editCoords = logicalCoordsForElement(edit);
- SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x, editCoords.y);
+ SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x,
+ editCoords.y, edit);
edit.blur();
ok(!SelectionHelperUI.isSelectionUIVisible, "selection no longer enabled");
clearSelection(edit);
yield waitForCondition(function () {
return !SelectionHelperUI.isSelectionUIVisible;
});
}
});
@@ -223,15 +225,35 @@ gTests.push({
edit.blur();
yield waitForCondition(function () {
return !SelectionHelperUI.isSelectionUIVisible;
});
}
});
+gTests.push({
+ desc: "Bug 957646 - Selection monocles sometimes don't display when tapping" +
+ " text ion the nav bar.",
+ run: function() {
+ yield showNavBar();
+
+ let edit = document.getElementById("urlbar-edit");
+ edit.value = "about:mozilla";
+
+ let editRectangle = edit.getBoundingClientRect();
+
+ // Tap outside the input but close enough for fluffing to take effect.
+ sendTap(window, editRectangle.left + 50, editRectangle.top - 2);
+
+ yield waitForCondition(function () {
+ return SelectionHelperUI.isSelectionUIVisible;
+ });
+ }
+});
+
function test() {
if (!isLandscapeMode()) {
todo(false, "browser_selection_tests need landscape mode to run.");
return;
}
runTests();
}
--- a/browser/metro/base/tests/mochitest/browser_ui_telemetry.js
+++ b/browser/metro/base/tests/mochitest/browser_ui_telemetry.js
@@ -21,8 +21,46 @@ gTests.push({
is(getTelemetryPayload().info.appName, "MetroFirefox");
let simpleMeasurements = getTelemetryPayload().simpleMeasurements;
ok(simpleMeasurements, "simpleMeasurements are truthy");
ok(simpleMeasurements.UITelemetry["metro-ui"]["window-width"], "window-width measurement was captured");
ok(simpleMeasurements.UITelemetry["metro-ui"]["window-height"], "window-height measurement was captured");
}
});
+
+gTests.push({
+ desc: "Test tab count telemetry",
+ run: function() {
+ // Wait for Session Manager to be initialized.
+ yield waitForCondition(() => window.__SSID);
+
+ Services.obs.notifyObservers(null, "reset-telemetry-vars", null);
+ yield waitForCondition(function () {
+ let simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ return simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"] == 1;
+ });
+
+ let simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 1);
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 1);
+
+ let tab2 = Browser.addTab("about:mozilla");
+ simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 2);
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 2);
+
+ let tab3 = Browser.addTab("about:config");
+ simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 3);
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
+
+ Browser.closeTab(tab2, { forceClose: true } );
+ simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 2);
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
+
+ Browser.closeTab(tab3, { forceClose: true } );
+ simpleMeasurements = getTelemetryPayload().simpleMeasurements;
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 1);
+ is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
+ }
+});
\ No newline at end of file
--- a/browser/metro/components/SessionStore.js
+++ b/browser/metro/components/SessionStore.js
@@ -19,16 +19,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
Cu.import("resource://gre/modules/NetUtil.jsm");
return NetUtil;
});
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
// -----------------------------------------------------------------------
// Session Store
// -----------------------------------------------------------------------
const STATE_STOPPED = 0;
const STATE_RUNNING = 1;
const STATE_QUITTING = -1;
@@ -47,28 +50,38 @@ SessionStore.prototype = {
_selectedWindow: 1,
_orderedWindows: [],
_lastSaveTime: 0,
_lastSessionTime: 0,
_interval: 10000,
_maxTabsUndo: 1,
_shouldRestore: false,
+ // Tab telemetry variables
+ _maxTabsOpen: 1,
+
init: function ss_init() {
// Get file references
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
this._sessionFileBackup = this._sessionFile.clone();
this._sessionCache = this._sessionFile.clone();
this._sessionFile.append("sessionstore.js");
this._sessionFileBackup.append("sessionstore.bak");
this._sessionCache.append("sessionstoreCache");
this._loadState = STATE_STOPPED;
try {
+ UITelemetry.addSimpleMeasureFunction("metro-tabs",
+ this._getTabStats.bind(this));
+ } catch (ex) {
+ // swallow exception that occurs if metro-tabs measure is already set up
+ }
+
+ try {
let shutdownWasUnclean = false;
if (this._sessionFileBackup.exists()) {
this._sessionFileBackup.remove(false);
shutdownWasUnclean = true;
}
if (this._sessionFile.exists()) {
@@ -179,29 +192,37 @@ SessionStore.prototype = {
// Remove the stale files in a separate step to keep the enumerator from
// messing up if we remove the files as we collect them.
staleFiles.forEach(function(aFile) {
aFile.remove(false);
})
},
+ _getTabStats: function() {
+ return {
+ currTabCount: this._currTabCount,
+ maxTabCount: this._maxTabsOpen
+ };
+ },
+
observe: function ss_observe(aSubject, aTopic, aData) {
let self = this;
let observerService = Services.obs;
switch (aTopic) {
case "app-startup":
observerService.addObserver(this, "final-ui-startup", true);
observerService.addObserver(this, "domwindowopened", true);
observerService.addObserver(this, "domwindowclosed", true);
observerService.addObserver(this, "browser-lastwindow-close-granted", true);
observerService.addObserver(this, "browser:purge-session-history", true);
observerService.addObserver(this, "quit-application-requested", true);
observerService.addObserver(this, "quit-application-granted", true);
observerService.addObserver(this, "quit-application", true);
+ observerService.addObserver(this, "reset-telemetry-vars", true);
break;
case "final-ui-startup":
observerService.removeObserver(this, "final-ui-startup");
if (WindowsPrefSync) {
// Pulls in Desktop controlled prefs and pushes out Metro controlled prefs
WindowsPrefSync.init();
}
this.init();
@@ -259,16 +280,17 @@ SessionStore.prototype = {
this._sessionFileBackup.remove(false);
observerService.removeObserver(this, "domwindowopened");
observerService.removeObserver(this, "domwindowclosed");
observerService.removeObserver(this, "browser-lastwindow-close-granted");
observerService.removeObserver(this, "quit-application-requested");
observerService.removeObserver(this, "quit-application-granted");
observerService.removeObserver(this, "quit-application");
+ observerService.removeObserver(this, "reset-telemetry-vars");
// If a save has been queued, kill the timer and save state now
if (this._saveTimer) {
this._saveTimer.cancel();
this._saveTimer = null;
this.saveState();
}
break;
@@ -290,34 +312,48 @@ SessionStore.prototype = {
this.saveStateNow();
}
break;
case "timer-callback":
// Timer call back for delayed saving
this._saveTimer = null;
this.saveState();
break;
+ case "reset-telemetry-vars":
+ // Used in mochitests only.
+ this._maxTabsOpen = 1;
}
},
+ updateTabTelemetryVars: function(window) {
+ this._currTabCount = window.Browser.tabs.length;
+ if (this._currTabCount > this._maxTabsOpen) {
+ this._maxTabsOpen = this._currTabCount;
+ }
+ },
+
handleEvent: function ss_handleEvent(aEvent) {
let window = aEvent.currentTarget.ownerDocument.defaultView;
switch (aEvent.type) {
case "TabOpen":
+ this.updateTabTelemetryVars(window);
case "TabClose": {
let browser = aEvent.originalTarget.linkedBrowser;
if (aEvent.type == "TabOpen") {
this.onTabAdd(window, browser);
}
else {
this.onTabClose(window, browser);
this.onTabRemove(window, browser);
}
break;
}
+ case "TabRemove":
+ this.updateTabTelemetryVars(window);
+ break;
case "TabSelect": {
let browser = aEvent.originalTarget.linkedBrowser;
this.onTabSelect(window, browser);
break;
}
}
},
@@ -356,27 +392,29 @@ SessionStore.prototype = {
let tabs = aWindow.Browser.tabs;
for (let i = 0; i < tabs.length; i++)
this.onTabAdd(aWindow, tabs[i].browser, true);
// Notification of tab add/remove/selection
let tabContainer = aWindow.document.getElementById("tabs");
tabContainer.addEventListener("TabOpen", this, true);
tabContainer.addEventListener("TabClose", this, true);
+ tabContainer.addEventListener("TabRemove", this, true);
tabContainer.addEventListener("TabSelect", this, true);
},
onWindowClose: function ss_onWindowClose(aWindow) {
// Ignore windows not tracked by SessionStore
if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
return;
let tabContainer = aWindow.document.getElementById("tabs");
tabContainer.removeEventListener("TabOpen", this, true);
tabContainer.removeEventListener("TabClose", this, true);
+ tabContainer.removeEventListener("TabRemove", this, true);
tabContainer.removeEventListener("TabSelect", this, true);
if (this._loadState == STATE_RUNNING) {
// Update all window data for a last time
this._collectWindowData(aWindow);
// Clear this window from the list
delete this._windows[aWindow.__SSID];
--- a/browser/metro/theme/platform.css
+++ b/browser/metro/theme/platform.css
@@ -635,20 +635,22 @@ tabmodalprompt:not([promptType="promptUs
min-width: @touch_action_snapped_minwidth@;
}
}
/*.meta -------------------------------------------------------------------- */
.meta {
background-color: @panel_light_color@;
+ /* bug 969354
background-image: url("chrome://browser/skin/images/firefox-watermark.png");
background-repeat: no-repeat;
background-position: center center;
background-attachment: fixed;
+ */
}
/* needs to observe the viewstate */
.meta-section-container {
padding: 45px 75px 0;
-moz-box-orient: horizontal;
}
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -555,16 +555,21 @@ menuitem:not([type]):not(.menuitem-toolt
}
/* Primary toolbar buttons */
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button,
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 {
-moz-appearance: none;
}
+:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1[open="true"],
+:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:hover:active {
+ padding: 3px;
+}
+
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-icon {
-moz-margin-end: 0;
padding: 2px 6px;
border: 1px solid transparent;
border-radius: 2px;
transition-property: background-color, border-color;
--- a/browser/themes/linux/devtools/computedview.css
+++ b/browser/themes/linux/devtools/computedview.css
@@ -36,26 +36,26 @@ body {
}
.property-view > * {
display: inline-block;
vertical-align: middle;
}
.property-name {
- width: 50%;
+ /* -12px is so the expander triangle isn't pushed up above the property */
+ width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
outline: 0;
}
.property-value {
- width: 50%;
- max-width: 100%;
+ width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-image: url(arrow-e.png);
background-repeat: no-repeat;
background-size: 5px 8px;
background-position: 2px center;
padding-left: 10px;
@@ -70,17 +70,18 @@ body {
padding-left: 8px;
}
@media (min-width: 400px) {
.property-name {
width: 200px;
}
.property-value {
- width: auto;
+ /* -212px is accounting for the 200px property-name and the 12px triangle */
+ width: calc(100% - 212px);
}
}
.property-content {
padding-left: 17px;
}
/* From skin */
--- a/browser/themes/linux/downloads/indicator.css
+++ b/browser/themes/linux/downloads/indicator.css
@@ -17,16 +17,18 @@
z-index: 5;
}
/*** Main indicator icon ***/
#downloads-button[cui-areatype="toolbar"] > #downloads-indicator-anchor > #downloads-indicator-icon {
background: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"),
0, 198, 18, 180) center no-repeat;
+ min-width: 18px;
+ min-height: 18px;
}
#downloads-button[cui-areatype="toolbar"][attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
background-image: url("chrome://browser/skin/downloads/download-glow.png");
}
#downloads-button[cui-areatype="menu-panel"][attention] {
list-style-image: url("chrome://browser/skin/downloads/download-glow-menuPanel.png");
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -2568,34 +2568,23 @@ toolbarbutton.chevron > .toolbarbutton-m
/* image preloading hack from shared/tabs.inc.css */
#TabsToolbar::before {
background-image:
url(chrome://browser/skin/tabbrowser/tab-background-end@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-middle@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png);
}
+ .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]),
.tabs-newtab-button:hover {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-middle@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-end@2x.png);
}
- .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-middle:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-middle@2x.png);
- }
-
- .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png);
- }
-
- .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-end@2x.png);
- }
-
.tab-background-middle[selected=true] {
background-image: url(chrome://browser/skin/tabbrowser/tab-active-middle@2x.png),
@fgTabTexture@,
none;
}
.tab-background-start[selected=true]:-moz-locale-dir(ltr)::after,
.tab-background-end[selected=true]:-moz-locale-dir(rtl)::after {
@@ -4120,16 +4109,20 @@ window > chatbox {
}
#main-window[tabsintitlebar][customize-entered] > #titlebar > #titlebar-content,
#main-window:not([tabsintitlebar]):not(:-moz-lwtheme) > #titlebar > #titlebar-content {
margin-top: 11px;
margin-bottom: 0px;
}
+#main-window:not([tabsintitlebar]):-moz-lwtheme > #titlebar {
+ margin-bottom: 5px;
+}
+
#main-window[tabsintitlebar]:-moz-lwtheme > #titlebar > #titlebar-content {
margin-top: 11px;
margin-bottom: 11px;
}
#main-window[customize-entered] #tab-view-deck {
background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
--- a/browser/themes/osx/devtools/computedview.css
+++ b/browser/themes/osx/devtools/computedview.css
@@ -54,26 +54,26 @@ body {
}
.property-view > * {
display: inline-block;
vertical-align: middle;
}
.property-name {
- width: 50%;
+ /* -12px is so the expander triangle isn't pushed up above the property */
+ width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
outline: 0;
}
.property-value {
- width: 50%;
- max-width: 100%;
+ width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-image: url(arrow-e.png);
background-repeat: no-repeat;
background-size: 5px 8px;
background-position: 2px center;
padding-left: 10px;
@@ -88,17 +88,18 @@ body {
padding-left: 8px;
}
@media (min-width: 400px) {
.property-name {
width: 200px;
}
.property-value {
- width: auto;
+ /* -212px is accounting for the 200px property-name and the 12px triangle */
+ width: calc(100% - 212px);
}
}
.property-content {
padding-left: 17px;
}
/* From skin */
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -489,25 +489,25 @@ box.requests-menu-status {
}
#custom-method-value {
width: 4.5em;
}
/* Footer */
-#requests-menu-footer {
- border-top: solid 1px hsla(210,5%,5%,.3);
-}
-
.theme-dark #requests-menu-footer {
+ border-top: 1px solid @table_itemDarkStartBorder@;
+ box-shadow: 0 1px 0 @table_itemDarkEndBorder@ inset;
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
}
.theme-light #requests-menu-footer {
+ border-top: 1px solid @table_itemLightStartBorder@;
+ box-shadow: 0 1px 0 @table_itemLightEndBorder@ inset;
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
}
.requests-menu-footer-button,
.requests-menu-footer-label {
min-width: 1em;
margin: 0;
border: none;
@@ -525,44 +525,51 @@ box.requests-menu-status {
}
.requests-menu-footer-spacer {
min-width: 2px;
}
.theme-dark .requests-menu-footer-spacer:not(:first-child),
.theme-dark .requests-menu-footer-button:not(:first-child) {
- -moz-border-start: 1px solid @table_itemDarkStartBorder@;
- box-shadow: -1px 0 0 @table_itemDarkEndBorder@;
+ -moz-border-start: 1px solid @table_itemDarkEndBorder@;
+ box-shadow: -1px 0 0 @table_itemDarkStartBorder@;
}
.theme-light .requests-menu-footer-spacer:not(:first-child),
.theme-light .requests-menu-footer-button:not(:first-child) {
- -moz-border-start: 1px solid @table_itemLightStartBorder@;
- box-shadow: -1px 0 0 @table_itemLightEndBorder@;
+ -moz-border-start: 1px solid @table_itemLightEndBorder@;
+ box-shadow: -1px 0 0 @table_itemLightStartBorder@;
}
.requests-menu-footer-button {
-moz-appearance: none;
background: rgba(0,0,0,0.025);
}
.requests-menu-footer-button:hover {
- background: rgba(0,0,0,0.20);
+ background: rgba(0,0,0,0.10);
+}
+
+.theme-dark .requests-menu-footer-button:hover:active {
+ background-color: rgba(29,79,115,0.4); /* Select Highlight Blue at 40% opacity */
}
-.requests-menu-footer-button:hover:active {
- background: rgba(0,0,0,0.35);
+.theme-light .requests-menu-footer-button:hover:active {
+ background-color: rgba(76,158,217,0.4); /* Select Highlight Blue at 40% opacity */
}
-.requests-menu-footer-button:not(:active)[checked] {
- background-color: rgba(0,0,0,0.25);
- background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3));
- background-size: 100% 1px;
- background-repeat: no-repeat;
+.theme-dark .requests-menu-footer-button:not(:active)[checked] {
+ background-color: rgba(29,79,115,1); /* Select Highlight Blue */
+ color: rgba(245,247,250,1); /* Light foreground text */
+}
+
+.theme-light .requests-menu-footer-button:not(:active)[checked] {
+ background-color: rgba(76,158,217,1); /* Select Highlight Blue */
+ color: rgba(245,247,250,1); /* Light foreground text */
}
.requests-menu-footer-label {
padding-top: 3px;
font-weight: 600;
}
/* Performance analysis buttons */
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -388,16 +388,20 @@
.devtools-sidebar-tabs > tabs > tab {
background-color: transparent;
}
.theme-dark .devtools-sidebar-tabs > tabs > tab {
background-image: linear-gradient(transparent, transparent), @smallSeparatorDark@;
}
+.theme-dark .devtools-sidebar-tabs > tabs > tab:hover {
+ background-image: linear-gradient(hsla(206,37%,4%,.2), hsla(206,37%,4%,.2)), @smallSeparatorDark@;
+}
+
.theme-dark .devtools-sidebar-tabs > tabs > tab:hover:active {
background-image: linear-gradient(hsla(206,37%,4%,.4), hsla(206,37%,4%,.4)), @smallSeparatorDark@;
}
.theme-dark .devtools-sidebar-tabs > tabs > tab[selected] + tab {
background-image: linear-gradient(transparent, transparent), @solidSeparatorDark@;
}
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -219,46 +219,26 @@
.tab-background-end[selected=true]:-moz-lwtheme::before,
.tab-background-middle[selected=true]:-moz-lwtheme {
background-color: transparent;
}
/* End selected tab */
/* new tab button border and gradient on hover */
+.tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]),
.tabs-newtab-button:hover {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png),
url(chrome://browser/skin/tabbrowser/tab-background-middle.png),
url(chrome://browser/skin/tabbrowser/tab-background-end.png);
background-position: left bottom, @tabCurveWidth@ bottom, right bottom;
background-repeat: no-repeat;
background-size: @tabCurveWidth@ 100%, calc(100% - (2 * @tabCurveWidth@)) 100%, @tabCurveWidth@ 100%;
}
-/* normal tab border and gradient on hover */
-.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-middle:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-middle.png);
- background-repeat: repeat-x;
- background-size: auto 100%;
-}
-
-.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]),
-.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
- background-repeat: no-repeat;
- background-size: 100% 100%;
-}
-
-.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png);
-}
-
-.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
- background-image: url(chrome://browser/skin/tabbrowser/tab-background-end.png);
-}
-
/* Tab pointer-events */
.tabbrowser-tab {
pointer-events: none;
}
.tab-background-middle,
.tabs-newtab-button,
.tab-close-button {
--- a/browser/themes/windows/devtools/computedview.css
+++ b/browser/themes/windows/devtools/computedview.css
@@ -54,26 +54,26 @@ body {
}
.property-view > * {
display: inline-block;
vertical-align: middle;
}
.property-name {
- width: 50%;
+ /* -12px is so the expander triangle isn't pushed up above the property */
+ width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
outline: 0;
}
.property-value {
- width: 50%;
- max-width: 100%;
+ width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-image: url(arrow-e.png);
background-repeat: no-repeat;
background-size: 5px 8px;
background-position: 2px center;
padding-left: 10px;
@@ -88,17 +88,18 @@ body {
padding-left: 8px;
}
@media (min-width: 400px) {
.property-name {
width: 200px;
}
.property-value {
- width: auto;
+ /* -212px is accounting for the 200px property-name and the 12px triangle */
+ width: calc(100% - 212px);
}
}
.property-content {
padding-left: 17px;
}
/* From skin */
--- a/dom/browser-element/mochitest/priority/test_HighPriority.html
+++ b/dom/browser-element/mochitest/priority/test_HighPriority.html
@@ -114,15 +114,16 @@ function runTest() {
document.body.appendChild(iframe);
}
const priorityChangeGracePeriod = 100;
addEventListener('testready', function() {
SpecialPowers.pushPrefEnv(
{set: [['dom.ipc.processPriorityManager.backgroundGracePeriodMS',
- priorityChangeGracePeriod]]},
+ priorityChangeGracePeriod],
+ ['dom.wakelock.enabled', true]]},
runTest);
});
</script>
</body>
</html>
--- a/dom/nfc/MozNDEFRecord.cpp
+++ b/dom/nfc/MozNDEFRecord.cpp
@@ -16,16 +16,17 @@ namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(MozNDEFRecord)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MozNDEFRecord)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ tmp->DropData();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MozNDEFRecord)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(MozNDEFRecord)
--- a/dom/power/moz.build
+++ b/dom/power/moz.build
@@ -1,16 +1,15 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
-if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
- TEST_DIRS += ['test']
+TEST_DIRS += ['test']
XPIDL_SOURCES += [
'nsIDOMWakeLockListener.idl',
'nsIPowerManagerService.idl',
]
XPIDL_MODULE = 'dom_power'
--- a/dom/power/test/browser_wakelocks.js
+++ b/dom/power/test/browser_wakelocks.js
@@ -217,17 +217,20 @@ let gSteps = [
},
];
function runNextStep() {
gCurStepIndex++;
if (gCurStepIndex < gSteps.length) {
gSteps[gCurStepIndex]();
} else {
- SpecialPowers.removePermission("power", kUrlSource);
finish();
}
}
function test() {
- SpecialPowers.addPermission("power", true, kUrlSource);
- runNextStep();
+ SpecialPowers.pushPermissions([
+ {type: "power", allow: true, context: kUrlSource}
+ ], function () {
+ SpecialPowers.pushPrefEnv({"set": [["dom.wakelock.enabled", true]]},
+ runNextStep);
+ });
}
--- a/dom/power/test/mochitest.ini
+++ b/dom/power/test/mochitest.ini
@@ -1,8 +1,13 @@
[DEFAULT]
[test_bug957893.html]
[test_bug957899.html]
+[test_wakelock_not_exposed.html]
+run-if = appname != "b2g"
[test_power_basics.html]
[test_power_set_cpusleepallowed.html]
+skip-if = toolkit != "gonk"
[test_power_set_screen_brightness.html]
+skip-if = toolkit != "gonk"
[test_power_set_screen_enabled.html]
+skip-if = toolkit != "gonk"
--- a/dom/power/test/test_bug957893.html
+++ b/dom/power/test/test_bug957893.html
@@ -2,20 +2,23 @@
<html>
<head>
<title>Test bug 957893 - Crash in WakeLock</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript">
- try {
- var wl = navigator.requestWakeLock('');
- ok(false, "RequestWakeLock throws an exception!");
- } catch(e) {
- ok(true, "RequestWakeLock throws an exception!");
- }
-
- info("Still alive!");
-
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ try {
+ var wl = navigator.requestWakeLock('');
+ ok(false, "RequestWakeLock throws an exception!");
+ } catch(e) {
+ ok(true, "RequestWakeLock throws an exception!");
+ }
+ info("Still alive!");
+ SimpleTest.finish();
+ }
+ SpecialPowers.pushPrefEnv({"set": [["dom.wakelock.enabled", true]]}, test);
</script>
</body>
</html>
--- a/dom/power/test/test_bug957899.html
+++ b/dom/power/test/test_bug957899.html
@@ -2,14 +2,19 @@
<html>
<head>
<title>Test bug 957899 - Crash in WakeLock</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript">
- var wl = navigator.requestWakeLock('a');
- ok(wl, "WakeLock created!");
- ok(!(wl instanceof XPathEvaluator), "Crashing?");
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ var wl = navigator.requestWakeLock('a');
+ ok(wl, "WakeLock created!");
+ ok(!(wl instanceof XPathEvaluator), "Crashing?");
+ SimpleTest.finish();
+ }
+ SpecialPowers.pushPrefEnv({"set": [["dom.wakelock.enabled", true]]}, test);
</script>
</body>
</html>
--- a/dom/power/test/test_power_basics.html
+++ b/dom/power/test/test_power_basics.html
@@ -3,25 +3,29 @@
<head>
<title>Test for Power API</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/** Test for Power API **/
-ok('mozPower' in navigator, "navigator.mozPower should exist");
-
/** Test permission **/
// In b2g, addPermission 'power' is only working after a document reload
// See bug 802312
SimpleTest.waitForExplicitFinish();
function startTest() {
+ SpecialPowers.pushPermissions([
+ {type: "power", allow: true, context: window.frames[0].document}
+ ], doTest1);
+}
+
+function doTest1() {
window.frames[0].frameElement.setAttribute('onload', 'doTest2()');
power = window.frames[0].navigator.mozPower;
ok(power, "Should be able to access power manager with permission.");
SpecialPowers.removePermission("power", window.frames[0].document);
window.frames[0].location.reload();
}
new file mode 100644
--- /dev/null
+++ b/dom/power/test/test_wakelock_not_exposed.html
@@ -0,0 +1,17 @@
+<!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=963366
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test navigator.requestWakeLock is not exposed to non-B2G platform</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+ ok(navigator.requestWakeLock === undefined,
+ "navigator.requestWakeLock is not exposed to non-B2G platform");
+ </script>
+</body>
+</html>
--- a/dom/src/storage/DOMStorage.cpp
+++ b/dom/src/storage/DOMStorage.cpp
@@ -156,33 +156,34 @@ DOMStorage::Clear()
return NS_OK;
}
namespace {
class StorageNotifierRunnable : public nsRunnable
{
public:
- StorageNotifierRunnable(nsISupports* aSubject)
- : mSubject(aSubject)
+ StorageNotifierRunnable(nsISupports* aSubject, const char16_t* aType)
+ : mSubject(aSubject), mType(aType)
{ }
NS_DECL_NSIRUNNABLE
private:
nsCOMPtr<nsISupports> mSubject;
+ const char16_t* mType;
};
NS_IMETHODIMP
StorageNotifierRunnable::Run()
{
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
- observerService->NotifyObservers(mSubject, "dom-storage2-changed", nullptr);
+ observerService->NotifyObservers(mSubject, "dom-storage2-changed", mType);
}
return NS_OK;
}
} // anonymous namespace
void
DOMStorage::BroadcastChangeNotification(const nsSubstring& aKey,
@@ -202,17 +203,21 @@ DOMStorage::BroadcastChangeNotification(
aOldValue,
aNewValue,
mDocumentURI,
static_cast<nsIDOMStorage*>(this));
if (NS_FAILED(rv)) {
return;
}
- nsRefPtr<StorageNotifierRunnable> r = new StorageNotifierRunnable(event);
+ nsRefPtr<StorageNotifierRunnable> r =
+ new StorageNotifierRunnable(event,
+ GetType() == LocalStorage
+ ? MOZ_UTF16("localStorage")
+ : MOZ_UTF16("sessionStorage"));
NS_DispatchToMainThread(r);
}
static const uint32_t ASK_BEFORE_ACCEPT = 1;
static const uint32_t ACCEPT_SESSION = 2;
static const uint32_t BEHAVIOR_REJECT = 2;
static const char kPermissionType[] = "cookie";
--- a/dom/system/OSFileConstants.cpp
+++ b/dom/system/OSFileConstants.cpp
@@ -8,19 +8,23 @@
#include "errno.h"
#include "prsystem.h"
#if defined(XP_UNIX)
#include "unistd.h"
#include "dirent.h"
#include "sys/stat.h"
-#if !defined(ANDROID)
+#if defined(ANDROID)
+#include <sys/vfs.h>
+#define statvfs statfs
+#else
+#include "sys/statvfs.h"
#include <spawn.h>
-#endif // !defined(ANDROID)
+#endif // defined(ANDROID)
#endif // defined(XP_UNIX)
#if defined(XP_LINUX)
#include <linux/fadvise.h>
#endif // defined(XP_LINUX)
#if defined(XP_MACOSX)
#include "copyfile.h"
@@ -521,16 +525,19 @@ static const dom::ConstantSpec gLibcProp
{ "OSFILE_SIZEOF_GID_T", INT_TO_JSVAL(sizeof (gid_t)) },
// The size of |uid_t|.
{ "OSFILE_SIZEOF_UID_T", INT_TO_JSVAL(sizeof (uid_t)) },
// The size of |time_t|.
{ "OSFILE_SIZEOF_TIME_T", INT_TO_JSVAL(sizeof (time_t)) },
+ // The size of |fsblkcnt_t|.
+ { "OSFILE_SIZEOF_FSBLKCNT_T", INT_TO_JSVAL(sizeof (fsblkcnt_t)) },
+
#if !defined(ANDROID)
// The size of |posix_spawn_file_actions_t|.
{ "OSFILE_SIZEOF_POSIX_SPAWN_FILE_ACTIONS_T", INT_TO_JSVAL(sizeof (posix_spawn_file_actions_t)) },
#endif // !defined(ANDROID)
// Defining |dirent|.
// Size
{ "OSFILE_SIZEOF_DIRENT", INT_TO_JSVAL(sizeof (dirent)) },
@@ -580,16 +587,23 @@ static const dom::ConstantSpec gLibcProp
{ "OSFILE_OFFSETOF_STAT_ST_CTIME", INT_TO_JSVAL(offsetof (struct stat, st_ctime)) },
#endif // defined(HAVE_ST_ATIME)
// Several OSes have a birthtime field. For the moment, supporting only Darwin.
#if defined(_DARWIN_FEATURE_64_BIT_INODE)
{ "OSFILE_OFFSETOF_STAT_ST_BIRTHTIME", INT_TO_JSVAL(offsetof (struct stat, st_birthtime)) },
#endif // defined(_DARWIN_FEATURE_64_BIT_INODE)
+ // Defining |statvfs|
+
+ { "OSFILE_SIZEOF_STATVFS", INT_TO_JSVAL(sizeof (struct statvfs)) },
+
+ { "OSFILE_OFFSETOF_STATVFS_F_BSIZE", INT_TO_JSVAL(offsetof (struct statvfs, f_bsize)) },
+ { "OSFILE_OFFSETOF_STATVFS_F_BAVAIL", INT_TO_JSVAL(offsetof (struct statvfs, f_bavail)) },
+
#endif // defined(XP_UNIX)
// System configuration
// Under MacOSX, to avoid using deprecated functions that do not
// match the constants we define in this object (including
--- a/dom/system/gonk/Nfc.js
+++ b/dom/system/gonk/Nfc.js
@@ -397,17 +397,16 @@ function Nfc() {
}
Services.obs.addObserver(this, NFC.TOPIC_MOZSETTINGS_CHANGED, false);
Services.obs.addObserver(this, NFC.TOPIC_XPCOM_SHUTDOWN, false);
Services.obs.addObserver(this, NFC.TOPIC_HARDWARE_STATE, false);
gMessageManager.init(this);
let lock = gSettingsService.createLock();
- lock.get(NFC.SETTING_NFC_POWER_LEVEL, this);
lock.get(NFC.SETTING_NFC_ENABLED, this);
// Maps sessionId (that are generated from nfcd) with a unique guid : 'SessionToken'
this.sessionTokenMap = {};
gSystemWorkerManager.registerNfcWorker(this.worker);
}
Nfc.prototype = {
@@ -518,18 +517,16 @@ Nfc.prototype = {
default:
throw new Error("Don't know about this message type: " + message.type);
}
},
// nsINfcWorker
worker: null,
- powerLevel: NFC.NFC_POWER_LEVEL_DISABLED,
-
sessionTokenMap: null,
/**
* Process a message from the content process.
*/
receiveMessage: function receiveMessage(message) {
debug("Received '" + JSON.stringify(message) + "' message from content process");
@@ -618,25 +615,16 @@ Nfc.prototype = {
* nsISettingsServiceCallback
*/
handle: function handle(aName, aResult) {
switch(aName) {
case NFC.SETTING_NFC_ENABLED:
debug("'nfc.enabled' is now " + aResult);
this._enabled = aResult;
- // General power setting
- let powerLevel = this._enabled ? NFC.NFC_POWER_LEVEL_ENABLED :
- NFC.NFC_POWER_LEVEL_DISABLED;
- // Only if the value changes, set the power config and persist
- if (powerLevel !== this.powerLevel) {
- debug("New Power Level " + powerLevel);
- this.setConfig({powerLevel: powerLevel});
- this.powerLevel = powerLevel;
- }
break;
}
},
/**
* nsIObserver
*/
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -2092,20 +2092,16 @@ RadioInterface.prototype = {
gTelephonyProvider.notifyCallDisconnected(this.clientId, message.call);
break;
case "conferenceCallStateChanged":
gTelephonyProvider.notifyConferenceCallStateChanged(message.state);
break;
case "cdmaCallWaiting":
gTelephonyProvider.notifyCdmaCallWaiting(this.clientId, message.number);
break;
- case "callError":
- gTelephonyProvider.notifyCallError(this.clientId, message.callIndex,
- message.errorMsg);
- break;
case "suppSvcNotification":
gTelephonyProvider.notifySupplementaryService(this.clientId,
message.callIndex,
message.notification);
break;
case "conferenceError":
gTelephonyProvider.notifyConferenceError(message.errorName,
message.errorMsg);
--- a/dom/system/gonk/nfc_consts.js
+++ b/dom/system/gonk/nfc_consts.js
@@ -60,15 +60,14 @@ this.NFC_POWER_LEVEL_UNKNOWN = -1
this.NFC_POWER_LEVEL_DISABLED = 0;
this.NFC_POWER_LEVEL_LOW = 1;
this.NFC_POWER_LEVEL_ENABLED = 2;
this.TOPIC_MOZSETTINGS_CHANGED = "mozsettings-changed";
this.TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown";
this.TOPIC_HARDWARE_STATE = "nfc-hardware-state-change";
this.SETTING_NFC_ENABLED = "nfc.enabled";
-this.SETTING_NFC_POWER_LEVEL = "nfc.powerlevel";
this.NFC_PEER_EVENT_READY = 0x01;
this.NFC_PEER_EVENT_LOST = 0x02;
// Allow this file to be imported via Components.utils.import().
this.EXPORTED_SYMBOLS = Object.keys(this);
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -1339,19 +1339,21 @@ let RIL = {
* @param number
* String containing the number to dial.
* @param clirMode
* Integer for showing/hidding the caller Id to the called party.
* @param uusInfo
* Integer doing something XXX TODO
*/
dial: function(options) {
- let onerror = (function onerror(errorMsg) {
- this._sendCallError(-1, errorMsg);
- }).bind(this);
+ let onerror = (function onerror(options, errorMsg) {
+ options.success = false;
+ options.errorMsg = errorMsg;
+ this.sendChromeMessage(options);
+ }).bind(this, options);
if (this._isEmergencyNumber(options.number)) {
this.dialEmergencyNumber(options, onerror);
} else {
if (!this._isCdma) {
// TODO: Both dial() and sendMMI() functions should be unified at some
// point in the future. In the mean time we handle temporary CLIR MMI
// commands through the dial() function. Please see bug 889737.
@@ -1405,17 +1407,17 @@ let RIL = {
this.setRadioEnabled({enabled: true});
return;
}
this.sendDialRequest(options);
},
sendDialRequest: function(options) {
- Buf.newParcel(options.request);
+ Buf.newParcel(options.request, options);
Buf.writeString(options.number);
Buf.writeInt32(options.clirMode || 0);
Buf.writeInt32(options.uusInfo || 0);
// TODO Why do we need this extra 0? It was put it in to make this
// match the format of the binary message.
Buf.writeInt32(0);
Buf.sendParcel();
},
@@ -1921,18 +1923,18 @@ let RIL = {
RIL_REQUEST_GPRS_DETACH;
this._attachDataRegistration = options.attach;
Buf.simpleRequest(request);
},
/**
* Get failure casue code for the most recently failed PDP context.
*/
- getFailCauseCode: function(options) {
- Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE, options);
+ getFailCauseCode: function(callback) {
+ Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE, {callback: callback});
},
/**
* Helper to parse MMI/USSD string. TS.22.030 Figure 3.5.3.2.
*/
_parseMMI: function(mmiString) {
if (!mmiString || !mmiString.length) {
return null;
@@ -3477,17 +3479,20 @@ let RIL = {
delete this.currentConference.participants[currentCall.callIndex];
delete this.currentCalls[currentCall.callIndex];
// We don't query the fail cause here as it triggers another asynchrouns
// request that leads to a problem of updating all conferece participants
// in one task.
this._handleDisconnectedCall(currentCall);
} else {
delete this.currentCalls[currentCall.callIndex];
- this.getFailCauseCode(currentCall);
+ this.getFailCauseCode((function(call, failCause) {
+ call.failCause = failCause;
+ this._handleDisconnectedCall(call);
+ }).bind(this, currentCall));
}
continue;
}
// Call is still valid.
if (newCall.state == currentCall.state &&
newCall.isMpty == currentCall.isMpty) {
continue;
@@ -3659,22 +3664,16 @@ let RIL = {
},
_handleDisconnectedCall: function(disconnectedCall) {
let message = {rilMessageType: "callDisconnected",
call: disconnectedCall};
this.sendChromeMessage(message);
},
- _sendCallError: function(callIndex, errorMsg) {
- this.sendChromeMessage({rilMessageType: "callError",
- callIndex: callIndex,
- errorMsg: errorMsg});
- },
-
_sendDataCallError: function(message, errorCode) {
// Should not include token for unsolicited response.
delete message.rilMessageToken;
message.rilMessageType = "datacallerror";
if (errorCode == ERROR_GENERIC_FAILURE) {
message.errorMsg = RIL_ERROR_TO_GECKO_ERROR[errorCode];
} else {
message.errorMsg = RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[errorCode];
@@ -5098,20 +5097,24 @@ RIL[REQUEST_GET_CURRENT_CALLS] = functio
};
}
calls[call.callIndex] = call;
}
this._processCalls(calls);
};
RIL[REQUEST_DIAL] = function REQUEST_DIAL(length, options) {
- if (options.rilRequestError) {
- // The connection is not established yet.
- options.callIndex = -1;
- this.getFailCauseCode(options);
+ options.success = (options.rilRequestError === 0);
+ if (options.success) {
+ this.sendChromeMessage(options);
+ } else {
+ this.getFailCauseCode((function(options, failCause) {
+ options.errorMsg = failCause;
+ this.sendChromeMessage(options);
+ }).bind(this, options));
}
};
RIL[REQUEST_GET_IMSI] = function REQUEST_GET_IMSI(length, options) {
if (options.rilRequestError) {
return;
}
this.iccInfoPrivate.imsi = Buf.readString();
@@ -5168,30 +5171,21 @@ RIL[REQUEST_CONFERENCE] = function REQUE
errorName: "addError",
errorMsg: RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError]};
this.sendChromeMessage(options);
return;
}
};
RIL[REQUEST_UDUB] = null;
RIL[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) {
- let num = 0;
- if (length) {
- num = Buf.readInt32();
- }
- if (!num) {
- // No response of REQUEST_LAST_CALL_FAIL_CAUSE. Change the call state into
- // 'disconnected' directly.
- this._handleDisconnectedCall(options);
- return;
- }
-
- let failCause = Buf.readInt32();
- options.failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[failCause];
- this._handleDisconnectedCall(options);
+ let num = length ? Buf.readInt32() : 0;
+ let failCause = num ? RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[Buf.readInt32()] : null;
+ if (options.callback) {
+ options.callback(failCause);
+ }
};
RIL[REQUEST_SIGNAL_STRENGTH] = function REQUEST_SIGNAL_STRENGTH(length, options) {
this._receivedNetworkInfo(NETWORK_INFO_SIGNAL);
if (options.rilRequestError) {
return;
}
--- a/dom/telephony/gonk/TelephonyProvider.js
+++ b/dom/telephony/gonk/TelephonyProvider.js
@@ -317,33 +317,16 @@ TelephonyProvider.prototype = {
default:
if (DEBUG) {
debug("Unknown rilSuppSvcNotification: " + aNotification);
}
return;
}
},
- _validateNumber: function(aNumber) {
- // note: isPlainPhoneNumber also accepts USSD and SS numbers
- if (gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
- return true;
- }
-
- let errorMsg = RIL.RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[RIL.CALL_FAIL_UNOBTAINABLE_NUMBER];
- let currentThread = Services.tm.currentThread;
- currentThread.dispatch(this.notifyCallError.bind(this, -1, errorMsg),
- Ci.nsIThread.DISPATCH_NORMAL);
- if (DEBUG) {
- debug("Number '" + aNumber + "' doesn't seem to be a viable number. Drop.");
- }
-
- return false;
- },
-
_updateDebugFlag: function() {
try {
DEBUG = RIL.DEBUG_RIL ||
Services.prefs.getBoolPref(kPrefRilDebuggingEnabled);
} catch (e) {}
},
_getDefaultServiceId: function() {
@@ -414,27 +397,42 @@ TelephonyProvider.prototype = {
}
promise.then(function() {
aListener.enumerateCallStateComplete();
});
},
dial: function(aClientId, aNumber, aIsEmergency) {
if (DEBUG) debug("Dialing " + (aIsEmergency ? "emergency " : "") + aNumber);
+
// we don't try to be too clever here, as the phone is probably in the
// locked state. Let's just check if it's a number without normalizing
if (!aIsEmergency) {
aNumber = gPhoneNumberUtils.normalize(aNumber);
}
- if (this._validateNumber(aNumber)) {
- this._getClient(aClientId).sendWorkerMessage("dial", {
- number: aNumber,
- isDialEmergency: aIsEmergency
- });
+
+ if (!gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
+ // Note: isPlainPhoneNumber also accepts USSD and SS numbers
+ if (DEBUG) debug("Number '" + aNumber + "' is not viable. Drop.");
+ let errorMsg = RIL.RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[RIL.CALL_FAIL_UNOBTAINABLE_NUMBER];
+ Services.tm.currentThread.dispatch(
+ this.notifyCallError.bind(this, aClientId, -1, errorMsg),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ return;
}
+
+ this._getClient(aClientId).sendWorkerMessage("dial", {
+ number: aNumber,
+ isDialEmergency: aIsEmergency
+ }, (function(clientId, response) {
+ if (!response.success) {
+ this.notifyCallError(clientId, -1, response.errorMsg);
+ }
+ return false;
+ }).bind(this, aClientId));
},
hangUp: function(aClientId, aCallIndex) {
this._getClient(aClientId).sendWorkerMessage("hangUp", { callIndex: aCallIndex });
},
startTone: function(aClientId, aDtmfChar) {
this._getClient(aClientId).sendWorkerMessage("startTone", { dtmfChar: aDtmfChar });
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -672,17 +672,17 @@ var interfaceNamesInGlobalScope =
{name: "MozStkCommandEvent", b2g: true, pref: "dom.icc.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozTimeManager", b2g: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozVoicemail", b2g: true, pref: "dom.voicemail.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozVoicemailEvent", b2g: true, pref: "dom.voicemail.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!
- "MozWakeLock",
+ {name: "MozWakeLock", b2g: true, pref: "dom.wakelock.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozWifiConnectionInfoEvent", b2g: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozWifiStatusChangeEvent", b2g: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozWifiP2pGroupOwner", b2g: true, permission: "wifi-manage"},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozWifiP2pManager", b2g: true, permission: "wifi-manage"},
--- a/dom/tests/mochitest/storageevent/mochitest.ini
+++ b/dom/tests/mochitest/storageevent/mochitest.ini
@@ -7,10 +7,11 @@ support-files =
frameSessionStorageMasterNotEqual.html
frameSessionStorageSlaveEqual.html
frameSessionStorageSlaveNotEqual.html
interOriginFrame.js
interOriginTest2.js
[test_storageLocalStorageEventCheckNoPropagation.html]
[test_storageLocalStorageEventCheckPropagation.html]
+[test_storageNotifications.html]
[test_storageSessionStorageEventCheckNoPropagation.html]
[test_storageSessionStorageEventCheckPropagation.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/storageevent/test_storageNotifications.html
@@ -0,0 +1,127 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>sessionStorage basic test</title>
+
+<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script type="application/javascript;version=1.7">
+
+var expectedTypes = [
+ "localStorage",
+ "localStorage",
+ "sessionStorage",
+ "localStorage",
+ "sessionStorage",
+ "sessionStorage",
+ "localStorage",
+ "sessionStorage",
+ "localStorage",
+ "sessionStorage",
+ "localStorage",
+ "sessionStorage",
+ "sessionStorage",
+ "localStorage",
+ "sessionStorage",
+ "localStorage",
+];
+
+var tests = Tests();
+function setup() {
+ sessionStorage.clear();
+ SimpleTest.executeSoon(function() {
+ tests.next();
+ });
+}
+
+function Tests()
+{
+ // Initially check the both storages are empty
+ is(sessionStorage.length, 0, "Session storage is empty [1]");
+ is(localStorage.length, 0, "Local storage is empty [1]");
+
+ var onStorageChanged = {
+ observe: function(subject, topic, type) {
+ if (topic == "dom-storage2-changed") {
+ ok(expectedTypes.length > 0, "Not more then expected events encountered");
+ is(type, expectedTypes.shift(), "Expected type of the storage notificaiton");
+ tests.next();
+ }
+ }
+ }
+
+ // Listen for dom-storage2-changed notification
+ SpecialPowers.Services.obs.addObserver(onStorageChanged,
+ "dom-storage2-changed", false);
+
+ // add an empty-value key
+ localStorage.setItem("empty", "");
+ yield undefined;
+
+ localStorage.setItem("empty", "value-1");
+ yield undefined;
+
+ sessionStorage.setItem("empty", "");
+ yield undefined;
+
+ localStorage.removeItem("empty");
+ yield undefined;
+
+ sessionStorage.setItem("empty", "value-1");
+ yield undefined;
+
+ sessionStorage.removeItem("empty");
+ yield undefined;
+
+ localStorage.setItem("key1", "value-1");
+ yield undefined;
+
+ sessionStorage.setItem("key2", "value-2");
+ yield undefined;
+
+ localStorage.setItem("key1", "value-1-2");
+ yield undefined;
+
+ sessionStorage.setItem("key2", "value-2-2");
+ yield undefined;
+
+ localStorage.setItem("key3", "value-3");
+ yield undefined;
+
+ sessionStorage.setItem("key4", "value-4");
+ yield undefined;
+
+ sessionStorage.removeItem("key4");
+ yield undefined;
+
+ localStorage.setItem("key4", "value-4");
+ yield undefined;
+
+ sessionStorage.clear();
+ yield undefined;
+
+ localStorage.clear();
+ yield undefined;
+
+ SimpleTest.executeSoon(function () {
+ SpecialPowers.Services.obs.removeObserver(onStorageChanged,
+ "dom-storage2-changed", false);
+ is(expectedTypes.length, 0, "received the correct number of events");
+
+ sessionStorage.clear();
+ localStorage.clear();
+ tests = null;
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body onload="setup();">
+
+</body>
+</html>
--- a/dom/webidl/MozWakeLock.webidl
+++ b/dom/webidl/MozWakeLock.webidl
@@ -1,15 +1,15 @@
/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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/. */
-[Func="Navigator::HasWakeLockSupport"]
+[Pref="dom.wakelock.enabled", Func="Navigator::HasWakeLockSupport"]
interface MozWakeLock
{
readonly attribute DOMString topic;
/**
* Release the wake lock.
* @throw NS_ERROR_DOM_INVALID_STATE_ERR if already unlocked.
*/
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -198,17 +198,17 @@ partial interface Navigator {
* all locks on the topic have been released.
*
* The returned MozWakeLock object is a token of the lock. You can
* unlock the lock via the object's |unlock| method. The lock is released
* automatically when its associated window is unloaded.
*
* @param aTopic resource name
*/
- [Throws, Func="Navigator::HasWakeLockSupport"]
+ [Throws, Pref="dom.wakelock.enabled", Func="Navigator::HasWakeLockSupport"]
MozWakeLock requestWakeLock(DOMString aTopic);
};
// nsIDOMNavigatorDeviceStorage
partial interface Navigator {
[Throws, Pref="device.storage.enabled"]
DeviceStorage? getDeviceStorage(DOMString type);
[Throws, Pref="device.storage.enabled"]
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -812,17 +812,17 @@ pref("browser.snippets.updateInterval",
// URL used to check for user's country code
pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
// URL used to ping metrics with stats about which snippets have been shown
pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
// These prefs require a restart to take effect.
-pref("browser.snippets.enabled", true);
+pref("browser.snippets.enabled", false);
pref("browser.snippets.syncPromo.enabled", false);
#ifdef MOZ_ANDROID_SYNTHAPKS
// The URL of the APK factory from which we obtain APKs for webapps.
// This currently points to the development server.
pref("browser.webapps.apkFactoryUrl", "http://dapk.net/application.apk");
// How frequently to check for webapp updates, in seconds (86400 is daily).
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -96,37 +96,45 @@ public class BrowserContract {
public static final String GUID = "guid";
public static final String TIME_DELETED = "timeDeleted";
}
@RobocopTarget
public static final class Favicons implements CommonColumns, DateSyncColumns {
private Favicons() {}
+ public static final String TABLE_NAME = "favicons";
+
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons");
public static final String URL = "url";
public static final String DATA = "data";
public static final String PAGE_URL = "page_url";
}
@RobocopTarget
public static final class Thumbnails implements CommonColumns {
private Thumbnails() {}
+ public static final String TABLE_NAME = "thumbnails";
+
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails");
public static final String URL = "url";
public static final String DATA = "data";
}
@RobocopTarget
public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns {
private Bookmarks() {}
+ public static final String TABLE_NAME = "bookmarks";
+
+ public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons";
+
public static final int FIXED_ROOT_ID = 0;
public static final int FAKE_DESKTOP_FOLDER_ID = -1;
public static final int FIXED_READING_LIST_ID = -2;
public static final int FIXED_PINNED_LIST_ID = -3;
public static final String MOBILE_FOLDER_GUID = "mobile";
public static final String PLACES_FOLDER_GUID = "places";
public static final String MENU_FOLDER_GUID = "menu";
@@ -157,26 +165,36 @@ public class BrowserContract {
public static final String TAGS = "tags";
public static final String DESCRIPTION = "description";
public static final String KEYWORD = "keyword";
}
@RobocopTarget
public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns {
private History() {}
+
+ public static final String TABLE_NAME = "history";
+
+ public static final String VIEW_WITH_FAVICONS = "history_with_favicons";
+
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
}
// Combined bookmarks and history
@RobocopTarget
public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns {
private Combined() {}
+
+ public static final String VIEW_NAME = "combined";
+
+ public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
+
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
public static final int DISPLAY_NORMAL = 0;
public static final int DISPLAY_READER = 1;
public static final String BOOKMARK_ID = "bookmark_id";
public static final String HISTORY_ID = "history_id";
public static final String DISPLAY = "display";
@@ -300,9 +318,52 @@ public class BrowserContract {
public static final String DATASET_ID = "dataset_id";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String DESCRIPTION = "description";
public static final String IMAGE_URL = "image_url";
public static final String CREATED = "created";
}
+
+ /*
+ * Contains names and schema definitions for tables and views
+ * no longer being used by current ContentProviders. These values are used
+ * to make incremental updates to the schema during a database upgrade. Will be
+ * removed with bug 947018.
+ */
+ static final class Obsolete {
+ public static final String TABLE_IMAGES = "images";
+ public static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
+ public static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
+ public static final String VIEW_COMBINED_WITH_IMAGES = "combined_with_images";
+
+ public static final class Images implements CommonColumns, SyncColumns {
+ private Images() {}
+
+ public static final String URL = "url_key";
+ public static final String FAVICON_URL = "favicon_url";
+ public static final String FAVICON = "favicon";
+ public static final String THUMBNAIL = "thumbnail";
+ public static final String _ID = "_id";
+ public static final String GUID = "guid";
+ public static final String DATE_CREATED = "created";
+ public static final String DATE_MODIFIED = "modified";
+ public static final String IS_DELETED = "deleted";
+ }
+
+ public static final class Combined {
+ private Combined() {}
+
+ public static final String THUMBNAIL = "thumbnail";
+ }
+
+ static final String TABLE_BOOKMARKS_JOIN_IMAGES = Bookmarks.TABLE_NAME + " LEFT OUTER JOIN " +
+ Obsolete.TABLE_IMAGES + " ON " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " +
+ DBUtils.qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
+
+ static final String TABLE_HISTORY_JOIN_IMAGES = History.TABLE_NAME + " LEFT OUTER JOIN " +
+ Obsolete.TABLE_IMAGES + " ON " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, History.URL) + " = " +
+ DBUtils.qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
+
+ static final String FAVICON_DB = "favicon_urls.db";
+ }
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -0,0 +1,1748 @@
+/* -*- 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.db;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Distribution;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
+import org.mozilla.gecko.db.BrowserContract.Favicons;
+import org.mozilla.gecko.db.BrowserContract.History;
+import org.mozilla.gecko.db.BrowserContract.Obsolete;
+import org.mozilla.gecko.db.BrowserContract.Thumbnails;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+final class BrowserDatabaseHelper extends SQLiteOpenHelper {
+
+ private static final String LOGTAG = "GeckoBrowserDBHelper";
+ public static final int DATABASE_VERSION = 17;
+ public static final String DATABASE_NAME = "browser.db";
+
+ final protected Context mContext;
+
+ static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
+ static final String TABLE_HISTORY = History.TABLE_NAME;
+ static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
+ static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
+
+ static final String VIEW_COMBINED = Combined.VIEW_NAME;
+ static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
+ static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
+ static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
+
+ static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
+ TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " +
+ qualifyColumn(TABLE_FAVICONS, Favicons._ID);
+
+ static final String TABLE_HISTORY_JOIN_FAVICONS = TABLE_HISTORY + " LEFT OUTER JOIN " +
+ TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " = " +
+ qualifyColumn(TABLE_FAVICONS, Favicons._ID);
+
+ static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
+ static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp";
+ static final String TABLE_IMAGES_TMP = Obsolete.TABLE_IMAGES + "_tmp";
+
+ private static final String[] mobileIdColumns = new String[] { Bookmarks._ID };
+ private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID };
+
+ public BrowserDatabaseHelper(Context context, String databasePath) {
+ super(context, databasePath, null, DATABASE_VERSION);
+ mContext = context;
+ }
+
+ private void createBookmarksTable(SQLiteDatabase db) {
+ debug("Creating " + TABLE_BOOKMARKS + " table");
+
+ // Android versions older than Froyo ship with an sqlite
+ // that doesn't support foreign keys.
+ String foreignKeyOnParent = null;
+ if (Build.VERSION.SDK_INT >= 8) {
+ foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
+ ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
+ }
+
+ db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
+ Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Bookmarks.TITLE + " TEXT," +
+ Bookmarks.URL + " TEXT," +
+ Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
+ Bookmarks.PARENT + " INTEGER," +
+ Bookmarks.POSITION + " INTEGER NOT NULL," +
+ Bookmarks.KEYWORD + " TEXT," +
+ Bookmarks.DESCRIPTION + " TEXT," +
+ Bookmarks.TAGS + " TEXT," +
+ Bookmarks.DATE_CREATED + " INTEGER," +
+ Bookmarks.DATE_MODIFIED + " INTEGER," +
+ Bookmarks.GUID + " TEXT NOT NULL," +
+ Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
+ (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
+ ");");
+
+ db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.URL + ")");
+ db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
+ db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.GUID + ")");
+ db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.DATE_MODIFIED + ")");
+ }
+
+ private void createBookmarksTableOn13(SQLiteDatabase db) {
+ debug("Creating " + TABLE_BOOKMARKS + " table");
+
+ // Android versions older than Froyo ship with an sqlite
+ // that doesn't support foreign keys.
+ String foreignKeyOnParent = null;
+ if (Build.VERSION.SDK_INT >= 8) {
+ foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
+ ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
+ }
+
+ db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
+ Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Bookmarks.TITLE + " TEXT," +
+ Bookmarks.URL + " TEXT," +
+ Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
+ Bookmarks.PARENT + " INTEGER," +
+ Bookmarks.POSITION + " INTEGER NOT NULL," +
+ Bookmarks.KEYWORD + " TEXT," +
+ Bookmarks.DESCRIPTION + " TEXT," +
+ Bookmarks.TAGS + " TEXT," +
+ Bookmarks.FAVICON_ID + " INTEGER," +
+ Bookmarks.DATE_CREATED + " INTEGER," +
+ Bookmarks.DATE_MODIFIED + " INTEGER," +
+ Bookmarks.GUID + " TEXT NOT NULL," +
+ Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
+ (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
+ ");");
+
+ db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.URL + ")");
+ db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
+ db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.GUID + ")");
+ db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.DATE_MODIFIED + ")");
+ }
+
+ private void createHistoryTable(SQLiteDatabase db) {
+ debug("Creating " + TABLE_HISTORY + " table");
+ db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
+ History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ History.TITLE + " TEXT," +
+ History.URL + " TEXT NOT NULL," +
+ History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
+ History.DATE_LAST_VISITED + " INTEGER," +
+ History.DATE_CREATED + " INTEGER," +
+ History.DATE_MODIFIED + " INTEGER," +
+ History.GUID + " TEXT NOT NULL," +
+ History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
+ ");");
+
+ db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
+ + History.URL + ")");
+ db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
+ + History.GUID + ")");
+ db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
+ + History.DATE_MODIFIED + ")");
+ db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
+ + History.DATE_LAST_VISITED + ")");
+ }
+
+ private void createHistoryTableOn13(SQLiteDatabase db) {
+ debug("Creating " + TABLE_HISTORY + " table");
+ db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
+ History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ History.TITLE + " TEXT," +
+ History.URL + " TEXT NOT NULL," +
+ History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
+ History.FAVICON_ID + " INTEGER," +
+ History.DATE_LAST_VISITED + " INTEGER," +
+ History.DATE_CREATED + " INTEGER," +
+ History.DATE_MODIFIED + " INTEGER," +
+ History.GUID + " TEXT NOT NULL," +
+ History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
+ ");");
+
+ db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
+ + History.URL + ")");
+ db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
+ + History.GUID + ")");
+ db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
+ + History.DATE_MODIFIED + ")");
+ db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
+ + History.DATE_LAST_VISITED + ")");
+ }
+
+ private void createImagesTable(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.TABLE_IMAGES + " table");
+ db.execSQL("CREATE TABLE " + Obsolete.TABLE_IMAGES + " (" +
+ Obsolete.Images._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Obsolete.Images.URL + " TEXT UNIQUE NOT NULL," +
+ Obsolete.Images.FAVICON + " BLOB," +
+ Obsolete.Images.FAVICON_URL + " TEXT," +
+ Obsolete.Images.THUMBNAIL + " BLOB," +
+ Obsolete.Images.DATE_CREATED + " INTEGER," +
+ Obsolete.Images.DATE_MODIFIED + " INTEGER," +
+ Obsolete.Images.GUID + " TEXT NOT NULL," +
+ Obsolete.Images.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
+ ");");
+
+ db.execSQL("CREATE INDEX images_url_index ON " + Obsolete.TABLE_IMAGES + "("
+ + Obsolete.Images.URL + ")");
+ db.execSQL("CREATE UNIQUE INDEX images_guid_index ON " + Obsolete.TABLE_IMAGES + "("
+ + Obsolete.Images.GUID + ")");
+ db.execSQL("CREATE INDEX images_modified_index ON " + Obsolete.TABLE_IMAGES + "("
+ + Obsolete.Images.DATE_MODIFIED + ")");
+ }
+
+ private void createFaviconsTable(SQLiteDatabase db) {
+ debug("Creating " + TABLE_FAVICONS + " table");
+ db.execSQL("CREATE TABLE " + TABLE_FAVICONS + " (" +
+ Favicons._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Favicons.URL + " TEXT UNIQUE," +
+ Favicons.DATA + " BLOB," +
+ Favicons.DATE_CREATED + " INTEGER," +
+ Favicons.DATE_MODIFIED + " INTEGER" +
+ ");");
+
+ db.execSQL("CREATE INDEX favicons_url_index ON " + TABLE_FAVICONS + "("
+ + Favicons.URL + ")");
+ db.execSQL("CREATE INDEX favicons_modified_index ON " + TABLE_FAVICONS + "("
+ + Favicons.DATE_MODIFIED + ")");
+ }
+
+ private void createThumbnailsTable(SQLiteDatabase db) {
+ debug("Creating " + TABLE_THUMBNAILS + " table");
+ db.execSQL("CREATE TABLE " + TABLE_THUMBNAILS + " (" +
+ Thumbnails._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Thumbnails.URL + " TEXT UNIQUE," +
+ Thumbnails.DATA + " BLOB" +
+ ");");
+
+ db.execSQL("CREATE INDEX thumbnails_url_index ON " + TABLE_THUMBNAILS + "("
+ + Thumbnails.URL + ")");
+ }
+
+ private void createBookmarksWithImagesView(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " AS " +
+ "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
+ ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
+ Obsolete.TABLE_BOOKMARKS_JOIN_IMAGES);
+ }
+
+ private void createBookmarksWithFaviconsView(SQLiteDatabase db) {
+ debug("Creating " + VIEW_BOOKMARKS_WITH_FAVICONS + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_FAVICONS + " AS " +
+ "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
+ ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Bookmarks.FAVICON +
+ ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Bookmarks.FAVICON_URL +
+ " FROM " + TABLE_BOOKMARKS_JOIN_FAVICONS);
+ }
+
+ private void createHistoryWithImagesView(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " AS " +
+ "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
+ ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
+ Obsolete.TABLE_HISTORY_JOIN_IMAGES);
+ }
+
+ private void createHistoryWithFaviconsView(SQLiteDatabase db) {
+ debug("Creating " + VIEW_HISTORY_WITH_FAVICONS + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " +
+ "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
+ ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON +
+ ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL +
+ " FROM " + TABLE_HISTORY_JOIN_FAVICONS);
+ }
+
+ private void createCombinedWithImagesView(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritze bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
+ ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
+ " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
+ }
+
+ private void createCombinedWithImagesViewOn9(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritze bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
+ ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
+ " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
+ }
+
+ private void createCombinedWithImagesViewOn10(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritze bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
+ ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
+ " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
+ }
+
+ private void createCombinedWithImagesViewOn11(SQLiteDatabase db) {
+ debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritze bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ // Only use DISPLAY_READER if the matching bookmark entry inside reading
+ // list folder is not marked as deleted.
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
+ " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
+ ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
+ " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
+ }
+
+ private void createCombinedViewOn12(SQLiteDatabase db) {
+ debug("Creating " + VIEW_COMBINED + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritze bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ // Only use DISPLAY_READER if the matching bookmark entry inside reading
+ // list folder is not marked as deleted.
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
+ " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
+ ")");
+
+ debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
+ " SELECT *, " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
+ qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
+ " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
+ " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
+ }
+
+ private void createCombinedViewOn13(SQLiteDatabase db) {
+ debug("Creating " + VIEW_COMBINED + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ Combined.FAVICON_ID +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritize bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ // Only use DISPLAY_READER if the matching bookmark entry inside reading
+ // list folder is not marked as deleted.
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
+ " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
+ ")");
+
+ debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
+ " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
+ qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
+ qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
+ " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
+ " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
+ }
+
+ private void createCombinedViewOn16(SQLiteDatabase db) {
+ debug("Creating " + VIEW_COMBINED + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
+ " SELECT " + Combined.BOOKMARK_ID + ", " +
+ Combined.HISTORY_ID + ", " +
+ // We need to return an _id column because CursorAdapter requires it for its
+ // default implementation for the getItemId() method. However, since
+ // we're not using this feature in the parts of the UI using this view,
+ // we can just use 0 for all rows.
+ "0 AS " + Combined._ID + ", " +
+ Combined.URL + ", " +
+ Combined.TITLE + ", " +
+ Combined.VISITS + ", " +
+ Combined.DISPLAY + ", " +
+ Combined.DATE_LAST_VISITED + ", " +
+ Combined.FAVICON_ID +
+ " FROM (" +
+ // Bookmarks without history.
+ " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ "-1 AS " + Combined.HISTORY_ID + ", " +
+ "-1 AS " + Combined.VISITS + ", " +
+ "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
+ " FROM " + TABLE_BOOKMARKS +
+ " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+ // Ignore pinned bookmarks.
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+ " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+ " UNION ALL" +
+ // History with and without bookmark.
+ " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
+ // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
+ // completely ignore them here because they're joined with history entries we care about.
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
+ Bookmarks.FIXED_PINNED_LIST_ID + " THEN NULL ELSE " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " END " +
+ "ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
+ // Prioritize bookmark titles over history titles, since the user may have
+ // customized the title for a bookmark.
+ "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+ qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
+ // Only use DISPLAY_READER if the matching bookmark entry inside reading
+ // list folder is not marked as deleted.
+ "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
+ " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
+ Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
+ qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
+ qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
+ qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
+ qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
+ " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+ " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+ " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
+ qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+ qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
+ ")");
+
+ debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
+
+ db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
+ " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
+ qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
+ qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
+ " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
+ " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ debug("Creating browser.db: " + db.getPath());
+
+ createBookmarksTableOn13(db);
+ createHistoryTableOn13(db);
+ createFaviconsTable(db);
+ createThumbnailsTable(db);
+
+ createBookmarksWithFaviconsView(db);
+ createHistoryWithFaviconsView(db);
+ createCombinedViewOn16(db);
+
+ createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
+ R.string.bookmarks_folder_places, 0);
+
+ createOrUpdateAllSpecialFolders(db);
+
+ // Create distribution bookmarks before our own default bookmarks
+ int pos = createDistributionBookmarks(db);
+ createDefaultBookmarks(db, pos);
+ }
+
+ private String getLocalizedProperty(JSONObject bookmark, String property, Locale locale) throws JSONException {
+ // Try the full locale
+ String fullLocale = property + "." + locale.toString();
+ if (bookmark.has(fullLocale)) {
+ return bookmark.getString(fullLocale);
+ }
+ // Try without a variant
+ if (!TextUtils.isEmpty(locale.getVariant())) {
+ String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_"));
+ if (bookmark.has(noVariant)) {
+ return bookmark.getString(noVariant);
+ }
+ }
+ // Try just the language
+ String lang = property + "." + locale.getLanguage();
+ if (bookmark.has(lang)) {
+ return bookmark.getString(lang);
+ }
+ // Default to the non-localized property name
+ return bookmark.getString(property);
+ }
+
+ // Returns the number of bookmarks inserted in the db
+ private int createDistributionBookmarks(SQLiteDatabase db) {
+ JSONArray bookmarks = Distribution.getBookmarks(mContext);
+ if (bookmarks == null) {
+ return 0;
+ }
+
+ Locale locale = Locale.getDefault();
+ int pos = 0;
+ Integer mobileFolderId = getMobileFolderId(db);
+ if (mobileFolderId == null) {
+ Log.e(LOGTAG, "Error creating distribution bookmarks: mobileFolderId is null");
+ return 0;
+ }
+
+ for (int i = 0; i < bookmarks.length(); i++) {
+ try {
+ final JSONObject bookmark = bookmarks.getJSONObject(i);
+
+ String title = getLocalizedProperty(bookmark, "title", locale);
+ final String url = getLocalizedProperty(bookmark, "url", locale);
+ createBookmark(db, title, url, pos, mobileFolderId);
+
+ if (bookmark.has("pinned")) {
+ try {
+ // Create a fake bookmark in the hidden pinned folder to pin bookmark
+ // to about:home top sites. Pass pos as the pinned position to pin
+ // sites in the order that bookmarks are specified in bookmarks.json.
+ if (bookmark.getBoolean("pinned")) {
+ createBookmark(db, title, url, pos, Bookmarks.FIXED_PINNED_LIST_ID);
+ }
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error pinning bookmark to top sites", e);
+ }
+ }
+
+ pos++;
+
+ // return early if there is no icon for this bookmark
+ if (!bookmark.has("icon")) {
+ continue;
+ }
+
+ // create icons in a separate thread to avoid blocking about:home on startup
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ SQLiteDatabase db = getWritableDatabase();
+ try {
+ String iconData = bookmark.getString("icon");
+ Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconData);
+ if (icon != null) {
+ createFavicon(db, url, icon);
+ }
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error creating distribution bookmark icon", e);
+ }
+ }
+ });
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error creating distribution bookmark", e);
+ }
+ }
+ return pos;
+ }
+
+ // Inserts default bookmarks, starting at a specified position
+ private void createDefaultBookmarks(SQLiteDatabase db, int pos) {
+ Class<?> stringsClass = R.string.class;
+ Field[] fields = stringsClass.getFields();
+ Pattern p = Pattern.compile("^bookmarkdefaults_title_");
+
+ Integer mobileFolderId = getMobileFolderId(db);
+ if (mobileFolderId == null) {
+ Log.e(LOGTAG, "Error creating default bookmarks: mobileFolderId is null");
+ return;
+ }
+
+ for (int i = 0; i < fields.length; i++) {
+ final String name = fields[i].getName();
+ Matcher m = p.matcher(name);
+ if (!m.find()) {
+ continue;
+ }
+ try {
+ int titleid = fields[i].getInt(null);
+ String title = mContext.getString(titleid);
+
+ Field urlField = stringsClass.getField(name.replace("_title_", "_url_"));
+ int urlId = urlField.getInt(null);
+ final String url = mContext.getString(urlId);
+ createBookmark(db, title, url, pos, mobileFolderId);
+
+ // create icons in a separate thread to avoid blocking about:home on startup
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ SQLiteDatabase db = getWritableDatabase();
+ Bitmap icon = getDefaultFaviconFromPath(name);
+ if (icon == null) {
+ icon = getDefaultFaviconFromDrawable(name);
+ }
+ if (icon != null) {
+ createFavicon(db, url, icon);
+ }
+ }
+ });
+ pos++;
+ } catch (java.lang.IllegalAccessException ex) {
+ Log.e(LOGTAG, "Can't create bookmark " + name, ex);
+ } catch (java.lang.NoSuchFieldException ex) {
+ Log.e(LOGTAG, "Can't create bookmark " + name, ex);
+ }
+ }
+ }
+
+ private void createBookmark(SQLiteDatabase db, String title, String url, int pos, int parent) {
+ ContentValues bookmarkValues = new ContentValues();
+ bookmarkValues.put(Bookmarks.PARENT, parent);
+
+ long now = System.currentTimeMillis();
+ bookmarkValues.put(Bookmarks.DATE_CREATED, now);
+ bookmarkValues.put(Bookmarks.DATE_MODIFIED, now);
+
+ bookmarkValues.put(Bookmarks.TITLE, title);
+ bookmarkValues.put(Bookmarks.URL, url);
+ bookmarkValues.put(Bookmarks.GUID, Utils.generateGuid());
+ bookmarkValues.put(Bookmarks.POSITION, pos);
+ db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, bookmarkValues);
+ }
+
+ private void createFavicon(SQLiteDatabase db, String url, Bitmap icon) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ ContentValues iconValues = new ContentValues();
+ iconValues.put(Favicons.PAGE_URL, url);
+
+ byte[] data = null;
+ if (icon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
+ data = stream.toByteArray();
+ } else {
+ Log.w(LOGTAG, "Favicon compression failed.");
+ }
+ iconValues.put(Favicons.DATA, data);
+
+ insertFavicon(db, iconValues);
+ }
+
+ private Bitmap getDefaultFaviconFromPath(String name) {
+ Class<?> stringClass = R.string.class;
+ try {
+ // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
+ Field faviconField = stringClass.getField(name.replace("_title_", "_favicon_"));
+ if (faviconField == null) {
+ return null;
+ }
+ int faviconId = faviconField.getInt(null);
+ String path = mContext.getString(faviconId);
+
+ String apkPath = mContext.getPackageResourcePath();
+ File apkFile = new File(apkPath);
+ String bitmapPath = "jar:jar:" + apkFile.toURI() + "!/" + AppConstants.OMNIJAR_NAME + "!/" + path;
+ return GeckoJarReader.getBitmap(mContext.getResources(), bitmapPath);
+ } catch (java.lang.IllegalAccessException ex) {
+ Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
+ } catch (java.lang.NoSuchFieldException ex) {
+ // If the field does not exist, that means we intend to load via a drawable
+ }
+ return null;
+ }
+
+ private Bitmap getDefaultFaviconFromDrawable(String name) {
+ Class<?> drawablesClass = R.drawable.class;
+ try {
+ // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
+ Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
+ if (faviconField == null) {
+ return null;
+ }
+ int faviconId = faviconField.getInt(null);
+ return BitmapUtils.decodeResource(mContext, faviconId);
+ } catch (java.lang.IllegalAccessException ex) {
+ Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
+ } catch (java.lang.NoSuchFieldException ex) {
+ // If the field does not exist, that means we intend to load via a file path
+ }
+ return null;
+ }
+
+ private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
+ createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
+ R.string.bookmarks_folder_mobile, 0);
+ createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
+ R.string.bookmarks_folder_toolbar, 1);
+ createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
+ R.string.bookmarks_folder_menu, 2);
+ createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
+ R.string.bookmarks_folder_tags, 3);
+ createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
+ R.string.bookmarks_folder_unfiled, 4);
+ createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
+ R.string.bookmarks_folder_reading_list, 5);
+ createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
+ R.string.bookmarks_folder_pinned, 6);
+ }
+
+ private void createOrUpdateSpecialFolder(SQLiteDatabase db,
+ String guid, int titleId, int position) {
+ ContentValues values = new ContentValues();
+ values.put(Bookmarks.GUID, guid);
+ values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
+ values.put(Bookmarks.POSITION, position);
+
+ if (guid.equals(Bookmarks.PLACES_FOLDER_GUID))
+ values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
+ else if (guid.equals(Bookmarks.READING_LIST_FOLDER_GUID))
+ values.put(Bookmarks._ID, Bookmarks.FIXED_READING_LIST_ID);
+ else if (guid.equals(Bookmarks.PINNED_FOLDER_GUID))
+ values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);
+
+ // Set the parent to 0, which sync assumes is the root
+ values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
+
+ String title = mContext.getResources().getString(titleId);
+ values.put(Bookmarks.TITLE, title);
+
+ long now = System.currentTimeMillis();
+ values.put(Bookmarks.DATE_CREATED, now);
+ values.put(Bookmarks.DATE_MODIFIED, now);
+
+ int updated = db.update(TABLE_BOOKMARKS, values,
+ Bookmarks.GUID + " = ?",
+ new String[] { guid });
+
+ if (updated == 0) {
+ db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
+ debug("Inserted special folder: " + guid);
+ } else {
+ debug("Updated special folder: " + guid);
+ }
+ }
+
+ private boolean isSpecialFolder(ContentValues values) {
+ String guid = values.getAsString(Bookmarks.GUID);
+ if (guid == null)
+ return false;
+
+ return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
+ guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
+ guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
+ guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
+ guid.equals(Bookmarks.TAGS_FOLDER_GUID);
+ }
+
+ private void migrateBookmarkFolder(SQLiteDatabase db, int folderId,
+ BookmarkMigrator migrator) {
+ Cursor c = null;
+
+ debug("Migrating bookmark folder with id = " + folderId);
+
+ String selection = Bookmarks.PARENT + " = " + folderId;
+ String[] selectionArgs = null;
+
+ boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);
+
+ // If we're loading the root folder, we have to account for
+ // any previously created special folder that was created without
+ // setting a parent id (e.g. mobile folder) and making sure we're
+ // not adding any infinite recursion as root's parent is root itself.
+ if (isRootFolder) {
+ selection = Bookmarks.GUID + " != ?" + " AND (" +
+ selection + " OR " + Bookmarks.PARENT + " = NULL)";
+ selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
+ }
+
+ List<Integer> subFolders = new ArrayList<Integer>();
+ List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();
+
+ try {
+ c = db.query(TABLE_BOOKMARKS_TMP,
+ null,
+ selection,
+ selectionArgs,
+ null, null, null);
+
+ // The key point here is that bookmarks should be added in
+ // parent order to avoid any problems with the foreign key
+ // in Bookmarks.PARENT.
+ while (c.moveToNext()) {
+ ContentValues values = new ContentValues();
+
+ // We're using a null projection in the query which
+ // means we're getting all columns from the table.
+ // It's safe to simply transform the row into the
+ // values to be inserted on the new table.
+ DatabaseUtils.cursorRowToContentValues(c, values);
+
+ boolean isSpecialFolder = isSpecialFolder(values);
+
+ // The mobile folder used to be created with PARENT = NULL.
+ // We want fix that here.
+ if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
+ values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
+
+ if (isRootFolder && !isSpecialFolder) {
+ invalidSpecialEntries.add(values);
+ continue;
+ }
+
+ if (migrator != null)
+ migrator.updateForNewTable(values);
+
+ debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
+ db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
+
+ Integer type = values.getAsInteger(Bookmarks.TYPE);
+ if (type != null && type == Bookmarks.TYPE_FOLDER)
+ subFolders.add(values.getAsInteger(Bookmarks._ID));
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+
+ // At this point is safe to assume that the mobile folder is
+ // in the new table given that we've always created it on
+ // database creation time.
+ final int nInvalidSpecialEntries = invalidSpecialEntries.size();
+ if (nInvalidSpecialEntries > 0) {
+ Integer mobileFolderId = getMobileFolderId(db);
+ if (mobileFolderId == null) {
+ Log.e(LOGTAG, "Error migrating invalid special folder entries: mobile folder id is null");
+ return;
+ }
+
+ debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
+ for (int i = 0; i < nInvalidSpecialEntries; i++) {
+ ContentValues values = invalidSpecialEntries.get(i);
+ values.put(Bookmarks.PARENT, mobileFolderId);
+
+ db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
+ }
+ }
+
+ final int nSubFolders = subFolders.size();
+ for (int i = 0; i < nSubFolders; i++) {
+ int subFolderId = subFolders.get(i);
+ migrateBookmarkFolder(db, subFolderId, migrator);
+ }
+ }
+
+ private void migrateBookmarksTable(SQLiteDatabase db) {
+ migrateBookmarksTable(db, null);
+ }
+
+ private void migrateBookmarksTable(SQLiteDatabase db, BookmarkMigrator migrator) {
+ debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
+ db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
+ " RENAME TO " + TABLE_BOOKMARKS_TMP);
+
+ debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
+
+ db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
+ db.execSQL("DROP INDEX IF EXISTS bookmarks_type_deleted_index");
+ db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
+ db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");
+
+ createBookmarksTable(db);
+ createBookmarksWithImagesView(db);
+
+ createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
+ R.string.bookmarks_folder_places, 0);
+
+ migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID, migrator);
+
+ // Ensure all special folders exist and have the
+ // right folder hierarchy.
+ createOrUpdateAllSpecialFolders(db);
+
+ debug("Dropping bookmarks temporary table");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
+ }
+
+
+ private void migrateHistoryTable(SQLiteDatabase db) {
+ debug("Renaming history table to " + TABLE_HISTORY_TMP);
+ db.execSQL("ALTER TABLE " + TABLE_HISTORY +
+ " RENAME TO " + TABLE_HISTORY_TMP);
+
+ debug("Dropping views and indexes related to " + TABLE_HISTORY);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ db.execSQL("DROP INDEX IF EXISTS history_url_index");
+ db.execSQL("DROP INDEX IF EXISTS history_guid_index");
+ db.execSQL("DROP INDEX IF EXISTS history_modified_index");
+ db.execSQL("DROP INDEX IF EXISTS history_visited_index");
+
+ createHistoryTable(db);
+ createHistoryWithImagesView(db);
+ createCombinedWithImagesView(db);
+
+ db.execSQL("INSERT INTO " + TABLE_HISTORY + " SELECT * FROM " + TABLE_HISTORY_TMP);
+
+ debug("Dropping history temporary table");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY_TMP);
+ }
+
+ private void migrateImagesTable(SQLiteDatabase db) {
+ debug("Renaming images table to " + TABLE_IMAGES_TMP);
+ db.execSQL("ALTER TABLE " + Obsolete.TABLE_IMAGES +
+ " RENAME TO " + TABLE_IMAGES_TMP);
+
+ debug("Dropping views and indexes related to " + Obsolete.TABLE_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ db.execSQL("DROP INDEX IF EXISTS images_url_index");
+ db.execSQL("DROP INDEX IF EXISTS images_guid_index");
+ db.execSQL("DROP INDEX IF EXISTS images_modified_index");
+
+ createImagesTable(db);
+ createHistoryWithImagesView(db);
+ createCombinedWithImagesView(db);
+
+ db.execSQL("INSERT INTO " + Obsolete.TABLE_IMAGES + " SELECT * FROM " + TABLE_IMAGES_TMP);
+
+ debug("Dropping images temporary table");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES_TMP);
+ }
+
+ private void upgradeDatabaseFrom1to2(SQLiteDatabase db) {
+ migrateBookmarksTable(db);
+ }
+
+ private void upgradeDatabaseFrom2to3(SQLiteDatabase db) {
+ debug("Dropping view: " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
+
+ createBookmarksWithImagesView(db);
+
+ debug("Dropping view: " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
+
+ createHistoryWithImagesView(db);
+ }
+
+ private void upgradeDatabaseFrom3to4(SQLiteDatabase db) {
+ migrateBookmarksTable(db, new BookmarkMigrator3to4());
+ }
+
+ private void upgradeDatabaseFrom4to5(SQLiteDatabase db) {
+ createCombinedWithImagesView(db);
+ }
+
+ private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
+ debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ createCombinedWithImagesView(db);
+ }
+
+ private void upgradeDatabaseFrom6to7(SQLiteDatabase db) {
+ debug("Removing history visits with NULL GUIDs");
+ db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL");
+
+ debug("Update images with NULL GUIDs");
+ String[] columns = new String[] { Obsolete.Images._ID };
+ Cursor cursor = null;
+ try {
+ cursor = db.query(Obsolete.TABLE_IMAGES, columns, Obsolete.Images.GUID + " IS NULL", null, null ,null, null, null);
+ ContentValues values = new ContentValues();
+ if (cursor.moveToFirst()) {
+ do {
+ values.put(Obsolete.Images.GUID, Utils.generateGuid());
+ db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images._ID + " = ?", new String[] {
+ cursor.getString(cursor.getColumnIndexOrThrow(Obsolete.Images._ID))
+ });
+ } while (cursor.moveToNext());
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ migrateBookmarksTable(db);
+ migrateHistoryTable(db);
+ migrateImagesTable(db);
+ }
+
+ private void upgradeDatabaseFrom7to8(SQLiteDatabase db) {
+ debug("Combining history entries with the same URL");
+
+ final String TABLE_DUPES = "duped_urls";
+ final String TOTAL = "total";
+ final String LATEST = "latest";
+ final String WINNER = "winner";
+
+ db.execSQL("CREATE TEMP TABLE " + TABLE_DUPES + " AS" +
+ " SELECT " + History.URL + ", " +
+ "SUM(" + History.VISITS + ") AS " + TOTAL + ", " +
+ "MAX(" + History.DATE_MODIFIED + ") AS " + LATEST + ", " +
+ "MAX(" + History._ID + ") AS " + WINNER +
+ " FROM " + TABLE_HISTORY +
+ " GROUP BY " + History.URL +
+ " HAVING count(" + History.URL + ") > 1");
+
+ db.execSQL("CREATE UNIQUE INDEX " + TABLE_DUPES + "_url_index ON " +
+ TABLE_DUPES + " (" + History.URL + ")");
+
+ final String fromClause = " FROM " + TABLE_DUPES + " WHERE " +
+ qualifyColumn(TABLE_DUPES, History.URL) + " = " +
+ qualifyColumn(TABLE_HISTORY, History.URL);
+
+ db.execSQL("UPDATE " + TABLE_HISTORY +
+ " SET " + History.VISITS + " = (SELECT " + TOTAL + fromClause + "), " +
+ History.DATE_MODIFIED + " = (SELECT " + LATEST + fromClause + "), " +
+ History.IS_DELETED + " = " +
+ "(" + History._ID + " <> (SELECT " + WINNER + fromClause + "))" +
+ " WHERE " + History.URL + " IN (SELECT " + History.URL + " FROM " + TABLE_DUPES + ")");
+
+ db.execSQL("DROP TABLE " + TABLE_DUPES);
+ }
+
+ private void upgradeDatabaseFrom8to9(SQLiteDatabase db) {
+ createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
+ R.string.bookmarks_folder_reading_list, 5);
+
+ debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ createCombinedWithImagesViewOn9(db);
+ }
+
+ private void upgradeDatabaseFrom9to10(SQLiteDatabase db) {
+ debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ createCombinedWithImagesViewOn10(db);
+ }
+
+ private void upgradeDatabaseFrom10to11(SQLiteDatabase db) {
+ debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
+ + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
+
+ createCombinedWithImagesViewOn11(db);
+ }
+
+ private void upgradeDatabaseFrom11to12(SQLiteDatabase db) {
+ debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+
+ createCombinedViewOn12(db);
+ }
+
+ private void upgradeDatabaseFrom12to13(SQLiteDatabase db) {
+ // Update images table with favicon URLs
+ SQLiteDatabase faviconsDb = null;
+ Cursor c = null;
+ try {
+ final String FAVICON_TABLE = "favicon_urls";
+ final String FAVICON_URL = "favicon_url";
+ final String FAVICON_PAGE = "page_url";
+
+ String dbPath = mContext.getDatabasePath(Obsolete.FAVICON_DB).getPath();
+ faviconsDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
+ String[] columns = new String[] { FAVICON_URL, FAVICON_PAGE };
+ c = faviconsDb.query(FAVICON_TABLE, columns, null, null, null, null, null, null);
+ int faviconIndex = c.getColumnIndexOrThrow(FAVICON_URL);
+ int pageIndex = c.getColumnIndexOrThrow(FAVICON_PAGE);
+ while (c.moveToNext()) {
+ ContentValues values = new ContentValues(1);
+ String faviconUrl = c.getString(faviconIndex);
+ String pageUrl = c.getString(pageIndex);
+ values.put(FAVICON_URL, faviconUrl);
+ db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images.URL + " = ?", new String[] { pageUrl });
+ }
+ } catch (SQLException e) {
+ // If we can't read from the database for some reason, we won't
+ // be able to import the favicon URLs. This isn't a fatal
+ // error, so continue the upgrade.
+ Log.e(LOGTAG, "Exception importing from " + Obsolete.FAVICON_DB, e);
+ } finally {
+ if (c != null)
+ c.close();
+ if (faviconsDb != null)
+ faviconsDb.close();
+ }
+
+ createFaviconsTable(db);
+
+ // Import favicons into the favicons table
+ db.execSQL("ALTER TABLE " + TABLE_HISTORY
+ + " ADD COLUMN " + History.FAVICON_ID + " INTEGER");
+ db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS
+ + " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER");
+
+ try {
+ c = db.query(Obsolete.TABLE_IMAGES,
+ new String[] {
+ Obsolete.Images.URL,
+ Obsolete.Images.FAVICON_URL,
+ Obsolete.Images.FAVICON,
+ Obsolete.Images.DATE_MODIFIED,
+ Obsolete.Images.DATE_CREATED
+ },
+ Obsolete.Images.FAVICON + " IS NOT NULL",
+ null, null, null, null);
+
+ while (c.moveToNext()) {
+ long faviconId = -1;
+ int faviconUrlIndex = c.getColumnIndexOrThrow(Obsolete.Images.FAVICON_URL);
+ String faviconUrl = null;
+ if (!c.isNull(faviconUrlIndex)) {
+ faviconUrl = c.getString(faviconUrlIndex);
+ Cursor c2 = null;
+ try {
+ c2 = db.query(TABLE_FAVICONS,
+ new String[] { Favicons._ID },
+ Favicons.URL + " = ?",
+ new String[] { faviconUrl },
+ null, null, null);
+ if (c2.moveToFirst()) {
+ faviconId = c2.getLong(c2.getColumnIndexOrThrow(Favicons._ID));
+ }
+ } finally {
+ if (c2 != null)
+ c2.close();
+ }
+ }
+
+ if (faviconId == -1) {
+ ContentValues values = new ContentValues(4);
+ values.put(Favicons.URL, faviconUrl);
+ values.put(Favicons.DATA, c.getBlob(c.getColumnIndexOrThrow(Obsolete.Images.FAVICON)));
+ values.put(Favicons.DATE_MODIFIED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_MODIFIED)));
+ values.put(Favicons.DATE_CREATED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_CREATED)));
+ faviconId = db.insert(TABLE_FAVICONS, null, values);
+ }
+
+ ContentValues values = new ContentValues(1);
+ values.put(FaviconColumns.FAVICON_ID, faviconId);
+ db.update(TABLE_HISTORY, values, History.URL + " = ?",
+ new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
+ db.update(TABLE_BOOKMARKS, values, Bookmarks.URL + " = ?",
+ new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+
+ createThumbnailsTable(db);
+
+ // Import thumbnails into the thumbnails table
+ db.execSQL("INSERT INTO " + TABLE_THUMBNAILS + " ("
+ + Thumbnails.URL + ", "
+ + Thumbnails.DATA + ") "
+ + "SELECT " + Obsolete.Images.URL + ", " + Obsolete.Images.THUMBNAIL
+ + " FROM " + Obsolete.TABLE_IMAGES
+ + " WHERE " + Obsolete.Images.THUMBNAIL + " IS NOT NULL");
+
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
+ db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
+
+ createBookmarksWithFaviconsView(db);
+ createHistoryWithFaviconsView(db);
+ createCombinedViewOn13(db);
+
+ db.execSQL("DROP TABLE IF EXISTS " + Obsolete.TABLE_IMAGES);
+ }
+
+ private void upgradeDatabaseFrom13to14(SQLiteDatabase db) {
+ createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
+ R.string.bookmarks_folder_pinned, 6);
+ }
+
+ private void upgradeDatabaseFrom14to15(SQLiteDatabase db) {
+ Cursor c = null;
+ try {
+ // Get all the pinned bookmarks
+ c = db.query(TABLE_BOOKMARKS,
+ new String[] { Bookmarks._ID, Bookmarks.URL },
+ Bookmarks.PARENT + " = ?",
+ new String[] { Integer.toString(Bookmarks.FIXED_PINNED_LIST_ID) },
+ null, null, null);
+
+ while (c.moveToNext()) {
+ // Check if this URL can be parsed as a URI with a valid scheme.
+ String url = c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
+ if (Uri.parse(url).getScheme() != null) {
+ continue;
+ }
+
+ // If it can't, update the URL to be an encoded "user-entered" value.
+ ContentValues values = new ContentValues(1);
+ String newUrl = Uri.fromParts("user-entered", url, null).toString();
+ values.put(Bookmarks.URL, newUrl);
+ db.update(TABLE_BOOKMARKS, values, Bookmarks._ID + " = ?",
+ new String[] { Integer.toString(c.getInt(c.getColumnIndexOrThrow(Bookmarks._ID))) });
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private void upgradeDatabaseFrom15to16(SQLiteDatabase db) {
+ db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
+ db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
+
+ createCombinedViewOn16(db);
+ }
+
+ private void upgradeDatabaseFrom16to17(SQLiteDatabase db) {
+ // Purge any 0-byte favicons/thumbnails
+ try {
+ db.execSQL("DELETE FROM " + TABLE_FAVICONS +
+ " WHERE length(" + Favicons.DATA + ") = 0");
+ db.execSQL("DELETE FROM " + TABLE_THUMBNAILS +
+ " WHERE length(" + Thumbnails.DATA + ") = 0");
+ } catch (SQLException e) {
+ Log.e(LOGTAG, "Error purging invalid favicons or thumbnails", e);
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ debug("Upgrading browser.db: " + db.getPath() + " from " +
+ oldVersion + " to " + newVersion);
+
+ // We have to do incremental upgrades until we reach the current
+ // database schema version.
+ for (int v = oldVersion + 1; v <= newVersion; v++) {
+ switch(v) {
+ case 2:
+ upgradeDatabaseFrom1to2(db);
+ break;
+
+ case 3:
+ upgradeDatabaseFrom2to3(db);
+ break;
+
+ case 4:
+ upgradeDatabaseFrom3to4(db);
+ break;
+
+ case 5:
+ upgradeDatabaseFrom4to5(db);
+ break;
+
+ case 6:
+ upgradeDatabaseFrom5to6(db);
+ break;
+
+ case 7:
+ upgradeDatabaseFrom6to7(db);
+ break;
+
+ case 8:
+ upgradeDatabaseFrom7to8(db);
+ break;
+
+ case 9:
+ upgradeDatabaseFrom8to9(db);
+ break;
+
+ case 10:
+ upgradeDatabaseFrom9to10(db);
+ break;
+
+ case 11:
+ upgradeDatabaseFrom10to11(db);
+ break;
+
+ case 12:
+ upgradeDatabaseFrom11to12(db);
+ break;
+
+ case 13:
+ upgradeDatabaseFrom12to13(db);
+ break;
+
+ case 14:
+ upgradeDatabaseFrom13to14(db);
+ break;
+
+ case 15:
+ upgradeDatabaseFrom14to15(db);
+ break;
+
+ case 16:
+ upgradeDatabaseFrom15to16(db);
+ break;
+
+ case 17:
+ upgradeDatabaseFrom16to17(db);
+ break;
+ }
+ }
+
+ // If an upgrade after 12->13 fails, the entire upgrade is rolled
+ // back, but we can't undo the deletion of favicon_urls.db if we
+ // delete this in step 13; therefore, we wait until all steps are
+ // complete before removing it.
+ if (oldVersion < 13 && newVersion >= 13
+ && mContext.getDatabasePath(Obsolete.FAVICON_DB).exists()
+ && !mContext.deleteDatabase(Obsolete.FAVICON_DB)) {
+ throw new SQLException("Could not delete " + Obsolete.FAVICON_DB);
+ }
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ debug("Opening browser.db: " + db.getPath());
+
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("PRAGMA foreign_keys=ON", null);
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ cursor = null;
+ try {
+ cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ // From Honeycomb on, it's possible to run several db
+ // commands in parallel using multiple connections.
+ if (Build.VERSION.SDK_INT >= 11) {
+ db.enableWriteAheadLogging();
+ db.setLockingEnabled(false);
+ } else {
+ // Pre-Honeycomb, we can do some lesser optimizations.
+ cursor = null;
+ try {
+ cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+ }
+
+ static final String qualifyColumn(String table, String column) {
+ return DBUtils.qualifyColumn(table, column);
+ }
+
+ // Calculate these once, at initialization. isLoggable is too expensive to
+ // have in-line in each log call.
+ private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+ private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+ protected static void trace(String message) {
+ if (logVerbose) {
+ Log.v(LOGTAG, message);
+ }
+ }
+
+ protected static void debug(String message) {
+ if (logDebug) {
+ Log.d(LOGTAG, message);
+ }
+ }
+
+ private Integer getMobileFolderId(SQLiteDatabase db) {
+ Cursor c = null;
+
+ try {
+ c = db.query(TABLE_BOOKMARKS,
+ mobileIdColumns,
+ Bookmarks.GUID + " = ?",
+ mobileIdSelectionArgs,
+ null, null, null);
+
+ if (c == null || !c.moveToFirst())
+ return null;
+
+ return c.getInt(c.getColumnIndex(Bookmarks._ID));
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ private long insertFavicon(SQLiteDatabase db, ContentValues values) {
+ // This method is a dupicate of BrowserProvider.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();
+ values.put(Favicons.DATE_CREATED, now);
+ values.put(Favicons.DATE_MODIFIED, now);
+ faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
+
+ if (pageUrl != null) {
+ ContentValues updateValues = new ContentValues(1);
+ updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
+ db.update(TABLE_HISTORY,
+ updateValues,
+ History.URL + " = ?",
+ new String[] { pageUrl });
+ db.update(TABLE_BOOKMARKS,
+ updateValues,
+ Bookmarks.URL + " = ?",
+ new String[] { pageUrl });
+ }
+
+ return faviconId;
+ }
+
+ private interface BookmarkMigrator {
+ public void updateForNewTable(ContentValues bookmark);
+ }
+
+ private class BookmarkMigrator3to4 implements BookmarkMigrator {
+ @Override
+ public void updateForNewTable(ContentValues bookmark) {
+ Integer isFolder = bookmark.getAsInteger("folder");
+ if (isFolder == null || isFolder != 1) {
+ bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
+ } else {
+ bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
+ }
+
+ bookmark.remove("folder");
+ }
+ }
+}
\ No newline at end of file
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -1,82 +1,52 @@
/* -*- 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.db;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Distribution;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
-import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.util.GeckoJarReader;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
import android.app.SearchManager;
-import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
-import android.graphics.Bitmap;
import android.net.Uri;
-import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class BrowserProvider extends ContentProvider {
+public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoBrowserProvider";
- private Context mContext;
-
- private PerProfileDatabases<BrowserDatabaseHelper> mDatabases;
-
- static final String DATABASE_NAME = "browser.db";
-
- static final int DATABASE_VERSION = 17;
// Maximum age of deleted records to be cleaned up (20 days in ms)
static final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
static final long DELETED_RECORDS_PURGE_LIMIT = 5;
// How many records to reposition in a single query.
@@ -89,30 +59,25 @@ public class BrowserProvider extends Con
static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000;
static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500;
// Minimum duration to keep when expiring.
static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks.
// Minimum number of thumbnails to keep around.
static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15;
- static final String TABLE_BOOKMARKS = "bookmarks";
- static final String TABLE_HISTORY = "history";
- static final String TABLE_FAVICONS = "favicons";
- static final String TABLE_THUMBNAILS = "thumbnails";
+ static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
+ static final String TABLE_HISTORY = History.TABLE_NAME;
+ static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
+ static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
- static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
- static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp";
- static final String TABLE_IMAGES_TMP = Obsolete.TABLE_IMAGES + "_tmp";
-
- static final String VIEW_COMBINED = "combined";
-
- static final String VIEW_BOOKMARKS_WITH_FAVICONS = "bookmarks_with_favicons";
- static final String VIEW_HISTORY_WITH_FAVICONS = "history_with_favicons";
- static final String VIEW_COMBINED_WITH_FAVICONS = "combined_with_favicons";
+ static final String VIEW_COMBINED = Combined.VIEW_NAME;
+ static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
+ static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
+ static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
// Bookmark matches
static final int BOOKMARKS = 100;
static final int BOOKMARKS_ID = 101;
static final int BOOKMARKS_FOLDER_ID = 102;
static final int BOOKMARKS_PARENT = 103;
static final int BOOKMARKS_POSITIONS = 104;
@@ -142,71 +107,26 @@ public class BrowserProvider extends Con
static final int THUMBNAIL_ID = 801;
static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
+ " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
+ " ASC";
static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
- static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
- TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " +
- qualifyColumn(TABLE_FAVICONS, Favicons._ID);
-
- static final String TABLE_HISTORY_JOIN_FAVICONS = TABLE_HISTORY + " LEFT OUTER JOIN " +
- TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " = " +
- qualifyColumn(TABLE_FAVICONS, Favicons._ID);
-
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
static final Map<String, String> HISTORY_PROJECTION_MAP;
static final Map<String, String> COMBINED_PROJECTION_MAP;
static final Map<String, String> SCHEMA_PROJECTION_MAP;
static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
static final Map<String, String> FAVICONS_PROJECTION_MAP;
static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
- static final class Obsolete {
- public static final String TABLE_IMAGES = "images";
- public static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
- public static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
- public static final String VIEW_COMBINED_WITH_IMAGES = "combined_with_images";
-
- public static final class Images implements CommonColumns, SyncColumns {
- private Images() {}
-
- public static final String URL = "url_key";
- public static final String FAVICON_URL = "favicon_url";
- public static final String FAVICON = "favicon";
- public static final String THUMBNAIL = "thumbnail";
- public static final String _ID = "_id";
- public static final String GUID = "guid";
- public static final String DATE_CREATED = "created";
- public static final String DATE_MODIFIED = "modified";
- public static final String IS_DELETED = "deleted";
- }
-
- public static final class Combined {
- private Combined() {}
-
- public static final String THUMBNAIL = "thumbnail";
- }
-
- static final String TABLE_BOOKMARKS_JOIN_IMAGES = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
- Obsolete.TABLE_IMAGES + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
-
- static final String TABLE_HISTORY_JOIN_IMAGES = TABLE_HISTORY + " LEFT OUTER JOIN " +
- Obsolete.TABLE_IMAGES + " ON " + qualifyColumn(TABLE_HISTORY, History.URL) + " = " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
-
- static final String FAVICON_DB = "favicon_urls.db";
- }
-
static {
// We will reuse this.
HashMap<String, String> map;
// Bookmarks
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
@@ -310,34 +230,16 @@ public class BrowserProvider extends Con
Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA);
SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
- private interface BookmarkMigrator {
- public void updateForNewTable(ContentValues bookmark);
- }
-
- private class BookmarkMigrator3to4 implements BookmarkMigrator {
- @Override
- public void updateForNewTable(ContentValues bookmark) {
- Integer isFolder = bookmark.getAsInteger("folder");
- if (isFolder == null || isFolder != 1) {
- bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
- } else {
- bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
- }
-
- bookmark.remove("folder");
- }
- }
-
static final String qualifyColumn(String table, String column) {
return table + "." + column;
}
private static boolean hasFaviconsInProjection(String[] projection) {
if (projection == null) return true;
for (int i = 0; i < projection.length; ++i) {
if (projection[i].equals(FaviconColumns.FAVICON) ||
@@ -359,1628 +261,16 @@ public class BrowserProvider extends Con
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
- final class BrowserDatabaseHelper extends SQLiteOpenHelper {
- public BrowserDatabaseHelper(Context context, String databasePath) {
- super(context, databasePath, null, DATABASE_VERSION);
- }
-
- private void createBookmarksTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_BOOKMARKS + " table");
-
- // Android versions older than Froyo ship with an sqlite
- // that doesn't support foreign keys.
- String foreignKeyOnParent = null;
- if (Build.VERSION.SDK_INT >= 8) {
- foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
- ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
- }
-
- db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
- Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Bookmarks.TITLE + " TEXT," +
- Bookmarks.URL + " TEXT," +
- Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
- Bookmarks.PARENT + " INTEGER," +
- Bookmarks.POSITION + " INTEGER NOT NULL," +
- Bookmarks.KEYWORD + " TEXT," +
- Bookmarks.DESCRIPTION + " TEXT," +
- Bookmarks.TAGS + " TEXT," +
- Bookmarks.DATE_CREATED + " INTEGER," +
- Bookmarks.DATE_MODIFIED + " INTEGER," +
- Bookmarks.GUID + " TEXT NOT NULL," +
- Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
- ");");
-
- db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.URL + ")");
- db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
- db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.GUID + ")");
- db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.DATE_MODIFIED + ")");
- }
-
- private void createBookmarksTableOn13(SQLiteDatabase db) {
- debug("Creating " + TABLE_BOOKMARKS + " table");
-
- // Android versions older than Froyo ship with an sqlite
- // that doesn't support foreign keys.
- String foreignKeyOnParent = null;
- if (Build.VERSION.SDK_INT >= 8) {
- foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
- ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
- }
-
- db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
- Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Bookmarks.TITLE + " TEXT," +
- Bookmarks.URL + " TEXT," +
- Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
- Bookmarks.PARENT + " INTEGER," +
- Bookmarks.POSITION + " INTEGER NOT NULL," +
- Bookmarks.KEYWORD + " TEXT," +
- Bookmarks.DESCRIPTION + " TEXT," +
- Bookmarks.TAGS + " TEXT," +
- Bookmarks.FAVICON_ID + " INTEGER," +
- Bookmarks.DATE_CREATED + " INTEGER," +
- Bookmarks.DATE_MODIFIED + " INTEGER," +
- Bookmarks.GUID + " TEXT NOT NULL," +
- Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
- ");");
-
- db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.URL + ")");
- db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
- db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.GUID + ")");
- db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.DATE_MODIFIED + ")");
- }
-
- private void createHistoryTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_HISTORY + " table");
- db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
- History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- History.TITLE + " TEXT," +
- History.URL + " TEXT NOT NULL," +
- History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
- History.DATE_LAST_VISITED + " INTEGER," +
- History.DATE_CREATED + " INTEGER," +
- History.DATE_MODIFIED + " INTEGER," +
- History.GUID + " TEXT NOT NULL," +
- History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- ");");
-
- db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
- + History.URL + ")");
- db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
- + History.GUID + ")");
- db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
- + History.DATE_MODIFIED + ")");
- db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
- + History.DATE_LAST_VISITED + ")");
- }
-
- private void createHistoryTableOn13(SQLiteDatabase db) {
- debug("Creating " + TABLE_HISTORY + " table");
- db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
- History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- History.TITLE + " TEXT," +
- History.URL + " TEXT NOT NULL," +
- History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
- History.FAVICON_ID + " INTEGER," +
- History.DATE_LAST_VISITED + " INTEGER," +
- History.DATE_CREATED + " INTEGER," +
- History.DATE_MODIFIED + " INTEGER," +
- History.GUID + " TEXT NOT NULL," +
- History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- ");");
-
- db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
- + History.URL + ")");
- db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
- + History.GUID + ")");
- db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
- + History.DATE_MODIFIED + ")");
- db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
- + History.DATE_LAST_VISITED + ")");
- }
-
- private void createImagesTable(SQLiteDatabase db) {
- debug("Creating " + Obsolete.TABLE_IMAGES + " table");
- db.execSQL("CREATE TABLE " + Obsolete.TABLE_IMAGES + " (" +
- Obsolete.Images._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Obsolete.Images.URL + " TEXT UNIQUE NOT NULL," +
- Obsolete.Images.FAVICON + " BLOB," +
- Obsolete.Images.FAVICON_URL + " TEXT," +
- Obsolete.Images.THUMBNAIL + " BLOB," +
- Obsolete.Images.DATE_CREATED + " INTEGER," +
- Obsolete.Images.DATE_MODIFIED + " INTEGER," +
- Obsolete.Images.GUID + " TEXT NOT NULL," +
- Obsolete.Images.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
- ");");
-
- db.execSQL("CREATE INDEX images_url_index ON " + Obsolete.TABLE_IMAGES + "("
- + Obsolete.Images.URL + ")");
- db.execSQL("CREATE UNIQUE INDEX images_guid_index ON " + Obsolete.TABLE_IMAGES + "("
- + Obsolete.Images.GUID + ")");
- db.execSQL("CREATE INDEX images_modified_index ON " + Obsolete.TABLE_IMAGES + "("
- + Obsolete.Images.DATE_MODIFIED + ")");
- }
-
- private void createFaviconsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_FAVICONS + " table");
- db.execSQL("CREATE TABLE " + TABLE_FAVICONS + " (" +
- Favicons._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Favicons.URL + " TEXT UNIQUE," +
- Favicons.DATA + " BLOB," +
- Favicons.DATE_CREATED + " INTEGER," +
- Favicons.DATE_MODIFIED + " INTEGER" +
- ");");
-
- db.execSQL("CREATE INDEX favicons_url_index ON " + TABLE_FAVICONS + "("
- + Favicons.URL + ")");
- db.execSQL("CREATE INDEX favicons_modified_index ON " + TABLE_FAVICONS + "("
- + Favicons.DATE_MODIFIED + ")");
- }
-
- private void createThumbnailsTable(SQLiteDatabase db) {
- debug("Creating " + TABLE_THUMBNAILS + " table");
- db.execSQL("CREATE TABLE " + TABLE_THUMBNAILS + " (" +
- Thumbnails._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Thumbnails.URL + " TEXT UNIQUE," +
- Thumbnails.DATA + " BLOB" +
- ");");
-
- db.execSQL("CREATE INDEX thumbnails_url_index ON " + TABLE_THUMBNAILS + "("
- + Thumbnails.URL + ")");
- }
-
- private void createBookmarksWithImagesView(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " AS " +
- "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
- ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
- Obsolete.TABLE_BOOKMARKS_JOIN_IMAGES);
- }
-
- private void createBookmarksWithFaviconsView(SQLiteDatabase db) {
- debug("Creating " + VIEW_BOOKMARKS_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_FAVICONS + " AS " +
- "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Bookmarks.FAVICON +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Bookmarks.FAVICON_URL +
- " FROM " + TABLE_BOOKMARKS_JOIN_FAVICONS);
- }
-
- private void createHistoryWithImagesView(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " AS " +
- "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
- ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
- Obsolete.TABLE_HISTORY_JOIN_IMAGES);
- }
-
- private void createHistoryWithFaviconsView(SQLiteDatabase db) {
- debug("Creating " + VIEW_HISTORY_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " +
- "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON +
- ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL +
- " FROM " + TABLE_HISTORY_JOIN_FAVICONS);
- }
-
- private void createCombinedWithImagesView(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritze bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
- ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
- " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
- }
-
- private void createCombinedWithImagesViewOn9(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritze bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
- ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
- " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
- }
-
- private void createCombinedWithImagesViewOn10(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritze bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
- ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
- " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
- }
-
- private void createCombinedWithImagesViewOn11(SQLiteDatabase db) {
- debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritze bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- // Only use DISPLAY_READER if the matching bookmark entry inside reading
- // list folder is not marked as deleted.
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
- " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
- ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
- " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
- }
-
- private void createCombinedViewOn12(SQLiteDatabase db) {
- debug("Creating " + VIEW_COMBINED + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritze bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- // Only use DISPLAY_READER if the matching bookmark entry inside reading
- // list folder is not marked as deleted.
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
- " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
- ")");
-
- debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
- " SELECT *, " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
- qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
- " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
- }
-
- private void createCombinedViewOn13(SQLiteDatabase db) {
- debug("Creating " + VIEW_COMBINED + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- Combined.FAVICON_ID +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritize bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- // Only use DISPLAY_READER if the matching bookmark entry inside reading
- // list folder is not marked as deleted.
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
- " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
- ")");
-
- debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
- " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
- " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
- }
-
- private void createCombinedViewOn16(SQLiteDatabase db) {
- debug("Creating " + VIEW_COMBINED + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
- " SELECT " + Combined.BOOKMARK_ID + ", " +
- Combined.HISTORY_ID + ", " +
- // We need to return an _id column because CursorAdapter requires it for its
- // default implementation for the getItemId() method. However, since
- // we're not using this feature in the parts of the UI using this view,
- // we can just use 0 for all rows.
- "0 AS " + Combined._ID + ", " +
- Combined.URL + ", " +
- Combined.TITLE + ", " +
- Combined.VISITS + ", " +
- Combined.DISPLAY + ", " +
- Combined.DATE_LAST_VISITED + ", " +
- Combined.FAVICON_ID +
- " FROM (" +
- // Bookmarks without history.
- " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- "-1 AS " + Combined.HISTORY_ID + ", " +
- "-1 AS " + Combined.VISITS + ", " +
- "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
- " FROM " + TABLE_BOOKMARKS +
- " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
- // Ignore pinned bookmarks.
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0 AND " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
- " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
- " UNION ALL" +
- // History with and without bookmark.
- " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
- // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
- // completely ignore them here because they're joined with history entries we care about.
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
- Bookmarks.FIXED_PINNED_LIST_ID + " THEN NULL ELSE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " END " +
- "ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
- // Prioritize bookmark titles over history titles, since the user may have
- // customized the title for a bookmark.
- "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
- qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
- // Only use DISPLAY_READER if the matching bookmark entry inside reading
- // list folder is not marked as deleted.
- "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
- " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
- Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
- qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
- qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
- qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
- qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
- " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
- " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
- " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
- qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND (" +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
- qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
- ")");
-
- debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
-
- db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
- " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
- qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
- " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
- " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- debug("Creating browser.db: " + db.getPath());
-
- createBookmarksTableOn13(db);
- createHistoryTableOn13(db);
- createFaviconsTable(db);
- createThumbnailsTable(db);
-
- createBookmarksWithFaviconsView(db);
- createHistoryWithFaviconsView(db);
- createCombinedViewOn16(db);
-
- createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
- R.string.bookmarks_folder_places, 0);
-
- createOrUpdateAllSpecialFolders(db);
-
- // Create distribution bookmarks before our own default bookmarks
- int pos = createDistributionBookmarks(db);
- createDefaultBookmarks(db, pos);
- }
-
- private String getLocalizedProperty(JSONObject bookmark, String property, Locale locale) throws JSONException {
- // Try the full locale
- String fullLocale = property + "." + locale.toString();
- if (bookmark.has(fullLocale)) {
- return bookmark.getString(fullLocale);
- }
- // Try without a variant
- if (!TextUtils.isEmpty(locale.getVariant())) {
- String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_"));
- if (bookmark.has(noVariant)) {
- return bookmark.getString(noVariant);
- }
- }
- // Try just the language
- String lang = property + "." + locale.getLanguage();
- if (bookmark.has(lang)) {
- return bookmark.getString(lang);
- }
- // Default to the non-localized property name
- return bookmark.getString(property);
- }
-
- // Returns the number of bookmarks inserted in the db
- private int createDistributionBookmarks(SQLiteDatabase db) {
- JSONArray bookmarks = Distribution.getBookmarks(mContext);
- if (bookmarks == null) {
- return 0;
- }
-
- Locale locale = Locale.getDefault();
- int pos = 0;
- Integer mobileFolderId = getMobileFolderId(db);
- if (mobileFolderId == null) {
- Log.e(LOGTAG, "Error creating distribution bookmarks: mobileFolderId is null");
- return 0;
- }
-
- for (int i = 0; i < bookmarks.length(); i++) {
- try {
- final JSONObject bookmark = bookmarks.getJSONObject(i);
-
- String title = getLocalizedProperty(bookmark, "title", locale);
- final String url = getLocalizedProperty(bookmark, "url", locale);
- createBookmark(db, title, url, pos, mobileFolderId);
-
- if (bookmark.has("pinned")) {
- try {
- // Create a fake bookmark in the hidden pinned folder to pin bookmark
- // to about:home top sites. Pass pos as the pinned position to pin
- // sites in the order that bookmarks are specified in bookmarks.json.
- if (bookmark.getBoolean("pinned")) {
- createBookmark(db, title, url, pos, Bookmarks.FIXED_PINNED_LIST_ID);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error pinning bookmark to top sites", e);
- }
- }
-
- pos++;
-
- // return early if there is no icon for this bookmark
- if (!bookmark.has("icon")) {
- continue;
- }
-
- // create icons in a separate thread to avoid blocking about:home on startup
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- SQLiteDatabase db = getWritableDatabase();
- try {
- String iconData = bookmark.getString("icon");
- Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconData);
- if (icon != null) {
- createFavicon(db, url, icon);
- }
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating distribution bookmark icon", e);
- }
- }
- });
- } catch (JSONException e) {
- Log.e(LOGTAG, "Error creating distribution bookmark", e);
- }
- }
- return pos;
- }
-
- // Inserts default bookmarks, starting at a specified position
- private void createDefaultBookmarks(SQLiteDatabase db, int pos) {
- Class<?> stringsClass = R.string.class;
- Field[] fields = stringsClass.getFields();
- Pattern p = Pattern.compile("^bookmarkdefaults_title_");
-
- Integer mobileFolderId = getMobileFolderId(db);
- if (mobileFolderId == null) {
- Log.e(LOGTAG, "Error creating default bookmarks: mobileFolderId is null");
- return;
- }
-
- for (int i = 0; i < fields.length; i++) {
- final String name = fields[i].getName();
- Matcher m = p.matcher(name);
- if (!m.find()) {
- continue;
- }
- try {
- int titleid = fields[i].getInt(null);
- String title = mContext.getString(titleid);
-
- Field urlField = stringsClass.getField(name.replace("_title_", "_url_"));
- int urlId = urlField.getInt(null);
- final String url = mContext.getString(urlId);
- createBookmark(db, title, url, pos, mobileFolderId);
-
- // create icons in a separate thread to avoid blocking about:home on startup
- ThreadUtils.postToBackgroundThread(new Runnable() {
- @Override
- public void run() {
- SQLiteDatabase db = getWritableDatabase();
- Bitmap icon = getDefaultFaviconFromPath(name);
- if (icon == null) {
- icon = getDefaultFaviconFromDrawable(name);
- }
- if (icon != null) {
- createFavicon(db, url, icon);
- }
- }
- });
- pos++;
- } catch (java.lang.IllegalAccessException ex) {
- Log.e(LOGTAG, "Can't create bookmark " + name, ex);
- } catch (java.lang.NoSuchFieldException ex) {
- Log.e(LOGTAG, "Can't create bookmark " + name, ex);
- }
- }
- }
-
- private void createBookmark(SQLiteDatabase db, String title, String url, int pos, int parent) {
- ContentValues bookmarkValues = new ContentValues();
- bookmarkValues.put(Bookmarks.PARENT, parent);
-
- long now = System.currentTimeMillis();
- bookmarkValues.put(Bookmarks.DATE_CREATED, now);
- bookmarkValues.put(Bookmarks.DATE_MODIFIED, now);
-
- bookmarkValues.put(Bookmarks.TITLE, title);
- bookmarkValues.put(Bookmarks.URL, url);
- bookmarkValues.put(Bookmarks.GUID, Utils.generateGuid());
- bookmarkValues.put(Bookmarks.POSITION, pos);
- db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, bookmarkValues);
- }
-
- private void createFavicon(SQLiteDatabase db, String url, Bitmap icon) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
-
- ContentValues iconValues = new ContentValues();
- iconValues.put(Favicons.PAGE_URL, url);
-
- byte[] data = null;
- if (icon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
- data = stream.toByteArray();
- } else {
- Log.w(LOGTAG, "Favicon compression failed.");
- }
- iconValues.put(Favicons.DATA, data);
-
- insertFavicon(db, iconValues);
- }
-
- private Bitmap getDefaultFaviconFromPath(String name) {
- Class<?> stringClass = R.string.class;
- try {
- // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
- Field faviconField = stringClass.getField(name.replace("_title_", "_favicon_"));
- if (faviconField == null) {
- return null;
- }
- int faviconId = faviconField.getInt(null);
- String path = mContext.getString(faviconId);
-
- String apkPath = mContext.getPackageResourcePath();
- File apkFile = new File(apkPath);
- String bitmapPath = "jar:jar:" + apkFile.toURI() + "!/" + AppConstants.OMNIJAR_NAME + "!/" + path;
- return GeckoJarReader.getBitmap(mContext.getResources(), bitmapPath);
- } catch (java.lang.IllegalAccessException ex) {
- Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
- } catch (java.lang.NoSuchFieldException ex) {
- // If the field does not exist, that means we intend to load via a drawable
- }
- return null;
- }
-
- private Bitmap getDefaultFaviconFromDrawable(String name) {
- Class<?> drawablesClass = R.drawable.class;
- try {
- // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
- Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
- if (faviconField == null) {
- return null;
- }
- int faviconId = faviconField.getInt(null);
- return BitmapUtils.decodeResource(mContext, faviconId);
- } catch (java.lang.IllegalAccessException ex) {
- Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
- } catch (java.lang.NoSuchFieldException ex) {
- // If the field does not exist, that means we intend to load via a file path
- }
- return null;
- }
-
- private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
- createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
- R.string.bookmarks_folder_mobile, 0);
- createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
- R.string.bookmarks_folder_toolbar, 1);
- createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
- R.string.bookmarks_folder_menu, 2);
- createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
- R.string.bookmarks_folder_tags, 3);
- createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
- R.string.bookmarks_folder_unfiled, 4);
- createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
- R.string.bookmarks_folder_reading_list, 5);
- createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
- R.string.bookmarks_folder_pinned, 6);
- }
-
- private void createOrUpdateSpecialFolder(SQLiteDatabase db,
- String guid, int titleId, int position) {
- ContentValues values = new ContentValues();
- values.put(Bookmarks.GUID, guid);
- values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
- values.put(Bookmarks.POSITION, position);
-
- if (guid.equals(Bookmarks.PLACES_FOLDER_GUID))
- values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
- else if (guid.equals(Bookmarks.READING_LIST_FOLDER_GUID))
- values.put(Bookmarks._ID, Bookmarks.FIXED_READING_LIST_ID);
- else if (guid.equals(Bookmarks.PINNED_FOLDER_GUID))
- values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);
-
- // Set the parent to 0, which sync assumes is the root
- values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
-
- String title = mContext.getResources().getString(titleId);
- values.put(Bookmarks.TITLE, title);
-
- long now = System.currentTimeMillis();
- values.put(Bookmarks.DATE_CREATED, now);
- values.put(Bookmarks.DATE_MODIFIED, now);
-
- int updated = db.update(TABLE_BOOKMARKS, values,
- Bookmarks.GUID + " = ?",
- new String[] { guid });
-
- if (updated == 0) {
- db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
- debug("Inserted special folder: " + guid);
- } else {
- debug("Updated special folder: " + guid);
- }
- }
-
- private boolean isSpecialFolder(ContentValues values) {
- String guid = values.getAsString(Bookmarks.GUID);
- if (guid == null)
- return false;
-
- return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
- guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
- guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
- guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
- guid.equals(Bookmarks.TAGS_FOLDER_GUID);
- }
-
- private void migrateBookmarkFolder(SQLiteDatabase db, int folderId,
- BookmarkMigrator migrator) {
- Cursor c = null;
-
- debug("Migrating bookmark folder with id = " + folderId);
-
- String selection = Bookmarks.PARENT + " = " + folderId;
- String[] selectionArgs = null;
-
- boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);
-
- // If we're loading the root folder, we have to account for
- // any previously created special folder that was created without
- // setting a parent id (e.g. mobile folder) and making sure we're
- // not adding any infinite recursion as root's parent is root itself.
- if (isRootFolder) {
- selection = Bookmarks.GUID + " != ?" + " AND (" +
- selection + " OR " + Bookmarks.PARENT + " = NULL)";
- selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
- }
-
- List<Integer> subFolders = new ArrayList<Integer>();
- List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();
-
- try {
- c = db.query(TABLE_BOOKMARKS_TMP,
- null,
- selection,
- selectionArgs,
- null, null, null);
-
- // The key point here is that bookmarks should be added in
- // parent order to avoid any problems with the foreign key
- // in Bookmarks.PARENT.
- while (c.moveToNext()) {
- ContentValues values = new ContentValues();
-
- // We're using a null projection in the query which
- // means we're getting all columns from the table.
- // It's safe to simply transform the row into the
- // values to be inserted on the new table.
- DatabaseUtils.cursorRowToContentValues(c, values);
-
- boolean isSpecialFolder = isSpecialFolder(values);
-
- // The mobile folder used to be created with PARENT = NULL.
- // We want fix that here.
- if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
- values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
-
- if (isRootFolder && !isSpecialFolder) {
- invalidSpecialEntries.add(values);
- continue;
- }
-
- if (migrator != null)
- migrator.updateForNewTable(values);
-
- debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
- db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
-
- Integer type = values.getAsInteger(Bookmarks.TYPE);
- if (type != null && type == Bookmarks.TYPE_FOLDER)
- subFolders.add(values.getAsInteger(Bookmarks._ID));
- }
- } finally {
- if (c != null)
- c.close();
- }
-
- // At this point is safe to assume that the mobile folder is
- // in the new table given that we've always created it on
- // database creation time.
- final int nInvalidSpecialEntries = invalidSpecialEntries.size();
- if (nInvalidSpecialEntries > 0) {
- Integer mobileFolderId = getMobileFolderId(db);
- if (mobileFolderId == null) {
- Log.e(LOGTAG, "Error migrating invalid special folder entries: mobile folder id is null");
- return;
- }
-
- debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
- for (int i = 0; i < nInvalidSpecialEntries; i++) {
- ContentValues values = invalidSpecialEntries.get(i);
- values.put(Bookmarks.PARENT, mobileFolderId);
-
- db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
- }
- }
-
- final int nSubFolders = subFolders.size();
- for (int i = 0; i < nSubFolders; i++) {
- int subFolderId = subFolders.get(i);
- migrateBookmarkFolder(db, subFolderId, migrator);
- }
- }
-
- private void migrateBookmarksTable(SQLiteDatabase db) {
- migrateBookmarksTable(db, null);
- }
-
- private void migrateBookmarksTable(SQLiteDatabase db, BookmarkMigrator migrator) {
- debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
- db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
- " RENAME TO " + TABLE_BOOKMARKS_TMP);
-
- debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
-
- db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_type_deleted_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
- db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");
-
- createBookmarksTable(db);
- createBookmarksWithImagesView(db);
-
- createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
- R.string.bookmarks_folder_places, 0);
-
- migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID, migrator);
-
- // Ensure all special folders exist and have the
- // right folder hierarchy.
- createOrUpdateAllSpecialFolders(db);
-
- debug("Dropping bookmarks temporary table");
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
- }
-
-
- private void migrateHistoryTable(SQLiteDatabase db) {
- debug("Renaming history table to " + TABLE_HISTORY_TMP);
- db.execSQL("ALTER TABLE " + TABLE_HISTORY +
- " RENAME TO " + TABLE_HISTORY_TMP);
-
- debug("Dropping views and indexes related to " + TABLE_HISTORY);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- db.execSQL("DROP INDEX IF EXISTS history_url_index");
- db.execSQL("DROP INDEX IF EXISTS history_guid_index");
- db.execSQL("DROP INDEX IF EXISTS history_modified_index");
- db.execSQL("DROP INDEX IF EXISTS history_visited_index");
-
- createHistoryTable(db);
- createHistoryWithImagesView(db);
- createCombinedWithImagesView(db);
-
- db.execSQL("INSERT INTO " + TABLE_HISTORY + " SELECT * FROM " + TABLE_HISTORY_TMP);
-
- debug("Dropping history temporary table");
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY_TMP);
- }
-
- private void migrateImagesTable(SQLiteDatabase db) {
- debug("Renaming images table to " + TABLE_IMAGES_TMP);
- db.execSQL("ALTER TABLE " + Obsolete.TABLE_IMAGES +
- " RENAME TO " + TABLE_IMAGES_TMP);
-
- debug("Dropping views and indexes related to " + Obsolete.TABLE_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- db.execSQL("DROP INDEX IF EXISTS images_url_index");
- db.execSQL("DROP INDEX IF EXISTS images_guid_index");
- db.execSQL("DROP INDEX IF EXISTS images_modified_index");
-
- createImagesTable(db);
- createHistoryWithImagesView(db);
- createCombinedWithImagesView(db);
-
- db.execSQL("INSERT INTO " + Obsolete.TABLE_IMAGES + " SELECT * FROM " + TABLE_IMAGES_TMP);
-
- debug("Dropping images temporary table");
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES_TMP);
- }
-
- private void upgradeDatabaseFrom1to2(SQLiteDatabase db) {
- migrateBookmarksTable(db);
- }
-
- private void upgradeDatabaseFrom2to3(SQLiteDatabase db) {
- debug("Dropping view: " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
-
- createBookmarksWithImagesView(db);
-
- debug("Dropping view: " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
-
- createHistoryWithImagesView(db);
- }
-
- private void upgradeDatabaseFrom3to4(SQLiteDatabase db) {
- migrateBookmarksTable(db, new BookmarkMigrator3to4());
- }
-
- private void upgradeDatabaseFrom4to5(SQLiteDatabase db) {
- createCombinedWithImagesView(db);
- }
-
- private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
- debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- createCombinedWithImagesView(db);
- }
-
- private void upgradeDatabaseFrom6to7(SQLiteDatabase db) {
- debug("Removing history visits with NULL GUIDs");
- db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL");
-
- debug("Update images with NULL GUIDs");
- String[] columns = new String[] { Obsolete.Images._ID };
- Cursor cursor = null;
- try {
- cursor = db.query(Obsolete.TABLE_IMAGES, columns, Obsolete.Images.GUID + " IS NULL", null, null ,null, null, null);
- ContentValues values = new ContentValues();
- if (cursor.moveToFirst()) {
- do {
- values.put(Obsolete.Images.GUID, Utils.generateGuid());
- db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images._ID + " = ?", new String[] {
- cursor.getString(cursor.getColumnIndexOrThrow(Obsolete.Images._ID))
- });
- } while (cursor.moveToNext());
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- migrateBookmarksTable(db);
- migrateHistoryTable(db);
- migrateImagesTable(db);
- }
-
- private void upgradeDatabaseFrom7to8(SQLiteDatabase db) {
- debug("Combining history entries with the same URL");
-
- final String TABLE_DUPES = "duped_urls";
- final String TOTAL = "total";
- final String LATEST = "latest";
- final String WINNER = "winner";
-
- db.execSQL("CREATE TEMP TABLE " + TABLE_DUPES + " AS" +
- " SELECT " + History.URL + ", " +
- "SUM(" + History.VISITS + ") AS " + TOTAL + ", " +
- "MAX(" + History.DATE_MODIFIED + ") AS " + LATEST + ", " +
- "MAX(" + History._ID + ") AS " + WINNER +
- " FROM " + TABLE_HISTORY +
- " GROUP BY " + History.URL +
- " HAVING count(" + History.URL + ") > 1");
-
- db.execSQL("CREATE UNIQUE INDEX " + TABLE_DUPES + "_url_index ON " +
- TABLE_DUPES + " (" + History.URL + ")");
-
- final String fromClause = " FROM " + TABLE_DUPES + " WHERE " +
- qualifyColumn(TABLE_DUPES, History.URL) + " = " +
- qualifyColumn(TABLE_HISTORY, History.URL);
-
- db.execSQL("UPDATE " + TABLE_HISTORY +
- " SET " + History.VISITS + " = (SELECT " + TOTAL + fromClause + "), " +
- History.DATE_MODIFIED + " = (SELECT " + LATEST + fromClause + "), " +
- History.IS_DELETED + " = " +
- "(" + History._ID + " <> (SELECT " + WINNER + fromClause + "))" +
- " WHERE " + History.URL + " IN (SELECT " + History.URL + " FROM " + TABLE_DUPES + ")");
-
- db.execSQL("DROP TABLE " + TABLE_DUPES);
- }
-
- private void upgradeDatabaseFrom8to9(SQLiteDatabase db) {
- createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
- R.string.bookmarks_folder_reading_list, 5);
-
- debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- createCombinedWithImagesViewOn9(db);
- }
-
- private void upgradeDatabaseFrom9to10(SQLiteDatabase db) {
- debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- createCombinedWithImagesViewOn10(db);
- }
-
- private void upgradeDatabaseFrom10to11(SQLiteDatabase db) {
- debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
- + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
-
- createCombinedWithImagesViewOn11(db);
- }
-
- private void upgradeDatabaseFrom11to12(SQLiteDatabase db) {
- debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
-
- createCombinedViewOn12(db);
- }
-
- private void upgradeDatabaseFrom12to13(SQLiteDatabase db) {
- // Update images table with favicon URLs
- SQLiteDatabase faviconsDb = null;
- Cursor c = null;
- try {
- final String FAVICON_TABLE = "favicon_urls";
- final String FAVICON_URL = "favicon_url";
- final String FAVICON_PAGE = "page_url";
-
- String dbPath = mContext.getDatabasePath(Obsolete.FAVICON_DB).getPath();
- faviconsDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
- String[] columns = new String[] { FAVICON_URL, FAVICON_PAGE };
- c = faviconsDb.query(FAVICON_TABLE, columns, null, null, null, null, null, null);
- int faviconIndex = c.getColumnIndexOrThrow(FAVICON_URL);
- int pageIndex = c.getColumnIndexOrThrow(FAVICON_PAGE);
- while (c.moveToNext()) {
- ContentValues values = new ContentValues(1);
- String faviconUrl = c.getString(faviconIndex);
- String pageUrl = c.getString(pageIndex);
- values.put(FAVICON_URL, faviconUrl);
- db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images.URL + " = ?", new String[] { pageUrl });
- }
- } catch (SQLException e) {
- // If we can't read from the database for some reason, we won't
- // be able to import the favicon URLs. This isn't a fatal
- // error, so continue the upgrade.
- Log.e(LOGTAG, "Exception importing from " + Obsolete.FAVICON_DB, e);
- } finally {
- if (c != null)
- c.close();
- if (faviconsDb != null)
- faviconsDb.close();
- }
-
- createFaviconsTable(db);
-
- // Import favicons into the favicons table
- db.execSQL("ALTER TABLE " + TABLE_HISTORY
- + " ADD COLUMN " + History.FAVICON_ID + " INTEGER");
- db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS
- + " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER");
-
- try {
- c = db.query(Obsolete.TABLE_IMAGES,
- new String[] {
- Obsolete.Images.URL,
- Obsolete.Images.FAVICON_URL,
- Obsolete.Images.FAVICON,
- Obsolete.Images.DATE_MODIFIED,
- Obsolete.Images.DATE_CREATED
- },
- Obsolete.Images.FAVICON + " IS NOT NULL",
- null, null, null, null);
-
- while (c.moveToNext()) {
- long faviconId = -1;
- int faviconUrlIndex = c.getColumnIndexOrThrow(Obsolete.Images.FAVICON_URL);
- String faviconUrl = null;
- if (!c.isNull(faviconUrlIndex)) {
- faviconUrl = c.getString(faviconUrlIndex);
- Cursor c2 = null;
- try {
- c2 = db.query(TABLE_FAVICONS,
- new String[] { Favicons._ID },
- Favicons.URL + " = ?",
- new String[] { faviconUrl },
- null, null, null);
- if (c2.moveToFirst()) {
- faviconId = c2.getLong(c2.getColumnIndexOrThrow(Favicons._ID));
- }
- } finally {
- if (c2 != null)
- c2.close();
- }
- }
-
- if (faviconId == -1) {
- ContentValues values = new ContentValues(4);
- values.put(Favicons.URL, faviconUrl);
- values.put(Favicons.DATA, c.getBlob(c.getColumnIndexOrThrow(Obsolete.Images.FAVICON)));
- values.put(Favicons.DATE_MODIFIED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_MODIFIED)));
- values.put(Favicons.DATE_CREATED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_CREATED)));
- faviconId = db.insert(TABLE_FAVICONS, null, values);
- }
-
- ContentValues values = new ContentValues(1);
- values.put(FaviconColumns.FAVICON_ID, faviconId);
- db.update(TABLE_HISTORY, values, History.URL + " = ?",
- new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
- db.update(TABLE_BOOKMARKS, values, Bookmarks.URL + " = ?",
- new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
- }
- } finally {
- if (c != null)
- c.close();
- }
-
- createThumbnailsTable(db);
-
- // Import thumbnails into the thumbnails table
- db.execSQL("INSERT INTO " + TABLE_THUMBNAILS + " ("
- + Thumbnails.URL + ", "
- + Thumbnails.DATA + ") "
- + "SELECT " + Obsolete.Images.URL + ", " + Obsolete.Images.THUMBNAIL
- + " FROM " + Obsolete.TABLE_IMAGES
- + " WHERE " + Obsolete.Images.THUMBNAIL + " IS NOT NULL");
-
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
-
- createBookmarksWithFaviconsView(db);
- createHistoryWithFaviconsView(db);
- createCombinedViewOn13(db);
-
- db.execSQL("DROP TABLE IF EXISTS " + Obsolete.TABLE_IMAGES);
- }
-
- private void upgradeDatabaseFrom13to14(SQLiteDatabase db) {
- createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
- R.string.bookmarks_folder_pinned, 6);
- }
-
- private void upgradeDatabaseFrom14to15(SQLiteDatabase db) {
- Cursor c = null;
- try {
- // Get all the pinned bookmarks
- c = db.query(TABLE_BOOKMARKS,
- new String[] { Bookmarks._ID, Bookmarks.URL },
- Bookmarks.PARENT + " = ?",
- new String[] { Integer.toString(Bookmarks.FIXED_PINNED_LIST_ID) },
- null, null, null);
-
- while (c.moveToNext()) {
- // Check if this URL can be parsed as a URI with a valid scheme.
- String url = c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
- if (Uri.parse(url).getScheme() != null) {
- continue;
- }
-
- // If it can't, update the URL to be an encoded "user-entered" value.
- ContentValues values = new ContentValues(1);
- String newUrl = Uri.fromParts("user-entered", url, null).toString();
- values.put(Bookmarks.URL, newUrl);
- db.update(TABLE_BOOKMARKS, values, Bookmarks._ID + " = ?",
- new String[] { Integer.toString(c.getInt(c.getColumnIndexOrThrow(Bookmarks._ID))) });
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- private void upgradeDatabaseFrom15to16(SQLiteDatabase db) {
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
- db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
-
- createCombinedViewOn16(db);
- }
-
- private void upgradeDatabaseFrom16to17(SQLiteDatabase db) {
- // Purge any 0-byte favicons/thumbnails
- try {
- db.execSQL("DELETE FROM " + TABLE_FAVICONS +
- " WHERE length(" + Favicons.DATA + ") = 0");
- db.execSQL("DELETE FROM " + TABLE_THUMBNAILS +
- " WHERE length(" + Thumbnails.DATA + ") = 0");
- } catch (SQLException e) {
- Log.e(LOGTAG, "Error purging invalid favicons or thumbnails", e);
- }
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- debug("Upgrading browser.db: " + db.getPath() + " from " +
- oldVersion + " to " + newVersion);
-
- // We have to do incremental upgrades until we reach the current
- // database schema version.
- for (int v = oldVersion + 1; v <= newVersion; v++) {
- switch(v) {
- case 2:
- upgradeDatabaseFrom1to2(db);
- break;
-
- case 3:
- upgradeDatabaseFrom2to3(db);
- break;
-
- case 4:
- upgradeDatabaseFrom3to4(db);
- break;
-
- case 5:
- upgradeDatabaseFrom4to5(db);
- break;
-
- case 6:
- upgradeDatabaseFrom5to6(db);
- break;
-
- case 7:
- upgradeDatabaseFrom6to7(db);
- break;
-
- case 8:
- upgradeDatabaseFrom7to8(db);
- break;
-
- case 9:
- upgradeDatabaseFrom8to9(db);
- break;
-
- case 10:
- upgradeDatabaseFrom9to10(db);
- break;
-
- case 11:
- upgradeDatabaseFrom10to11(db);
- break;
-
- case 12:
- upgradeDatabaseFrom11to12(db);
- break;
-
- case 13:
- upgradeDatabaseFrom12to13(db);
- break;
-
- case 14:
- upgradeDatabaseFrom13to14(db);
- break;
-
- case 15:
- upgradeDatabaseFrom14to15(db);
- break;
-
- case 16:
- upgradeDatabaseFrom15to16(db);
- break;
-
- case 17:
- upgradeDatabaseFrom16to17(db);
- break;
- }
- }
-
- // If an upgrade after 12->13 fails, the entire upgrade is rolled
- // back, but we can't undo the deletion of favicon_urls.db if we
- // delete this in step 13; therefore, we wait until all steps are
- // complete before removing it.
- if (oldVersion < 13 && newVersion >= 13
- && mContext.getDatabasePath(Obsolete.FAVICON_DB).exists()
- && !mContext.deleteDatabase(Obsolete.FAVICON_DB)) {
- throw new SQLException("Could not delete " + Obsolete.FAVICON_DB);
- }
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- debug("Opening browser.db: " + db.getPath());
-
- Cursor cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA foreign_keys=ON", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
- cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- // From Honeycomb on, it's possible to run several db
- // commands in parallel using multiple connections.
- if (Build.VERSION.SDK_INT >= 11) {
- db.enableWriteAheadLogging();
- db.setLockingEnabled(false);
- } else {
- // Pre-Honeycomb, we can do some lesser optimizations.
- cursor = null;
- try {
- cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
- } finally {
- if (cursor != null)
- cursor.close();
- }
- }
- }
- }
-
- private static final String[] mobileIdColumns = new String[] { Bookmarks._ID };
- private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID };
-
- private Integer getMobileFolderId(SQLiteDatabase db) {
- Cursor c = null;
-
- try {
- c = db.query(TABLE_BOOKMARKS,
- mobileIdColumns,
- Bookmarks.GUID + " = ?",
- mobileIdSelectionArgs,
- null, null, null);
-
- if (c == null || !c.moveToFirst())
- return null;
-
- return c.getInt(c.getColumnIndex(Bookmarks._ID));
- } finally {
- if (c != null)
- c.close();
- }
- }
-
- private SQLiteDatabase getReadableDatabase(Uri uri) {
- trace("Getting readable database for URI: " + uri);
-
- String profile = null;
-
- if (uri != null)
- profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-
- return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
- }
-
- private SQLiteDatabase getWritableDatabase(Uri uri) {
- trace("Getting writable database for URI: " + uri);
-
- String profile = null;
-
- if (uri != null)
- profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-
- return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
- }
-
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
// 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
@@ -2072,72 +362,49 @@ public class BrowserProvider extends Con
* Call this method within a transaction.
*/
private void expireThumbnails(final SQLiteDatabase db) {
Log.d(LOGTAG, "Expiring thumbnails.");
final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
final String sql = "DELETE FROM " + TABLE_THUMBNAILS +
" WHERE " + Thumbnails.URL + " NOT IN ( " +
" SELECT " + Combined.URL +
- " FROM " + VIEW_COMBINED +
+ " FROM " + Combined.VIEW_NAME +
" ORDER BY " + sortOrder +
" LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT +
") AND " + Thumbnails.URL + " NOT IN ( " +
" SELECT " + Bookmarks.URL +
" FROM " + TABLE_BOOKMARKS +
" WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
")";
trace("Clear thumbs using query: " + sql);
db.execSQL(sql);
}
private boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
- private boolean isTest(Uri uri) {
- String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
- return !TextUtils.isEmpty(isTest);
- }
-
private boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
private boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
private boolean shouldIncrementVisits(Uri uri) {
String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
return Boolean.parseBoolean(incrementVisits);
}
@Override
- public boolean onCreate() {
- debug("Creating BrowserProvider");
-
- synchronized (this) {
- mContext = getContext();
- mDatabases = new PerProfileDatabases<BrowserDatabaseHelper>(
- getContext(), DATABASE_NAME, new DatabaseHelperFactory<BrowserDatabaseHelper>() {
- @Override
- public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
- return new BrowserDatabaseHelper(context, databasePath);
- }
- });
- }
-
- return true;
- }
-
- @Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
trace("Getting URI type: " + uri);
switch (match) {
case BOOKMARKS:
trace("URI is BOOKMARKS: " + uri);
@@ -2156,47 +423,21 @@ public class BrowserProvider extends Con
return SearchManager.SUGGEST_MIME_TYPE;
}
debug("URI has unrecognized type: " + uri);
return null;
}
+ @SuppressWarnings("fallthrough")
@Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete on URI: " + uri);
-
+ public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+ trace("Calling delete in transaction on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
- int deleted = 0;
-
- if (Build.VERSION.SDK_INT >= 11) {
- trace("Beginning delete transaction: " + uri);
- db.beginTransaction();
- try {
- deleted = deleteInTransaction(db, uri, selection, selectionArgs);
- db.setTransactionSuccessful();
- trace("Successful delete transaction: " + uri);
- } finally {
- db.endTransaction();
- }
- } else {
- deleted = deleteInTransaction(db, uri, selection, selectionArgs);
- }
-
- if (deleted > 0)
- getContext().getContentResolver().notifyChange(uri, null);
-
- return deleted;
- }
-
- @SuppressWarnings("fallthrough")
- public int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
- trace("Calling delete in transaction on URI: " + 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 = ?");
@@ -2270,47 +511,16 @@ public class BrowserProvider extends Con
}
debug("Deleted " + deleted + " rows for URI: " + uri);
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);
- }
- } 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);
- }
-
- if (result != null)
- getContext().getContentResolver().notifyChange(uri, null);
-
- return result;
- }
-
public Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
int match = URI_MATCHER.match(uri);
long id = -1;
switch (match) {
case BOOKMARKS: {
@@ -2344,45 +554,18 @@ public class BrowserProvider extends Con
debug("Inserted ID in database: " + id);
if (id >= 0)
return ContentUris.withAppendedId(uri, id);
return null;
}
+ @SuppressWarnings("fallthrough")
@Override
- public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- trace("Calling update on URI: " + uri);
-
- 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);
- }
-
- if (updated > 0)
- getContext().getContentResolver().notifyChange(uri, null);
-
- return updated;
- }
-
- @SuppressWarnings("fallthrough")
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;
switch (match) {
@@ -2581,17 +764,17 @@ public class BrowserProvider extends Con
qb.setTables(TABLE_THUMBNAILS);
break;
}
case SCHEMA: {
debug("Query is on schema.");
MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
- schemaCursor.newRow().add(DATABASE_VERSION);
+ schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION);
return schemaCursor;
}
case COMBINED: {
debug("Query is on combined: " + uri);
if (TextUtils.isEmpty(sortOrder))
@@ -2601,17 +784,17 @@ public class BrowserProvider extends Con
// results when a history entry has multiple bookmarks.
groupBy = Combined.URL;
qb.setProjectionMap(COMBINED_PROJECTION_MAP);
if (hasFaviconsInProjection(projection))
qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
else
- qb.setTables(VIEW_COMBINED);
+ qb.setTables(Combined.VIEW_NAME);
break;
}
case SEARCH_SUGGEST: {
debug("Query is on search suggest: " + uri);
selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
Combined.TITLE + " LIKE ?)");
@@ -2954,23 +1137,25 @@ public class BrowserProvider extends Con
new String[] { pageUrl });
}
long insertFavicon(Uri uri, ContentValues values) {
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);
- stripEmptyByteArray(values, Favicons.DATA);
+ 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.
@@ -3009,17 +1194,17 @@ public class BrowserProvider extends Con
int updated = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
Cursor cursor = null;
Long faviconId = null;
long now = System.currentTimeMillis();
trace("Updating favicon for URL: " + faviconUrl);
- stripEmptyByteArray(values, Favicons.DATA);
+ 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);
@@ -3063,17 +1248,17 @@ public class BrowserProvider extends Con
}
long insertThumbnail(Uri uri, ContentValues values) {
String url = values.getAsString(Thumbnails.URL);
final SQLiteDatabase db = getWritableDatabase(uri);
trace("Inserting thumbnail for URL: " + url);
- stripEmptyByteArray(values, Thumbnails.DATA);
+ DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
}
int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateThumbnail(uri, values, selection, selectionArgs,
true /* insert if needed */);
@@ -3086,46 +1271,31 @@ public class BrowserProvider extends Con
}
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);
- stripEmptyByteArray(values, Thumbnails.DATA);
+ DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
trace("Updating thumbnail for URL: " + url);
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;
}
- /**
- * Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
- * @param values ContentValues of query
- * @param columnName Name of data column to verify
- */
- private void stripEmptyByteArray(ContentValues values, String columnName) {
- if (values.containsKey(columnName)) {
- byte[] data = values.getAsByteArray(columnName);
- if (data == null || data.length == 0) {
- Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
- values.putNull(columnName);
- }
- }
- }
-
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);
}
@@ -3274,36 +1444,18 @@ public class BrowserProvider extends Con
if (failures) {
throw new OperationApplicationException();
}
return results;
}
@Override
- public int bulkInsert(Uri uri, ContentValues[] values) {
- if (values == null)
- return 0;
-
- int numValues = values.length;
- int successes = 0;
-
- final SQLiteDatabase db = getWritableDatabase(uri);
-
- db.beginTransaction();
+ protected BrowserDatabaseHelper createDatabaseHelper(
+ Context context, String databasePath) {
+ return new BrowserDatabaseHelper(context, databasePath);
+ }
- try {
- for (int i = 0; i < numValues; i++) {
- insertInTransaction(uri, values[i]);
- successes++;
- }
- trace("Flushing DB bulkinsert...");
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- if (successes > 0)
- mContext.getContentResolver().notifyChange(uri, null);
-
- return successes;
+ @Override
+ protected String getDatabaseName() {
+ return BrowserDatabaseHelper.DATABASE_NAME;
}
}
--- a/mobile/android/base/db/DBUtils.java
+++ b/mobile/android/base/db/DBUtils.java
@@ -76,9 +76,24 @@ public class DBUtils {
try {
Thread.sleep(retries * 100);
} catch (InterruptedException ie) { }
}
}
Log.d(LOGTAG, "Failed to unlock database");
GeckoAppShell.listOfOpenFiles();
}
+
+ /**
+ * Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
+ * @param values ContentValues of query
+ * @param columnName Name of data column to verify
+ */
+ public static void stripEmptyByteArray(ContentValues values, String columnName) {
+ if (values.containsKey(columnName)) {
+ byte[] data = values.getAsByteArray(columnName);
+ if (data == null || data.length == 0) {
+ Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
+ values.putNull(columnName);
+ }
+ }
+ }
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/TransactionalProvider.java
@@ -0,0 +1,277 @@
+/* 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.db;
+
+import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+/*
+ * Abstract class containing methods needed to make a SQLite-based content provider with a
+ * database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
+ * updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
+ */
+public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
+ private static final String LOGTAG = "GeckoTransProvider";
+ protected Context mContext;
+ protected PerProfileDatabases<T> mDatabases;
+
+ /*
+ * Returns the name of the database file. Used to get a path
+ * to the DB file.
+ *
+ * @return name of the database file
+ */
+ abstract protected String getDatabaseName();
+
+ /*
+ * Creates and returns an instance of a DB helper. Given a
+ * context and a path to the DB file
+ *
+ * @param context to use to create the database helper
+ * @param databasePath path to the DB file
+ * @return instance of the database helper
+ */
+ abstract protected T createDatabaseHelper(Context context, String databasePath);
+
+ /*
+ * Inserts an item into the database within a DB transaction.
+ *
+ * @param uri query URI
+ * @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
+ */
+ abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+
+ /*
+ * Updates the database within a DB transaction.
+ *
+ * @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
+ */
+ 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.
+ *
+ * @param uri content URI optionally indicating the profile of the user
+ * @return instance of a readable SQLiteDatabase
+ */
+ protected SQLiteDatabase getReadableDatabase(Uri uri) {
+ String profile = null;
+ if (uri != null) {
+ profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+ }
+
+ return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
+ }
+
+ /*
+ * Fetches a writeable database based on the profile indicated in the
+ * passed URI. If the URI does not contain a profile param, the default profile
+ * is used
+ *
+ * @param uri content URI optionally indicating the profile of the user
+ * @return instance of a writeable SQLiteDatabase
+ */
+ protected SQLiteDatabase getWritableDatabase(Uri uri) {
+ String profile = null;
+ if (uri != null) {
+ profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+ }
+
+ return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).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;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ trace("Calling delete on URI: " + uri);
+
+ 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 {
+ deleted = deleteInTransaction(uri, selection, selectionArgs);
+ }
+
+ 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);
+ }
+ } 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);
+ }
+
+ 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);
+
+ 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);
+ }
+
+ if (updated > 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+
+ return updated;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ if (values == null) {
+ return 0;
+ }
+
+ int numValues = values.length;
+ int successes = 0;
+
+ final boolean hasTransactions = Build.VERSION.SDK_INT >= 11;
+ final SQLiteDatabase db = getWritableDatabase(uri);
+
+ if (hasTransactions) {
+ db.beginTransaction();
+ }
+
+ try {
+ for (int i = 0; i < numValues; i++) {
+ insertInTransaction(uri, values[i]);
+ successes++;
+ }
+ if (hasTransactions) {
+ trace("Flushing DB bulkinsert...");
+ db.setTransactionSuccessful();
+ }
+ } finally {
+ if (hasTransactions) {
+ db.endTransaction();
+ }
+ }
+
+ if (successes > 0) {
+ mContext.getContentResolver().notifyChange(uri, null);
+ }
+
+ return successes;
+ }
+
+ protected boolean isTest(Uri uri) {
+ String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
+ return !TextUtils.isEmpty(isTest);
+ }
+
+ // Calculate these once, at initialization. isLoggable is too expensive to
+ // have in-line in each log call.
+ private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+ private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+ protected static void trace(String message) {
+ if (logVerbose) {
+ Log.v(LOGTAG, message);
+ }
+ }
+
+ protected static void debug(String message) {
+ if (logDebug) {
+ Log.d(LOGTAG, message);
+ }
+ }
+}
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -260,16 +260,20 @@ public class FxAccountSyncAdapter extend
handleError(e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
callback.handleError(null, e);
}
+
+ @Override
+ public void handleBackoff(int backoffSeconds) {
+ }
});
}
protected static void showNotification(Context context, State state) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final Action action = state.getNeededAction();
if (action == Action.None) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -107,26 +107,28 @@ gbjar.sources += [
'BrowserApp.java',
'CameraImageResultHandler.java',
'CameraVideoResultHandler.java',
'ContactService.java',
'ContextGetter.java',
'CustomEditText.java',
'DataReportingNotification.java',
'db/BrowserContract.java',
+ 'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
'db/BrowserProvider.java',
'db/DBUtils.java',
'db/FormHistoryProvider.java',
'db/HomeProvider.java',
'db/LocalBrowserDB.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabases.java',
'db/SQLiteBridgeContentProvider.java',
'db/TabsProvider.java',
+ 'db/TransactionalProvider.java',
'Distribution.java',
'DoorHangerPopup.java',
'EditBookmarkDialog.java',
'favicons/cache/FaviconCache.java',
'favicons/cache/FaviconCacheElement.java',
'favicons/cache/FaviconsForURL.java',
'favicons/decoders/FaviconDecoder.java',
'favicons/decoders/ICODecoder.java',
index 11e89d34aba2222cc1deaca5b6137bdfa730727b..f6ef37ba9405fa3b66ec402ad46ed45fa7315ce7
GIT binary patch
literal 2200
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
index 4abb71ee3cdb8c9c33617bffa9bffad6e5fb46d7..a8782e2d66561e93a04a582a8c7fde57de949717
GIT binary patch
literal 1591
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
index 204562d39f09be565fbe3df4a04b157dd94222c9..8314fc45b04f9969a20990fd0194e2ca9c0d8c6c
GIT binary patch
literal 1531
zc$@+I1qAwuP)<h;3K|Lk000e1NJLTq000jF000^Y1^@s6Xw3#<000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R0004!Nkl<ZI1%lXJxjw-6ozwd0`;R(SNjh-3brDY
zPGVaG7Z(Rb7jf5LA>z`}Nzp+H;s;8<T#N;6D(?P)x+)4PHs_i*n2@Hatpzvla_)KG
z=kzo;xkphttbD!_DOc;Kh#e3`bTC9DI(kS%bBGob$OVce-PjaF8T>FJ@}MT<7t6Y_
zNUGTP>(b<<nN;GytG5&vE{(VvBAo;ccp>a1%Xjlbrn1wCy5qTj`>3dosRMKpR0tS4
zd~hwrxm+$39v(Wj4k+pvK@mPU@bK-<vGJ)LA(Y9W0>1_hJaj5t*VZ+mZu_Bbd6Ufz
z&15bxo0V>`-<JwC{n~&2mnT5yS2r<Ir1sk;cvYGzM7H0L#J*Lfs<A6bEce=-CGVw^
zi52_Xez?C~V^(Ut6oUf~*1gZTY9rynyPs~Bl(O;i`Zh72Nj<g{Bk6k>pxB-gQm$g(
zwk+K{Mc})W#DOuV_|x2Sazy0*`32p5W+a{^O4a%WCMQc-Oef>Bmd767-nMoA2Kc%0
hv|t_38DdZ&xG!%1onhWR4`ToT002ovPDHLkV1ks3;hO*e
index 131f05dd288a4af90f7f0f99decea7de30ab9974..03a16d427eaed2b195664a088851eaaa6828b153
GIT binary patch
literal 4067
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
VU@R4MDlGs2002ovPDHLkV1kfu<TL;P
index 3a2e33adfcc36adccc3262af0bec5aa0f3cb9fc3..23ebe23741ed00da21613a532e9cee05198e61ff
GIT binary patch
literal 9730
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
index 1b6e67572fb215857031bc88474009299016ce99..fe2e174886c9f4864029dcde4d5634554e4050e8
GIT binary patch
literal 3204
zc$@)*414p5P)<h;3K|Lk000e1NJLTq001rk001rs1^@s6SGg-U000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R000OZNkl<ZXa%)dd2AlV6@N3kv$nYs1qeq<Y~vh2
zf=COfA(Xa>qLh%@#&*gP6$K##h=NpTX)1_BptpLYDg{9sq5MHbL~SQHSBX+URlzNS
zLVySoAD_e7LIMI@9PcrYH~V=%X7`)@zVX2-@xJ5t-oBlk_h#OF405igz6rInPY??o
z#^#|T=K^Q-jI&di#8XhR6M<tL0@N{1|0Qq<5RfaMp}MmXWO$h9!-RRC<nIj~34Xlv
zX*inmxN(c!ti{1qaHe;xcOQ#z1vo6$GvJ@YI0FinF1jaD;_075Y;7VU?Uxj-Fc?Ai
zR0gg5Y7r?GVBSs`-29l|8cKO3NPMm+o!noH@F$b=P;Olsv~pEZGSKQ=#$D$@?!Xy&
zm`ZXA`k98V;A?r9eQhvvy6`rkgndtR`uE;goN{^alDurhOPVo)Ve}S#?@zOGTOJWQ
z^RWG`?viPac$xIgH2v1ESS_CG>wpD$cwJsG@S#?34Hu%9bC|CiS<?|>G#qr9@B?iy
z&lPODO87+2Ia}m{=<RQFmnE{idv4%`4)2$&2L8evc1)geHMs<KV9p+RGmAUW3@16<
z+eJao$m+Pmj&~ild$8SEoz>~QG4R25cRkJNbvkfDG{at#U2To{EpwpBq*F3_A<yA_
zOQF^MRvxA-@Isrj)^YiiQ~{b|Wr6p#i^YI&O<q7HIj3*!vMqxx=4qM=e4rU-bBF)2
zWIRbC@?l@rg<!Ce)`zOqOxaQB)4A|^p1la_vVKy5J2l=zv_c-21x=LyU^qCS+YYoj
z?G(&nT~>{zDK+oZS>F0At`vBGyStQ1(xq8|9Zyv_b5Z?%?}EMXkuFBYJ_HH<y3B^=
zfIlpBz*#!B6u8Uy?YhE-Mv1-*@wRYOHq$B_={7VM-q3kP&*=`I+W(Tj5JPzvy_ogd
zppq5P`-Sc=4!rMjXkvh?bVW6q!aN8*EN<)$e&6(Xv@49@j+BkV-c9ji%w_#B+Rz>T
zoJH_;df8zU#w6n|w!Gd9jfpM}-0|Eswh0p(MlksQVgI6rp5VpAQPb97BaPxC2~YHY
z6WVmI-xj<OBDSfoX403-7^gD&y+j@d&Le(xBB*wk5q{!>-?HEd_#lgY=bhjN(vQ6i
zOU>^rR`E?;;a&{cwI-cWRxG1&MdAaka4u!;*+!s2$WX4G+Xed#Dp=ABBcp!ML8JJg
zbmYe-Ojg#o&3}~k?7OmZ6HgUsX5n&ip3-r*h86_NOG@l*=#I8lf}gb=3Q-`xOVz_V
z`J$tEcO{>?jvAEZisw`c7U_ygG)noqO8P{nl65o+Hz$0xds9#Fyp)it`FS?X;Y*Z`
zQ{+~GQx!S1uF7cZ4?BA+9MSn|G*z`7)i6rW5bM>QBQUE5fie8sUjb6JS~-}i3nTf8
zl`jUf6^>ItW%~e=gAm~*lYFXVp~DW@_@c$=qzwQrwZ5M>xRng4njEgApRA5H4Q1Pg
zJf9or%X4b2;#!^uD#<A;O7s^%xDigXaZ8m=9dRH>&fc~$;3((V;3kUU+_}WYmS)I^
z!g0#dvewGzfEUY<Q8xub=HNH1++cC@MpXrdtPB8`{c9zhV23TD{<9KJw!y1ID8ij@
zgQXLPR~3#!B>!P!K-pY3^H^_Oedvrj@7A%PDn3Hgil5TAcPfI$l(cz^%O($xMt`?4
zP#D_K-*({JFv}KC88pkQ^(wZ4LoMRVl=WBH`eYH?6UQ=lH@row;%hdBYSg_N@oO8b
z!Z2i~(KxOww0l>YY+h@F6VYRj+HR?31aBr=>A1v2AUByDn`CtEadn|xY&7Z0WUSb3
zQR3aF&Qf%}YJlsw=o#N(6P7l3ao`_E;ltx;rMSz!KeSr>Mh_%Qt3;-X)u7~zv6DSd
z+@E0fe+N!IeU{ik+thf&l*p^ySpb_8OuQ*q%-|K`eh1jiiNfYiFS}O;!4J;t0d@YD
zx~TcSSh)P3DPe>+N-6(GYuP5Xbf<Jx7<$$|Iy^0H2;&h~Ho-`6H#GodT}>r_G-Au0
zTJhqc)#5?Il?FlCP>G25%_(~Z4{+gZ8*k9c2RVO0_f-mf{&xEHK*F86(vB`3^Anbq
z1nuI6cF?lqX<mw?#24zjf>a)s0#^v?*#AtU{$!b91%diXpGLu5I%KF>d}Vw|r(0)f
zl%Wqdku<dlO9if!BT;Y#jeH?%Zsh408Kk^E+caPe++xH}KuFE>&s2w$HBHUSM5!8R
z?uuvBk#8b1-KpKWr?J!Dpz8)IG{F`QFc2IPf9WC*p`p!U&cG+cg-t;)ULM`1=
zng+RhaN1rzOhMn1@yNIUS}7+VG@X!bp`dfCoH1|}j5@GE3@%ZtRVFx~vy(qGb_Umz
zJe82NFs-Gp^wlhWJUjvh5y~G?zO5m<)V$XH7^jL3P|I>1_x?mncRR%?Wq@}!ZVi8>
zV-(%j3g-wRo~2S})evOuV_qGIhIo}4G^1V<x*BaHi;3rV&}d#8(c&>@TeLe1>|X(A
zxQ@4zCdkBtCZ{yo#|Iol*1w6bI^rfHT}CHMi@3*jlI(O{K2e(T%BQNfP93IJ@-gYa
zpI;8sW=!{1IEbrg!mm`vWR)IhtE1f7<B$5+&EEmW)^gH;X9D2|Tg8RK!DV#raUSUw
zB7@Ut3QdoX<mlH9m3;Ku|0uoWQIKqyaXdn2JMWYHUHTdQZQ|buBP@)Lgnj4iirchQ
q0mwY>3upOb3&5kN<W{lqM(`P?wzgt)^5voc0000<MNUMnLSTY|!7&#A
index 1a50daf6de8daa4bc0efbc80c57a9bd7406f1a03..1c1d14c0c9fe12ac64a7e11398f96e4472692bb9
GIT binary patch
literal 3581
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
EKY#qeivR!s
index bb978372f0eaa8809130f20a41387f0530703d2a..d1fd254cd5f29da2be41454691777ad78b64ab56
GIT binary patch
literal 1383
zc$@)e1(^DYP)<h;3K|Lk000e1NJLTq000UA000pP1^@s6<Rd-h000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R0002~Nkl<ZC>7OGKMw&h9BuE82o^CLEEkKIB*<;L
zAj0HB_&$6GHW7b@vt5Fi#71JYi3JgN*YZv(=g(mEOWMBQd#_2`S3=&ZP^>jE-e^Mo
zM3z-pfNCRfws1-db6`sSY0Ma9JrWMiD5WuoLWslZe9bMb;TpYyDQy_BXe8K|CUC&u
zprMCRi-zLeyQG77Evi2>qR||yw^WsW9Nxddbw%6X|2_Ws)<{Onv)V{e7544g+~wxf
z?3M7~&hfHu+(YxCdNqsnLnDqC;mWcO$J4cne5DZ>95isA5dh)a+-?ZY9SmUkA)u?6
pWbL+_Z6cZ46G6uFNxvg`tT#*2ZF*JeLbd<^002ovPDHLkV1j4ZpQiu-
index 7d82a89cb183bbd9cb6fc6edc414691769dc93cb..82304783977b499a023c562b09f478b6d83409be
GIT binary patch
literal 1357
zc$@)E1+w~yP)<h;3K|Lk000e1NJLTq000UA000pP1^@s6<Rd-h000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R0002wNkl<ZC>3L1Km*$kpUMJZG+_otWPu&~kGn9j
zFmDHm{rmIR)5Fnn4@@4eaO<9vYOL(cCm5L+1VNJj|1rG({qwJdw~P5L5T6M|EZwq2
zkd1?BGfWXk48#VBf#g7Z=7xp_7IjU7jf_l8rXT?nz{n`f$<Af0A}_t1arfa<QyG|;
zFdUDf;orY$DEy($A==l>%JR~?WE54b7#SIru=)Dmzkhp<?cEjwjzUoAiwTIIU|?ia
zLs9(y|E*UaURp%^_`D}Au0f`NBLCl?-(mm%{rdyLAT}&Ppx6TA;7X4Ge+^@)_@E5?
P00000NkvXXu0mjf_p6dm
index b6e7595e0bf7d65c88a5dc18f1d4c201ea22b273..b328c7b44144c68fa6d908cdb0b663b1b02d757e
GIT binary patch
literal 2446
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
M07*qoM6N<$f~=CL8~^|S
index bcb5f1596358515e25405d43f56c6f0b4169341c..2f7d92b706c5f98ca1b0c4efe7eb5fa6e1dff1c4
GIT binary patch
literal 4934
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
Ef^MZoNB{r;
index 71f60fb224ee8b5a81eada857423af784c253e6d..5b084016df1357f609908b3485a5070be4f75dba
GIT binary patch
literal 603
zc$@)S0;K(kP)<h;3K|Lk000e1NJLTq001Na001Kh1^@s6`~|2W0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz{YgYYRCwCVn6XX*K@f)b$k9MzG_f|=
zDHU%ZZva#_YUBxk(ni6;8lFInu`n?Kp8$w2a248l*g`=}z!*WCSq1{Qr{3<uGRb5&
zA(?#l?=Z8o7V~NC!~G;I3Gf*Cymi1G@H%*9^XAU+6W{tLL<E)$_%Zmp&u>7W5`bN4
z0Rosj!T1H4Hy1n&U^OWqQl;#1EGkw6sGKh#fI*0=235Qak1q#co%tUE24EQF;^m(T
z0SrRCEEr}+DNrf>n?mI!!f>UijQmsxe(;oW!G?LXJ<g|Th0bK6S)nis?nb%WTM?NM
zcp}*ug;dl*khPOjzW;n~u(ykIC6&esWtdq*m3S@%r~t;8c}t9QD9%>HIEM`$QHj!T
zHYfYR)L!P;uqdNKdn!?ey{>RC3l(51W?F!Ty&hk`XjRE0EkONOca%f4Dp<|d0rc@$
zXZ@!J8?;)=v242g##MndnQPP4QvQpee}xW?CV%_x?X`51h1S7=YAQfhIK+ZGQ61`V
z4jZYUAUU$_1M}v+d_UT^m2&7oOSji`CjwzMCUuBrCf6WPNmLjMrqsdF$m^0{HPHQ2
za6%QY){!FOWm$0=MmQ)gX)Y6vKt?B55sazXz5p=!Re1eyXr7mQ{Sb`USxGRx??VE(
pBwc%B9wowMvoC1A3huW60|2$mxTx-6AH4to002ovPDHLkV1g!S1knHh
index 83f98d3c0b114737f70f7f59340dff790fbbe38d..e470ebc6d671e612b89cf858a3ff2ae6b655da2b
GIT binary patch
literal 1109
zc$|GxOH30%7+xq*AVGW!Cg`wUz}VfLw!7L5D=lroCM;lSBAjHqJB1DGgWZ9a9t@%f
zF<iWmXgv8yG%-G6qDC~n5)<Q1J&}0RgC@ocdeY!*Y0;RJ$?VR|pYQwT-*5kaexR?X
zp?+^YMNtjOWAPMOf%nvIB5$`)SRqR@P7L8eD~ofAgQ%!xWe`Xj$|y=9MVpy?gE}b6
z*RQ9C@Q~aqs+Peh9)`&qHsy_u&b+Ot;|PNc8r4mS{=W2#2D&EECwQ5Y?HC%<j};s=
zSm;Zug>f~k(Va&@M_wcX2Eq!+8xy80<|VqQE0VnzW@%7_;BkpwOKM0S05Qu!fM<e!
zm4grn0s}+5AizT)z`+2^ktz6LNaO`Ea2S*ZO>T3vteA>-m+m4bi5|n)7Fjlz%Q3ki
zV>zQN42Q#>Mj+rP2){dHVkPf4-2-KXIC51-x3O-Sz&k4$YYIy=UH$~aUY9l9l1;=g
zHm}$$WH|O8pe%0;HH>w%i&H4OG1^Vf*oaLb*P3!v^5e1xJhCmu9Hd~&Nn6%L*~Nh|
z3tR4(WrNtD0NP-X;fm&QL5^n(CItDCmqC`}#Q+pJj?8ud+GIu5O^@4N6qjW&X}VZ3
zRg{cNG>I^}u8FXV3x>l&;s_s&L70G1*p-L|LxBL~gRxjtD0AbMI%Oaem$}+sZg)kl
zS2t^H<ml5#>vk*y6#WtP%DKcU=d#AtD(4cf$Yn_~?0;2J4v|vxw(B)c4(nZlOwt+-
z>4E*~z&X-Cmy_{mI=`IxwYu8*DpI{Td_Q}U`m!r>t9Nwij&Cybo*Ld#eJS$feBB%8
zB*<Rd@nClO=F796uK4lF?_|@Nd1dy+4PSQ)_jUI8<^^%&L)FzSk?GlnmTyyY59=<}
zG&ZJdJDQ$1J!*6!+qZT$Q*WdD`fCy|PJzXiyQiv}3$;JArk=N_TR&5~55ATji?3U^
gRWJBHN}Fpr>fO#g*GKOCeCjoNGSL^m-8C}z2fBb-&Hw-a
index c237c29559c2b4f0e74dd89f33bcbe17fb0f866e..51efb99a2175cd63a33338e8a58ae0a7bc983d6a
GIT binary patch
literal 1118
zc$|GxUuYCZ9NueabDWK)D4JG^!v;j5w>xv2-R72Dle@i~xsr|P<-j9I&ED=@Zb|k}
zb|<;bQ8Wf041^Y%QhXCk3++oED%gjDO6x-d?Th5a_+VRF5J4o`hdu=2>?IdPVusnB
znIGTx&2PW`{T7CYPPN6}i!lt-mLJF#=nBFo`WAir_4<Ewd6)E;$cR%RHO<3J+HlGk
z<SlI+7qDi`POab`hG{u%7E7e04$8V?aaxGs0?TE>(bF5anm&OEDC2R{PO(pZzQqF5
zNU>)`6{@a+tL8x6!z1;fqF$fSlLp)S5$FkIDqvxvfxw!yeK|<64PBY;!!XZ+282wc
z*sY{W>M&3o4+D`C;yOeKND_y-L`gy)fCNMd9?~hrQI{-Aa^gd<Jy?31XH?`uHn)8j
zJ*C(xA+F5xwOWm<37q4N^C+22h8l@PoFd}>tWC5aZu^~0g)H`U&vc3D*dRP>WoMeC
zSho2Imb)Ws``b2A!}vgRdBj2fC7`PA4YjNtv`-4SvNzf<&bpW{VBeYcbo%2eogvwk
z6%T8~@rsT!*>rKZ>JZ1TIxbK~Bya)=9Bh~e1t@YB*M-DwF9jZoastT^(s>eq6RM`0
zcE~;15LZ<>Z~H{Eb)3(pSQ_C>(~waH3dy9D?iSMuLj5R>GW}_xE0I8=peSjn$;~?Y
zw1sWb<Qgxynb&f|y4hl5&z!+V&T}l#@JBXx$wkQtLPpHxs9t9GT(-EzF1a+g5`?ei
z@-!L#m8xilXsLzUof@Zyoi4#PZ4Hn1z}I8@zNh{3Z9bbW25aSw&CT9l``$SG*_YQV
zf5ak%rHiHc!Dr_pQ|CuV*J5X5SE3#8%`g64taco2Z~6Pv9~CBgXZ^Q#j@Alabu;&V
z`iGhS+$eX9!AEnSv@XhHtC8jX9n-hJYstM2A2H?omo`sL9@?0@dt><bzT;OXTQ}|;
zZ2RWv%Ev7aE?zly`M~nR(k~ZI=cB8U_14(6|J&M+{PlSKZSo|z?MM3NBFvvvFn+T>
Q^DylAeE(4Phs@agbM)b51^@s6
--- a/mobile/android/base/sync/net/SyncResponse.java
+++ b/mobile/android/base/sync/net/SyncResponse.java
@@ -44,16 +44,30 @@ public class SyncResponse {
public int getStatusCode() {
return this.response.getStatusLine().getStatusCode();
}
public boolean wasSuccessful() {
return this.getStatusCode() == 200;
}
+ /**
+ * Fetch the content type of the HTTP response body.
+ *
+ * @return a <code>Header</code> instance, or <code>null</code> if there was
+ * no body or no valid Content-Type.
+ */
+ public Header getContentType() {
+ HttpEntity entity = this.response.getEntity();
+ if (entity == null) {
+ return null;
+ }
+ return entity.getContentType();
+ }
+
private String body = null;
public String body() throws IllegalStateException, IOException {
if (body != null) {
return body;
}
InputStreamReader is = new InputStreamReader(this.response.getEntity().getContent());
// Oh, Java, you are so evil.
body = new Scanner(is).useDelimiter("\\A").next();
@@ -141,41 +155,75 @@ public class SyncResponse {
return (int)((then - now) / 1000); // Convert milliseconds to seconds.
} catch (DateParseException e) {
Logger.warn(LOG_TAG, "Retry-After header neither integer nor date: " + retryAfter);
return -1;
}
}
/**
+ * @return A number of seconds, or -1 if the 'X-Backoff' header was not
+ * present.
+ */
+ public int backoffInSeconds() throws NumberFormatException {
+ return this.getIntegerHeader("x-backoff");
+ }
+
+ /**
* @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not
* present.
*/
public int weaveBackoffInSeconds() throws NumberFormatException {
return this.getIntegerHeader("x-weave-backoff");
}
/**
- * @return A number of milliseconds, or -1 if neither the 'Retry-After' or
- * 'X-Weave-Backoff' header was present.
+ * Extract a number of seconds, or -1 if none of the specified headers were present.
+ *
+ * @param includeRetryAfter
+ * if <code>true</code>, the Retry-After header is excluded. This is
+ * useful for processing non-error responses where a Retry-After
+ * header would be unexpected.
+ * @return the maximum of the three possible backoff headers, in seconds.
*/
- public long totalBackoffInMilliseconds() {
+ public int totalBackoffInSeconds(boolean includeRetryAfter) {
int retryAfterInSeconds = -1;
- try {
- retryAfterInSeconds = retryAfterInSeconds();
- } catch (NumberFormatException e) {
+ if (includeRetryAfter) {
+ try {
+ retryAfterInSeconds = retryAfterInSeconds();
+ } catch (NumberFormatException e) {
+ }
}
int weaveBackoffInSeconds = -1;
try {
weaveBackoffInSeconds = weaveBackoffInSeconds();
} catch (NumberFormatException e) {
}
- long totalBackoff = (long) Math.max(retryAfterInSeconds, weaveBackoffInSeconds);
+ int backoffInSeconds = -1;
+ try {
+ backoffInSeconds = backoffInSeconds();
+ } catch (NumberFormatException e) {
+ }
+
+ int totalBackoff = Math.max(retryAfterInSeconds, Math.max(backoffInSeconds, weaveBackoffInSeconds));
+ if (totalBackoff < 0) {
+ return -1;
+ } else {
+ return totalBackoff;
+ }
+ }
+
+ /**
+ * @return A number of milliseconds, or -1 if neither the 'Retry-After',
+ * 'X-Backoff', or 'X-Weave-Backoff' header were present.
+ */
+ public long totalBackoffInMilliseconds() {
+ long totalBackoff = totalBackoffInSeconds(true);
if (totalBackoff < 0) {
return -1;
} else {
return 1000 * totalBackoff;
}
}
/**
--- a/mobile/android/base/sync/net/SyncStorageRecordRequest.java
+++ b/mobile/android/base/sync/net/SyncStorageRecordRequest.java
@@ -20,16 +20,17 @@ import org.mozilla.gecko.sync.ThreadPool
* Includes:
* * Basic Auth headers (via Resource)
* * Error responses:
* * 401
* * 503
* * Headers:
* * Retry-After
* * X-Weave-Backoff
+ * * X-Backoff
* * X-Weave-Records?
* * ...
* * Timeouts
* * Network errors
* * application/newlines
* * JSON parsing
* * Content-Type and Content-Length validation.
*/
--- a/mobile/android/base/tests/testUITelemetry.java
+++ b/mobile/android/base/tests/testUITelemetry.java
@@ -1,24 +1,31 @@
package org.mozilla.gecko.tests;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.Telemetry;
import android.util.Log;
public class testUITelemetry extends JavascriptTest {
public testUITelemetry() {
super("testUITelemetry.js");
}
@Override
public void testJavascript() throws Exception {
blockForGeckoReady();
+
+ // We can't run these tests unless telemetry is turned on --
+ // the events will be dropped on the floor.
+ Log.i("GeckoTest", "Enabling telemetry.");
+ PrefsHelper.setPref(AppConstants.TELEMETRY_PREF_NAME, true);
+
Log.i("GeckoTest", "Adding telemetry events.");
-
try {
Telemetry.sendUIEvent("enone", "method0");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method1");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method2");
Telemetry.startUISession("bar");
Telemetry.sendUIEvent("efoobar", "method3", "foobarextras");
--- a/mobile/android/base/tests/testUITelemetry.js
+++ b/mobile/android/base/tests/testUITelemetry.js
@@ -1,31 +1,47 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
-Components.utils.import("resource://gre/modules/Services.jsm");
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
function do_check_array_eq(a1, a2) {
do_check_eq(a1.length, a2.length);
for (let i = 0; i < a1.length; ++i) {
do_check_eq(a1[i], a2[i]);
}
}
-add_test(function test_telemetry_events() {
- let bridge = Components.classes["@mozilla.org/android/bridge;1"]
- .getService(Components.interfaces.nsIAndroidBridge);
+function getObserver() {
+ let bridge = Cc["@mozilla.org/android/bridge;1"]
+ .getService(Ci.nsIAndroidBridge);
let obsXPCOM = bridge.browserApp.getUITelemetryObserver();
do_check_true(!!obsXPCOM);
+ return obsXPCOM.wrappedJSObject;
+}
- let obs = obsXPCOM.wrappedJSObject;
+/**
+ * The following event test will fail if telemetry isn't enabled. The Java-side
+ * part of this test should have turned it on; fail if it didn't work.
+ */
+add_test(function test_enabled() {
+ let obs = getObserver();
do_check_true(!!obs);
+ do_check_true(obs.enabled);
+ run_next_test();
+});
+add_test(function test_telemetry_events() {
+ let obs = getObserver();
let measurements = obs.getUIMeasurements();
let expected = [
["event", "enone", "method0", [], null],
["event", "efoo", "method1", ["foo"], null],
["event", "efoo", "method2", ["foo"], null],
["event", "efoobar", "method3", ["foo", "bar"], "foobarextras"],
["session", "foo", "reasonfoo"],
--- a/mobile/android/base/tokenserver/TokenServerClient.java
+++ b/mobile/android/base/tokenserver/TokenServerClient.java
@@ -24,16 +24,17 @@ import org.mozilla.gecko.sync.net.BaseRe
import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
+import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
import ch.boye.httpclientandroidlib.message.BasicHeader;
/**
@@ -88,36 +89,62 @@ public class TokenServerClient {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
+ /**
+ * Notify the delegate that some kind of backoff header (X-Backoff,
+ * X-Weave-Backoff, Retry-After) was received and should be acted upon.
+ *
+ * This method is non-terminal, and will be followed by a separate
+ * <code>invoke*</code> call.
+ *
+ * @param delegate
+ * the delegate to inform.
+ * @param backoffSeconds
+ * the number of seconds for which the system should wait before
+ * making another token server request to this server.
+ */
+ protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleBackoff(backoffSeconds);
+ }
+ });
+ }
+
protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
- public TokenServerToken processResponse(HttpResponse response)
- throws TokenServerException {
- SyncResponse res = new SyncResponse(response);
+ public TokenServerToken processResponse(SyncResponse res) throws TokenServerException {
int statusCode = res.getStatusCode();
Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
// Responses should *always* be JSON, even in the case of 4xx and 5xx
// errors. If we don't see JSON, the server is likely very unhappy.
- String contentType = response.getEntity().getContentType().getValue();
- if (!contentType.equals("application/json") && !contentType.startsWith("application/json;")) {
+ final Header contentType = res.getContentType();
+ if (contentType == null) {
+ throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+ }
+
+ final String type = contentType.getValue();
+ if (!type.equals("application/json") &&
+ !type.startsWith("application/json;")) {
Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
contentType + ". Misconfigured server?");
throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
}
// Responses should *always* be a valid JSON object.
ExtendedJSONObject result;
try {
@@ -225,20 +252,31 @@ public class TokenServerClient {
this.assertion = assertion;
this.clientState = clientState;
this.resource = resource;
this.conditionsAccepted = conditionsAccepted;
}
@Override
public void handleHttpResponse(HttpResponse response) {
+ // Skew.
SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
skewHandler.updateSkew(response, System.currentTimeMillis());
+
+ // Extract backoff regardless of whether this was an error response, and
+ // Retry-After for 503 responses. The error will be handled elsewhere.)
+ SyncResponse res = new SyncResponse(response);
+ final boolean includeRetryAfter = res.getStatusCode() == 503;
+ int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter);
+ if (backoffInSeconds > -1) {
+ client.notifyBackoff(delegate, backoffInSeconds);
+ }
+
try {
- TokenServerToken token = client.processResponse(response);
+ TokenServerToken token = client.processResponse(res);
client.invokeHandleSuccess(delegate, token);
} catch (TokenServerException e) {
client.invokeHandleFailure(delegate, e);
}
}
@Override
public void handleTransportException(GeneralSecurityException e) {
--- a/mobile/android/base/tokenserver/TokenServerClientDelegate.java
+++ b/mobile/android/base/tokenserver/TokenServerClientDelegate.java
@@ -1,12 +1,16 @@
/* 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.tokenserver;
-
public interface TokenServerClientDelegate {
void handleSuccess(TokenServerToken token);
void handleFailure(TokenServerException e);
void handleError(Exception e);
+
+ /**
+ * Might be called multiple times, in addition to the other terminating handler methods.
+ */
+ void handleBackoff(int backoffSeconds);
}
\ No newline at end of file
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -4301,8 +4301,11 @@ pref("dom.inter-app-communication-api.en
// The tables used for Safebrowsing phishing and malware checks.
pref("urlclassifier.malware_table", "goog-malware-shavar");
pref("urlclassifier.phish_table", "goog-phish-shavar");
pref("urlclassifier.download_block_table", "");
pref("urlclassifier.download_allow_table", "");
// Turn off Spatial navigation by default.
pref("snav.enabled", false);
+
+// Wakelock is disabled by default.
+pref("dom.wakelock.enabled", false);
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -168,16 +168,21 @@ function installFakePAC() {
}
function uninstallFakePAC() {
_("Uninstalling fake PAC.");
let CID = PACSystemSettings.CID;
Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
}
-// We want to ensure the legacy provider is used for most of these tests; the
-// tests that know how to deal with the Firefox Accounts identity hack things
-// to ensure that still works.
+// We want to ensure the legacy provider is used for most of these tests,
+// including after a service.startOver. The tests that know how to deal with
+// the Firefox Accounts identity hack things to ensure that still works.
function setDefaultIdentityConfig() {
Cu.import("resource://gre/modules/Services.jsm");
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", true);
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
+ Services.prefs.clearUserPref("services.sync-testing.startOverKeepIdentity");
+ });
}
setDefaultIdentityConfig();
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -101,20 +101,20 @@ WeaveService.prototype = {
} catch (_) {
// That pref doesn't exist - so let's assume this is a first-run.
// If sync already appears configured, we assume it's for the legacy
// provider.
let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
fxAccountsEnabled = !prefs.prefHasUserValue("username");
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", fxAccountsEnabled);
}
- // Currently we don't support toggling this pref after initialization, so
- // inject the pref value as a regular boolean.
- delete this.fxAccountsEnabled;
- return this.fxAccountsEnabled = fxAccountsEnabled;
+ // Currently we don't support toggling this pref after initialization -
+ // except when sync is reset - but this 1 exception is enough that we can't
+ // cache the value.
+ return fxAccountsEnabled;
},
observe: function (subject, topic, data) {
switch (topic) {
case "app-startup":
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
os.addObserver(this, "final-ui-startup", true);
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -232,18 +232,17 @@ this.BrowserIDManager.prototype = {
/**
* Sets the active account name.
*
* This should almost always be called in favor of setting username, as
* username is derived from account.
*
* Changing the account name has the side-effect of wiping out stored
- * credentials. Keep in mind that persistCredentials() will need to be called
- * to flush the changes to disk.
+ * credentials.
*
* Set this value to null to clear out identity information.
*/
set account(value) {
throw "account setter should be not used in BrowserIDManager";
},
/**
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -35,28 +35,40 @@ PasswordEngine.prototype = {
_trackerObj: PasswordTracker,
_recordObj: LoginRec,
applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
_syncFinish: function _syncFinish() {
SyncEngine.prototype._syncFinish.call(this);
// Delete the weave credentials from the server once
- if (!Svc.Prefs.get("deletePwd", false)) {
+ if (!Svc.Prefs.get("deletePwdFxA", false)) {
try {
- let ids = Services.logins.findLogins({}, PWDMGR_HOST, "", "")
- .map(function(info) {
- return info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid;
- });
- let coll = new Collection(this.engineURL, null, this.service);
- coll.ids = ids;
- let ret = coll.delete();
- this._log.debug("Delete result: " + ret);
-
- Svc.Prefs.set("deletePwd", true);
+ let ids = [];
+ for (let host of Utils.getSyncCredentialsHosts()) {
+ for (let info of Services.logins.findLogins({}, host, "", "")) {
+ ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid);
+ }
+ }
+ if (ids.length) {
+ let coll = new Collection(this.engineURL, null, this.service);
+ coll.ids = ids;
+ let ret = coll.delete();
+ this._log.debug("Delete result: " + ret);
+ if (!ret.success && ret.status != 400) {
+ // A non-400 failure means try again next time.
+ return;
+ }
+ } else {
+ this._log.debug("Didn't find any passwords to delete");
+ }
+ // If there were no ids to delete, or we succeeded, or got a 400,
+ // record success.
+ Svc.Prefs.set("deletePwdFxA", true);
+ Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
}
catch(ex) {
this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
}
}
},
_findDupe: function _findDupe(item) {
@@ -145,18 +157,19 @@ PasswordStore.prototype = {
getAllIDs: function PasswordStore__getAllIDs() {
let items = {};
let logins = Services.logins.getAllLogins({});
for (let i = 0; i < logins.length; i++) {
// Skip over Weave password/passphrase entries
let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
- if (metaInfo.hostname == PWDMGR_HOST)
+ if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) {
continue;
+ }
items[metaInfo.guid] = metaInfo;
}
return items;
},
changeItemID: function PasswordStore__changeItemID(oldID, newID) {
@@ -283,17 +296,17 @@ PasswordTracker.prototype = {
switch (data) {
case "modifyLogin":
subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo);
// fallthrough
case "addLogin":
case "removeLogin":
// Skip over Weave password/passphrase changes.
subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
- if (subject.hostname == PWDMGR_HOST) {
+ if (Utils.getSyncCredentialsHosts().has(subject.hostname)) {
break;
}
this.score += SCORE_INCREMENT_XLARGE;
this._log.trace(data + ": " + subject.guid);
this.addChangedID(subject.guid);
break;
case "removeAllLogins":
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -443,19 +443,21 @@ IdentityManager.prototype = {
password, "", "");
Services.logins.addLogin(login);
},
/**
* Deletes Sync credentials from the password manager.
*/
deleteSyncCredentials: function deleteSyncCredentials() {
- let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
- for each (let login in logins) {
- Services.logins.removeLogin(login);
+ for (let host of Utils.getSyncCredentialsHosts()) {
+ let logins = Services.logins.findLogins({}, host, "", "");
+ for each (let login in logins) {
+ Services.logins.removeLogin(login);
+ }
}
// Wait until after store is updated in case it fails.
this._basicPassword = null;
this._basicPasswordAllowLookup = true;
this._basicPasswordUpdated = false;
this._syncKey = null;
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -884,17 +884,42 @@ Sync11Service.prototype = {
this._ignorePrefObserver = true;
Svc.Prefs.resetBranch("");
this._ignorePrefObserver = false;
Svc.Prefs.set("lastversion", WEAVE_VERSION);
this.identity.deleteSyncCredentials();
- Svc.Obs.notify("weave:service:start-over:finish");
+ // If necessary, reset the identity manager, then re-initialize it so the
+ // FxA manager is used. This is configurable via a pref - mainly for tests.
+ let keepIdentity = false;
+ try {
+ keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
+ } catch (_) { /* no such pref */ }
+ if (keepIdentity) {
+ Svc.Obs.notify("weave:service:start-over:finish");
+ return;
+ }
+
+ this.identity.username = "";
+ Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
+ this.status.__authManager = null;
+ this.identity = Status._authManager;
+ this._clusterManager = this.identity.createClusterManager(this);
+
+ // Tell the new identity manager to initialize itself
+ this.identity.initialize().then(() => {
+ Svc.Obs.notify("weave:service:start-over:finish");
+ }).then(null, err => {
+ this._log.error("startOver failed to re-initialize the identity manager: " + err);
+ // Still send the observer notification so the current state is
+ // reflected in the UI.
+ Svc.Obs.notify("weave:service:start-over:finish");
+ });
},
persistLogin: function persistLogin() {
try {
this.identity.persistCredentials(true);
} catch (ex) {
this._log.info("Unable to persist credentials: " + ex);
}
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -596,16 +596,48 @@ this.Utils = {
calculateBackoff: function calculateBackoff(attempts, baseInterval,
statusInterval) {
let backoffInterval = attempts *
(Math.floor(Math.random() * baseInterval) +
baseInterval);
return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
statusInterval);
},
+
+ /**
+ * Return a set of hostnames (including the protocol) which may have
+ * credentials for sync itself stored in the login manager.
+ *
+ * In general, these hosts will not have their passwords synced, will be
+ * reset when we drop sync credentials, etc.
+ */
+ getSyncCredentialsHosts: function() {
+ // This is somewhat expensive and the result static, so we cache the result.
+ if (this._syncCredentialsHosts) {
+ return this._syncCredentialsHosts;
+ }
+ let result = new Set();
+ // the legacy sync host.
+ result.add(PWDMGR_HOST);
+ // The FxA hosts - these almost certainly all have the same hostname, but
+ // better safe than sorry...
+ for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
+ "identity.fxaccounts.remote.uri",
+ "identity.fxaccounts.settings.uri"]) {
+ let prefVal;
+ try {
+ prefVal = Services.prefs.getCharPref(prefName);
+ } catch (_) {
+ continue;
+ }
+ let uri = Services.io.newURI(prefVal, null, null);
+ result.add(uri.prePath);
+ }
+ return this._syncCredentialsHosts = result;
+ },
};
XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter;
});
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_fxa_startOver.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/browserid_identity.js");
+Cu.import("resource://services-sync/service.js");
+
+function run_test() {
+ initTestLogging("Trace");
+ run_next_test();
+}
+
+add_task(function* test_startover() {
+ let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
+
+ yield configureIdentity({username: "johndoe"});
+ // The pref that forces FxA identities should not be set.
+ do_check_false(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
+ // And the boolean flag on the xpcom service should reflect this.
+ let xps = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ do_check_false(xps.fxAccountsEnabled);
+
+ // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
+ // extends it)
+ do_check_false(Service.identity instanceof BrowserIDManager);
+ Service.login();
+ // We should have a cluster URL
+ do_check_true(Service.clusterURL.length > 0);
+
+ // remember some stuff so we can reset it after.
+ let oldIdentidy = Service.identity;
+ let oldClusterManager = Service._clusterManager;
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function observeStartOverFinished() {
+ Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
+ deferred.resolve();
+ }, "weave:service:start-over:finish", false);
+
+ Service.startOver();
+ yield deferred; // wait for the observer to fire.
+
+ // should have reset the pref that indicates if FxA is enabled.
+ do_check_true(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
+ // the xpcom service should agree FxA is enabled.
+ do_check_true(xps.fxAccountsEnabled);
+ // should have swapped identities.
+ do_check_true(Service.identity instanceof BrowserIDManager);
+ // should have clobbered the cluster URL
+ do_check_eq(Service.clusterURL, "");
+
+ // reset the world.
+ Service.identity = oldIdentity = Service.identity;
+ Service._clusterManager = Service._clusterManager;
+ Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
+
+ Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -116,16 +116,19 @@ skip-if = os == "android"
[test_notifications.js]
[test_score_triggers.js]
[test_sendcredentials_controller.js]
[test_status.js]
[test_status_checkSetup.js]
[test_syncscheduler.js]
[test_upgrade_old_sync_key.js]
+# Firefox Accounts specific tests
+[test_fxa_startOver.js]
+
# Finally, we test each engine.
[test_addons_engine.js]
run-sequentially = Hardcoded port in static files.
[test_addons_reconciler.js]
[test_addons_store.js]
run-sequentially = Hardcoded port in static files.
[test_addons_tracker.js]
[test_bookmark_batch_fail.js]
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -773,17 +773,17 @@ SpecialPowersAPI.prototype = {
return;
}
/* Set lock and get prefs from the _pendingPrefs queue */
this._applyingPermissions = true;
var transaction = this._pendingPermissions.shift();
var pendingActions = transaction[0];
var callback = transaction[1];
- lastPermission = pendingActions[pendingActions.length-1];
+ var lastPermission = pendingActions[pendingActions.length-1];
var self = this;
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
this._permissionObserver._lastPermission = lastPermission;
this._permissionObserver._callback = callback;
this._permissionObserver._nextCallback = function () {
self._applyingPermissions = false;
// Now apply any permissions that may have been queued while we were applying
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -780,16 +780,31 @@ File.copy = function copy(sourcePath, de
* behavior may not be the same across all platforms.
*/
File.move = function move(sourcePath, destPath, options) {
return Scheduler.post("move", [Type.path.toMsg(sourcePath),
Type.path.toMsg(destPath), options], [sourcePath, destPath]);
};
/**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} Platform-specific path to a directory on the disk to
+ * query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
+ return Scheduler.post("getAvailableFreeSpace",
+ [Type.path.toMsg(sourcePath)], sourcePath
+ ).then(Type.uint64_t.fromMsg);
+};
+
+/**
* Remove an empty directory.
*
* @param {string} path The name of the directory to remove.
* @param {*=} options Additional options.
* - {bool} ignoreAbsent If |true|, do not fail if the
* directory does not exist yet.
*/
File.removeEmptyDir = function removeEmptyDir(path, options) {
--- a/toolkit/components/osfile/modules/osfile_async_worker.js
+++ b/toolkit/components/osfile/modules/osfile_async_worker.js
@@ -319,16 +319,20 @@ const EXCEPTION_NAMES = {
copy: function copy(sourcePath, destPath, options) {
return File.copy(Type.path.fromMsg(sourcePath),
Type.path.fromMsg(destPath), options);
},
move: function move(sourcePath, destPath, options) {
return File.move(Type.path.fromMsg(sourcePath),
Type.path.fromMsg(destPath), options);
},
+ getAvailableFreeSpace: function getAvailableFreeSpace(sourcePath) {
+ return Type.uint64_t.toMsg(
+ File.getAvailableFreeSpace(Type.path.fromMsg(sourcePath)));
+ },
makeDir: function makeDir(path, options) {
return File.makeDir(Type.path.fromMsg(path), options);
},
removeEmptyDir: function removeEmptyDir(path, options) {
return File.removeEmptyDir(Type.path.fromMsg(path), options);
},
remove: function remove(path) {
return File.remove(Type.path.fromMsg(path));
--- a/toolkit/components/osfile/modules/osfile_unix_back.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_back.jsm
@@ -213,16 +213,34 @@
Const.OSFILE_OFFSETOF_TIMEVAL_TV_USEC,
"tv_usec",
Type.long.implementation);
Type.timeval = timeval.getType();
Type.timevals = new SharedAll.Type("two timevals",
ctypes.ArrayType(Type.timeval.implementation, 2));
}
+ // Types fsblkcnt_t and fsfilcnt_t, used by structure |statvfs|
+ Type.fsblkcnt_t =
+ Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName("fsblkcnt_t");
+
+ // Structure |statvfs|
+ // Use an hollow structure
+ {
+ let statvfs = new SharedAll.HollowStructure("statvfs",
+ Const.OSFILE_SIZEOF_STATVFS);
+
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BSIZE,
+ "f_bsize", Type.unsigned_long.implementation);
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL,
+ "f_bavail", Type.fsblkcnt_t.implementation);
+
+ Type.statvfs = statvfs.getType();
+ }
+
// Declare libc functions as functions of |OS.Unix.File|
// Finalizer-related functions
libc.declareLazy(SysFile, "_close",
"close", ctypes.default_abi,
/*return */ctypes.int,
/*fd*/ ctypes.int);
@@ -501,16 +519,22 @@
/*return*/ Type.long,
/*fd_in*/ Type.fd,
/*off_in*/ Type.off_t.in_ptr,
/*fd_out*/ Type.fd,
/*off_out*/Type.off_t.in_ptr,
/*len*/ Type.size_t,
/*flags*/ Type.unsigned_int); // Linux/Android-specific
+ libc.declareLazyFFI(SysFile, "statvfs",
+ "statvfs", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.statvfs.out_ptr);
+
libc.declareLazyFFI(SysFile, "symlink",
"symlink", ctypes.default_abi,
/*return*/ Type.negativeone_or_nothing,
/*source*/ Type.path,
/*dest*/ Type.path);
libc.declareLazyFFI(SysFile, "truncate",
"truncate", ctypes.default_abi,
--- a/toolkit/components/osfile/modules/osfile_unix_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -347,16 +347,37 @@
ctypes.errno == Const.ENOENT) {
return;
}
throw new File.Error("removeEmptyDir");
}
};
/**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} sourcePath Platform-specific path to a directory on
+ * the disk to query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+ File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) {
+ let fileSystemInfo = new Type.statvfs.implementation();
+ let fileSystemInfoPtr = fileSystemInfo.address();
+
+ throw_on_negative("statvfs", UnixFile.statvfs(sourcePath, fileSystemInfoPtr));
+
+ let bytes = new Type.uint64_t.implementation(
+ fileSystemInfo.f_bsize * fileSystemInfo.f_bavail);
+
+ return bytes.value;
+ };
+
+ /**
* Default mode for opening directories.
*/
const DEFAULT_UNIX_MODE_DIR = Const.S_IRWXU;
/**
* Create a directory.
*
* @param {string} path The name of the directory.
--- a/toolkit/components/osfile/modules/osfile_win_back.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_back.jsm
@@ -291,16 +291,24 @@
libc.declareLazyFFI(SysFile, "GetCurrentDirectory",
"GetCurrentDirectoryW", ctypes.winapi_abi,
/*return*/ Type.zero_or_DWORD,
/*length*/ Type.DWORD,
/*buf*/ Type.out_path
);
+ libc.declareLazyFFI(SysFile, "GetDiskFreeSpaceEx",
+ "GetDiskFreeSpaceExW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*directoryName*/ Type.path,
+ /*freeBytesForUser*/ Type.uint64_t.out_ptr,
+ /*totalBytesForUser*/ Type.uint64_t.out_ptr,
+ /*freeTotalBytesOnDrive*/ Type.uint64_t.out_ptr);
+
libc.declareLazyFFI(SysFile, "GetFileInformationByHandle",
"GetFileIn