diff --git a/b2g/chrome/content/devtools.js b/b2g/chrome/content/devtools.js index a046fa4916a..5cdc449f601 100644 --- a/b2g/chrome/content/devtools.js +++ b/b2g/chrome/content/devtools.js @@ -15,6 +15,12 @@ XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() { return devtools.require("devtools/toolkit/webconsole/utils").Utils; }); +XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() { + const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + return devtools.require("devtools/server/actors/eventlooplag").EventLoopLagFront; +}); + + /** * The Widget Panel is an on-device developer tool that displays widgets, * showing visual debug information about apps. Each widget corresponds to a @@ -358,4 +364,67 @@ let consoleWatcher = { return source; } }; + devtoolsWidgetPanel.registerWatcher(consoleWatcher); + + +let jankWatcher = { + _client: null, + _fronts: new Map(), + _active: false, + + init: function(client) { + this._client = client; + + SettingsListener.observe('devtools.hud.jank', false, + this.settingsListener.bind(this)); + }, + + settingsListener: function(value) { + if (this._active == value) { + return; + } + this._active = value; + + // Toggle the state of existing fronts. + let fronts = this._fronts; + for (let app of fronts.keys()) { + if (value) { + fronts.get(app).start(); + } else { + fronts.get(app).stop(); + app.metrics.set('jank', 0); + app.display(); + } + } + }, + + trackApp: function(app) { + app.metrics.set('jank', 0); + + let front = new EventLoopLagFront(this._client, app.actor); + this._fronts.set(app, front); + + front.on('event-loop-lag', time => { + app.metrics.set('jank', time); + + if (!app.display()) { + devtoolsWidgetPanel.log('jank: ' + time + 'ms'); + } + }); + + if (this._active) { + front.start(); + } + }, + + untrackApp: function(app) { + let fronts = this._fronts; + if (fronts.has(app)) { + fronts.get(app).destroy(); + fronts.delete(app); + } + } +}; + +devtoolsWidgetPanel.registerWatcher(jankWatcher); diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc index 68607ce9d28..21abe3eb6fb 100644 --- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -50,6 +50,9 @@ +#ifdef XP_MACOSX + +#endif @@ -350,6 +353,9 @@ +#ifdef XP_MACOSX + +#endif diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 8f2c28a324b..aa408cabe83 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -3037,17 +3037,7 @@ filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL); this.mTabListeners[0] = tabListener; this.mTabFilters[0] = filter; - - try { - // We assume this can only fail because mCurrentBrowser's docShell - // hasn't been created, yet. This may be caused by code accessing - // gBrowser before the window has finished loading. - this._addProgressListenerForInitialTab(); - } catch (e) { - // The binding was constructed too early, wait until the initial - // tab's document is ready, then add the progress listener. - this._waitForInitialContentDocument(); - } + this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL); this.style.backgroundColor = Services.prefs.getBoolPref("browser.display.use_system_colors") ? @@ -3083,31 +3073,6 @@ ]]> - - - - - - { - if (this.browsers[0].contentWindow == subject) { - Services.obs.removeObserver(obs, topic); - this._addProgressListenerForInitialTab(); - } - }; - - // We use content-document-global-created as an approximation for - // "docShell is initialized". We can do this because in the - // mTabProgressListener we care most about the STATE_STOP notification - // that will reset mBlank. That means it's important to at least add - // the progress listener before the initial about:blank load stops - // if we can't do it before the load starts. - Services.obs.addObserver(obs, "content-document-global-created", false); - ]]> - - " + aText + ""); tabs.push(newTab); @@ -61,7 +64,9 @@ function continueTests1() { // Confirm the first tab is still correct, ensure re-hiding works as expected gBrowser.selectedTab = tabs[0]; ok(!gFindBar.hidden, "First tab shows find bar!"); - is(gFindBar._findField.value, texts[0], "First tab persists find value!"); + // When the Find Clipboard is supported, this test not relevant. + if (!HasFindClipboard) + is(gFindBar._findField.value, texts[0], "First tab persists find value!"); ok(gFindBar.getElement("highlight").checked, "Highlight button state persists!"); @@ -94,7 +99,8 @@ function continueTests2() { ok(gFindBar.hidden, "Fourth tab doesn't show find bar!"); is(gFindBar, gBrowser.getFindBar(), "Find bar is right one!"); gFindBar.open(); - is(gFindBar._findField.value, texts[1], + let toTest = HasFindClipboard ? texts[2] : texts[1]; + is(gFindBar._findField.value, toTest, "Fourth tab has second tab's find value!"); newWindow = gBrowser.replaceTabWithWindow(tabs.pop()); @@ -104,7 +110,8 @@ function continueTests2() { // Test that findbar gets restored when a tab is moved to a new window. function checkNewWindow() { ok(!newWindow.gFindBar.hidden, "New window shows find bar!"); - is(newWindow.gFindBar._findField.value, texts[1], + let toTest = HasFindClipboard ? texts[2] : texts[1]; + is(newWindow.gFindBar._findField.value, toTest, "New window find bar has correct find value!"); ok(!newWindow.gFindBar.getElement("find-next").disabled, "New window findbar has enabled buttons!"); diff --git a/browser/base/content/test/general/browser_bug567306.js b/browser/base/content/test/general/browser_bug567306.js index 1fa35efa851..f3507f66eea 100644 --- a/browser/base/content/test/general/browser_bug567306.js +++ b/browser/base/content/test/general/browser_bug567306.js @@ -2,7 +2,10 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -let Ci = Components.interfaces; +const {Ci: interfaces, Cc: classes} = Components; + +let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); +let HasFindClipboard = Clipboard.supportsFindClipboard(); function test() { waitForExplicitFinish(); @@ -37,7 +40,10 @@ function onFocus(win) { let findBar = win.gFindBar; selectText(win.content); findBar.onFindCommand(); - is(findBar._findField.value, "Select Me", "Findbar is initialized with selection"); + // When the OS supports the Find Clipboard (OSX), the find field value is + // persisted across Fx sessions, thus not useful to test. + if (!HasFindClipboard) + is(findBar._findField.value, "Select Me", "Findbar is initialized with selection"); findBar.close(); win.close(); finish(); diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index 8fee283f03f..4842e1b4322 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -614,6 +614,7 @@ you can use these alternative items. Otherwise, their values should be empty. - + diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 1e9831eaa3c..539550634e2 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -2434,11 +2434,11 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url- .panel-promo-box { margin: 8px -16px -16px -16px; padding: 8px 16px; - background: #e5e5e5; - border-top: 1px solid hsla(0,0%,0%,.1); + background-color: hsla(210,4%,10%,.07); + border-top: 1px solid hsla(210,4%,10%,.12); border-radius: 0 0 5px 5px; box-shadow: 0 -1px hsla(0,0%,100%,.5) inset, 0 1px 1px hsla(0,0%,0%,.03) inset; - color: #808080; + color: hsl(0,0%,30%); } .panel-promo-icon { @@ -3214,6 +3214,10 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { } } +#identity-popup { + margin-top: 1px; +} + #identity-popup > .panel-arrowcontainer > .panel-arrowcontent { padding: 0; } @@ -3223,7 +3227,8 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { } #identity-popup-button-container { - background: linear-gradient(to bottom, rgba(0,0,0,0.04) 60%, transparent); + background-color: hsla(210,4%,10%,.07); + border-top: 1px solid hsla(210,4%,10%,.12); padding: 16px; margin-top: 5px; } @@ -4022,7 +4027,6 @@ toolbar[mode="icons"] > *|* > .toolbarbutton-badge[badge]:not([badge=""]):-moz-l } #social-share-panel { - margin-top: 3px; max-height: 600px; min-height: 100px; max-width: 800px; diff --git a/browser/themes/osx/downloads/downloads.css b/browser/themes/osx/downloads/downloads.css index d317eb36d9e..315851adc51 100644 --- a/browser/themes/osx/downloads/downloads.css +++ b/browser/themes/osx/downloads/downloads.css @@ -4,6 +4,10 @@ /*** Panel and outer controls ***/ +#downloadsPanel { + margin-top: -1px; +} + #downloadsPanel > .panel-arrowcontainer > .panel-arrowcontent { padding: 0; } @@ -19,8 +23,8 @@ } #downloadsFooter { - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; } #downloadsHistory { @@ -29,11 +33,6 @@ cursor: pointer; } -#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory { - border-top-left-radius: 6px; - border-top-right-radius: 6px; -} - #downloadsPanel[hasdownloads] > #downloadsFooter { background: #e5e5e5; border-top: 1px solid hsla(0,0%,0%,.1); @@ -49,18 +48,13 @@ #downloadsPanel[keyfocus] > #downloadsFooter > #downloadsHistory:focus { outline: 2px -moz-mac-focusring solid; outline-offset: -2px; - -moz-outline-radius-bottomleft: 5px; - -moz-outline-radius-bottomright: 5px; + -moz-outline-radius-bottomleft: 4px; + -moz-outline-radius-bottomright: 4px; } #downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:focus { - -moz-outline-radius-topleft: 5px; - -moz-outline-radius-topright: 5px; -} - -#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:focus > .button-box { - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; + -moz-outline-radius-topleft: 4px; + -moz-outline-radius-topright: 4px; } /*** Downloads Summary and List items ***/ diff --git a/browser/themes/shared/customizableui/panelUIOverlay.inc.css b/browser/themes/shared/customizableui/panelUIOverlay.inc.css index da152e3740e..b2400652f9a 100644 --- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css +++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css @@ -30,11 +30,10 @@ padding: 4px; background-color: hsla(0,0%,100%,.97); background-clip: padding-box; - border-right: 1px solid hsla(210,4%,10%,.2); - border-left: 1px solid hsla(210,4%,10%,.2); + border-left: 1px solid hsla(210,4%,10%,.3); box-shadow: 0 3px 5px hsla(210,4%,10%,.1), 0 0 7px hsla(210,4%,10%,.1); - color: #222426; + color: hsl(0,0%,15%); -moz-margin-start: @exitSubviewGutterWidth@; } @@ -68,19 +67,13 @@ .panel-subview-header, .subviewbutton.panel-subview-footer { padding: 12px; - background-color: hsla(210,4%,10%,.04); } .panel-subview-header { margin: -4px -4px 4px; - box-shadow: 0 -1px 0 hsla(210,4%,10%,.08) inset; - color: #797c80; -} - -.subviewbutton.panel-subview-footer { - margin: 4px -4px -4px; - box-shadow: 0 1px 0 hsla(210,4%,10%,.08) inset; - border-radius: 0; + background-color: hsla(210,4%,10%,.05); + box-shadow: 0 -1px 0 hsla(210,4%,10%,.05) inset; + color: hsl(0,0%,50%); } .cui-widget-panelview .panel-subview-header { @@ -201,8 +194,8 @@ toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"]:not(.panel-wide-it } /* Help SDK buttons fit in. */ -toolbarpaletteitem[place="palette"] > toolbarbutton[sdk-button="true"] > .toolbarbutton-icon, -toolbarbutton[sdk-button="true"][cui-areatype="menu-panel"] > .toolbarbutton-icon { +toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-placeholder, +#personal-bookmarks[cui-areatype="menu-panel"] > #bookmarks-toolbar-placeholder { height: 32px; width: 32px; } @@ -320,20 +313,19 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { display: flex; flex-shrink: 0; flex-direction: column; - background-color: rgba(0, 0, 0, 0.05); - box-shadow: 0 -1px 0 rgba(0,0,0,.15); + background-color: hsla(210,4%,10%,.07); padding: 0; margin: 0; } #PanelUI-footer-inner { display: flex; - box-shadow: 0 -1px 0 rgba(0,0,0,.15); + border-top: 1px solid hsla(210,4%,10%,.14); } #PanelUI-footer-inner > toolbarseparator { border: 0; - border-left: 1px solid rgba(0,0,0,0.1); + border-left: 1px solid hsla(210,4%,10%,.14); margin: 7px 0 7px; -moz-appearance: none; } @@ -352,15 +344,16 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { -moz-appearance: none; box-shadow: none; background-image: none; - border: 1px solid transparent; - border-bottom-style: none; + border: none; border-radius: 0; transition: background-color; -moz-box-orient: horizontal; } #PanelUI-fxa-status { - border-bottom-style: solid; + border-top: 1px solid hsla(210,4%,10%,.14); + border-bottom: 1px solid transparent; + margin-bottom: -1px; } #PanelUI-fxa-status > .toolbarbutton-text { @@ -451,18 +444,26 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { opacity: 0.4; } +#PanelUI-fxa-status:not([disabled]):hover, #PanelUI-help:not([disabled]):hover, #PanelUI-customize:hover, #PanelUI-quit:not([disabled]):hover { - outline: 1px solid rgba(0,0,0,0.1); - background-color: rgba(0,0,0,0.1); - box-shadow: none; + outline: 1px solid hsla(210,4%,10%,.07); + background-color: hsla(210,4%,10%,.07); } -#PanelUI-fxa-status:not([disabled]):hover { - background-color: rgba(0,0,0,0.1); - border-bottom-color: rgba(0,0,0,0.1); - box-shadow: 0 -1px 0 rgba(0,0,0,0.2); +#PanelUI-fxa-status:not([disabled]):hover:active, +#PanelUI-help:not([disabled]):hover:active, +#PanelUI-customize:hover:active, +#PanelUI-quit:not([disabled]):hover:active { + outline: 1px solid hsla(210,4%,10%,.12); + background-color: hsla(210,4%,10%,.12); + box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset; +} + +#PanelUI-fxa-status:not([disabled]):hover, +#PanelUI-fxa-status:not([disabled]):hover:active { + outline: none; } #PanelUI-quit:not([disabled]):hover { @@ -472,6 +473,7 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { #PanelUI-quit:not([disabled]):hover:active { background-color: #ad3434; + outline-color: #992e2e; } #customization-panelHolder #PanelUI-customize { @@ -552,12 +554,12 @@ panelview .toolbarbutton-1@buttonStateHover@, #edit-controls@inAnyPanel@ > toolbarbutton@buttonStateHover@, #zoom-controls@inAnyPanel@ > toolbarbutton@buttonStateHover@ { background-color: hsla(210,4%,10%,.08); - border-color: hsla(210,4%,10%,.1); + border-color: hsla(210,4%,10%,.11); } #edit-controls@inAnyPanel@@buttonStateHover@, #zoom-controls@inAnyPanel@@buttonStateHover@ { - border-color: hsla(210,4%,10%,.1); + border-color: hsla(210,4%,10%,.11); } panelview .toolbarbutton-1@buttonStateActive@, @@ -565,9 +567,28 @@ panelview .toolbarbutton-1@buttonStateActive@, .widget-overflow-list .toolbarbutton-1@buttonStateActive@, #edit-controls@inAnyPanel@ > toolbarbutton@buttonStateActive@, #zoom-controls@inAnyPanel@ > toolbarbutton@buttonStateActive@ { + background-color: hsla(210,4%,10%,.12); + border-color: hsla(210,4%,10%,.14); + box-shadow: 0 1px 0 hsla(210,4%,10%,.03) inset; +} + +.subviewbutton.panel-subview-footer { + margin: 4px -4px -4px; + background-color: hsla(210,4%,10%,.07); + border-top: 1px solid hsla(210,4%,10%,.12); + border-radius: 0; + color: hsl(0,0%,25%) +} + +.subviewbutton.panel-subview-footer@buttonStateHover@ { + background-color: hsla(210,4%,10%,.1); + border-top: 1px solid hsla(210,4%,10%,.12); +} + +.subviewbutton.panel-subview-footer@buttonStateActive@ { background-color: hsla(210,4%,10%,.15); - border-color: hsla(210,4%,10%,.15); - box-shadow: 0 1px 0 0 hsla(210,4%,10%,.05) inset; + border-top: 1px solid hsla(210,4%,10%,.12); + box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset; } #BMB_bookmarksPopup > .subviewbutton { diff --git a/browser/themes/shared/devtools/dark-theme.css b/browser/themes/shared/devtools/dark-theme.css index eac3a49a7c0..9579487b5ba 100644 --- a/browser/themes/shared/devtools/dark-theme.css +++ b/browser/themes/shared/devtools/dark-theme.css @@ -175,12 +175,6 @@ box-shadow: 0 0 0 1px rgba(0,0,0,0.5); } -.variables-view-scope:focus > .title, -.variable-or-property:focus > .title { - background: #3689b2; /* fg-color2 */ - color: white; -} - /* CodeMirror specific styles. * Best effort to match the existing theme, some of the colors * are duplicated here to prevent weirdness in the main theme. */ diff --git a/browser/themes/shared/devtools/light-theme.css b/browser/themes/shared/devtools/light-theme.css index 0971890372c..d67d0540afc 100644 --- a/browser/themes/shared/devtools/light-theme.css +++ b/browser/themes/shared/devtools/light-theme.css @@ -174,12 +174,6 @@ box-shadow: 0 0 0 1px #EFEFEF; } -.variables-view-scope:focus > .title, -.variable-or-property:focus > .title { - background: #4c9ed9; /* fg-color2 */ - color: #f5f7fa; -} - /* CodeMirror specific styles. * Best effort to match the existing theme, some of the colors * are duplicated here to prevent weirdness in the main theme. */ diff --git a/browser/themes/shared/devtools/widgets.inc.css b/browser/themes/shared/devtools/widgets.inc.css index cf7e74365c5..494289f1ec5 100644 --- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -561,6 +561,18 @@ color: #585959; /* Grey foreground text */ } +.theme-dark .variables-view-scope:focus > .title, +.theme-dark .variable-or-property:focus > .title { + background-color: #1d4f73; /* Selection colors */ + color: #f5f7fa; +} + +.theme-light .variables-view-scope:focus > .title, +.theme-light .variable-or-property:focus > .title { + background-color: #4c9ed9; /* Selection colors */ + color: #f5f7fa; +} + .variables-view-scope > .title { border-top-width: 1px; border-top-style: solid; diff --git a/browser/themes/shared/plugin-doorhanger.inc.css b/browser/themes/shared/plugin-doorhanger.inc.css index 6a9529554f1..b7dbad0d3ad 100644 --- a/browser/themes/shared/plugin-doorhanger.inc.css +++ b/browser/themes/shared/plugin-doorhanger.inc.css @@ -26,7 +26,8 @@ } .click-to-play-plugins-notification-button-container { - background: linear-gradient(rgba(0,0,0,0.04) 60%, transparent); + background-color: hsla(210,4%,10%,.07); + border-top: 1px solid hsla(210,4%,10%,.12); padding: 10px; margin-top: 5px; } diff --git a/content/media/webrtc/MediaEngineDefault.cpp b/content/media/webrtc/MediaEngineDefault.cpp index 3dbb0851590..114a902b43c 100644 --- a/content/media/webrtc/MediaEngineDefault.cpp +++ b/content/media/webrtc/MediaEngineDefault.cpp @@ -11,7 +11,9 @@ #include "ImageContainer.h" #include "ImageTypes.h" #include "prmem.h" +#include "nsContentUtils.h" +#include "nsIFilePicker.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" @@ -164,18 +166,38 @@ MediaEngineDefaultVideoSource::Snapshot(uint32_t aDuration, nsIDOMFile** aFile) #ifndef MOZ_WIDGET_ANDROID return NS_ERROR_NOT_IMPLEMENTED; #else - if (!AndroidBridge::Bridge()) { - return NS_ERROR_UNEXPECTED; + nsAutoString filePath; + nsCOMPtr filePicker = do_CreateInstance("@mozilla.org/filepicker;1"); + if (!filePicker) + return NS_ERROR_FAILURE; + + nsXPIDLString title; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "Browse", title); + int16_t mode = static_cast(nsIFilePicker::modeOpen); + + nsresult rv = filePicker->Init(nullptr, title, mode); + NS_ENSURE_SUCCESS(rv, rv); + filePicker->AppendFilters(nsIFilePicker::filterImages); + + // XXX - This API should be made async + PRInt16 dialogReturn; + rv = filePicker->Show(&dialogReturn); + NS_ENSURE_SUCCESS(rv, rv); + if (dialogReturn == nsIFilePicker::returnCancel) { + *aFile = nullptr; + return NS_OK; } - nsAutoString filePath; - AndroidBridge::Bridge()->ShowFilePickerForMimeType(filePath, NS_LITERAL_STRING("image/*")); + nsCOMPtr localFile; + filePicker->GetFile(getter_AddRefs(localFile)); - nsCOMPtr file; - nsresult rv = NS_NewLocalFile(filePath, false, getter_AddRefs(file)); - NS_ENSURE_SUCCESS(rv, rv); + if (!localFile) { + *aFile = nullptr; + return NS_OK; + } - NS_ADDREF(*aFile = new nsDOMFileFile(file)); + nsCOMPtr domFile = new nsDOMFileFile(localFile); + domFile.forget(aFile); return NS_OK; #endif } diff --git a/mobile/android/base/ActivityHandlerHelper.java b/mobile/android/base/ActivityHandlerHelper.java index 90404d043b0..cdedaf0e5be 100644 --- a/mobile/android/base/ActivityHandlerHelper.java +++ b/mobile/android/base/ActivityHandlerHelper.java @@ -238,82 +238,6 @@ public class ActivityHandlerHelper implements GeckoEventListener { }); } - private Intent getFilePickerIntent(Context context, String aMimeType) { - ArrayList intents = new ArrayList(); - final Prompt.PromptListItem[] items = - getItemsAndIntentsForFilePicker(context, aMimeType, intents); - - if (intents.size() == 0) { - Log.i(LOGTAG, "no activities for the file picker!"); - return null; - } - - if (intents.size() == 1) { - return intents.get(0); - } - - final PromptService ps = GeckoAppShell.getGeckoInterface().getPromptService(); - final String title = getFilePickerTitle(context, aMimeType); - - // Runnable has to be called to show an intent-like - // context menu UI using the PromptService. - ThreadUtils.postToUiThread(new Runnable() { - @Override public void run() { - ps.show(title, "", items, false, null); - } - }); - - String promptServiceResult = ps.getResponse(null); - int itemId = -1; - try { - itemId = new JSONObject(promptServiceResult).getInt("button"); - - if (itemId == -1) { - return null; - } - } catch (JSONException e) { - Log.e(LOGTAG, "result from promptservice was invalid: ", e); - return null; - } - - return intents.get(itemId); - } - - boolean showFilePicker(Activity parentActivity, String aMimeType, ActivityResultHandler handler) { - Intent intent = getFilePickerIntent(parentActivity, aMimeType); - - if (intent == null) { - return false; - } - parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(handler)); - return true; - } - - String showFilePicker(Activity parentActivity, String aMimeType) { - Intent intent = getFilePickerIntent(parentActivity, aMimeType); - - if (intent == null) { - return ""; - } - - if (intent.getAction().equals(MediaStore.ACTION_IMAGE_CAPTURE)) { - parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(mCameraImageResultHandler)); - } else if (intent.getAction().equals(MediaStore.ACTION_VIDEO_CAPTURE)) { - parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(mCameraVideoResultHandler)); - } else if (intent.getAction().equals(Intent.ACTION_GET_CONTENT)) { - parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(mFilePickerResultHandlerSync)); - } else { - Log.e(LOGTAG, "We should not get an intent with another action!"); - return ""; - } - - String filePickerResult; - while (null == (filePickerResult = mFilePickerResult.poll())) { - GeckoAppShell.processNextNativeEvent(true); - } - return filePickerResult; - } - /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and * sends the file returned to the passed in handler. If a null handler is passed in, will still * pick and launch the file picker, but will throw away the result. diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index e3ee47968cc..4a750cca813 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -31,7 +31,7 @@ import org.mozilla.gecko.prompts.Prompt; import org.mozilla.gecko.toolbar.AutocompleteHandler; import org.mozilla.gecko.toolbar.BrowserToolbar; import org.mozilla.gecko.util.Clipboard; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.MenuUtils; diff --git a/mobile/android/base/ContactService.java b/mobile/android/base/ContactService.java index 80b4848f374..03c4b1a3423 100644 --- a/mobile/android/base/ContactService.java +++ b/mobile/android/base/ContactService.java @@ -6,7 +6,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.gfx.LayerView; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; diff --git a/mobile/android/base/util/EventDispatcher.java b/mobile/android/base/EventDispatcher.java similarity index 77% rename from mobile/android/base/util/EventDispatcher.java rename to mobile/android/base/EventDispatcher.java index 5b6d50883b4..2e3fbd02e60 100644 --- a/mobile/android/base/util/EventDispatcher.java +++ b/mobile/android/base/EventDispatcher.java @@ -2,7 +2,11 @@ * 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.util; +package org.mozilla.gecko; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONObject; @@ -14,6 +18,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public final class EventDispatcher { private static final String LOGTAG = "GeckoEventDispatcher"; + private static final String GUID = "__guid__"; private final Map> mEventListeners = new HashMap>(); @@ -52,18 +57,16 @@ public final class EventDispatcher { } } - public String dispatchEvent(String message) { + public void dispatchEvent(String message) { try { JSONObject json = new JSONObject(message); - return dispatchEvent(json); + dispatchEvent(json); } catch (Exception e) { Log.e(LOGTAG, "dispatchEvent: malformed JSON.", e); } - - return ""; } - public String dispatchEvent(JSONObject json) { + public void dispatchEvent(JSONObject json) { // { // "type": "value", // "event_specific": "value", @@ -87,29 +90,29 @@ public final class EventDispatcher { if (listeners == null || listeners.size() == 0) { Log.d(LOGTAG, "dispatchEvent: no listeners registered for event '" + type + "'"); - return ""; + return; } - String response = null; - for (GeckoEventListener listener : listeners) { listener.handleMessage(type, json); - if (listener instanceof GeckoEventResponder) { - String newResponse = ((GeckoEventResponder)listener).getResponse(json); - if (response != null && newResponse != null) { - Log.e(LOGTAG, "Received two responses for message of type " + type); - } - response = newResponse; - } } - - if (response != null) - return response; - } catch (Exception e) { Log.e(LOGTAG, "handleGeckoMessage throws " + e, e); } - return ""; + } + + public static void sendResponse(JSONObject message, JSONObject response) { + try { + response.put(GUID, message.getString(GUID)); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Return", response.toString())); + } catch(Exception ex) { } + } + + public static void sendError(JSONObject message, JSONObject error) { + try { + error.put(GUID, message.getString(GUID)); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Error", error.toString())); + } catch(Exception ex) { } } } diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 4208fda05ea..29af49b9467 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -23,9 +23,7 @@ import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.updater.UpdateService; import org.mozilla.gecko.updater.UpdateServiceHelper; import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; @@ -134,7 +132,6 @@ public abstract class GeckoApp ContextGetter, GeckoAppShell.GeckoInterface, GeckoEventListener, - GeckoEventResponder, GeckoMenu.Callback, GeckoMenu.MenuPresenter, LocationListener, @@ -187,7 +184,6 @@ public abstract class GeckoApp protected GeckoProfile mProfile; public static int mOrientation; protected boolean mIsRestoringActivity; - private String mCurrentResponse = ""; private ContactService mContactService; private PromptService mPromptService; @@ -685,7 +681,7 @@ public abstract class GeckoApp } JSONObject handlersJSON = new JSONObject(); handlersJSON.put("apps", new JSONArray(appList)); - mCurrentResponse = handlersJSON.toString(); + EventDispatcher.sendResponse(message, handlersJSON); } else if (event.equals("Intent:Open")) { GeckoAppShell.openUriExternal(message.optString("url"), message.optString("mime"), message.optString("packageName"), @@ -693,7 +689,9 @@ public abstract class GeckoApp } else if (event.equals("Locale:Set")) { setLocale(message.getString("locale")); } else if (event.equals("NativeApp:IsDebuggable")) { - mCurrentResponse = getIsDebuggable() ? "true" : "false"; + JSONObject ret = new JSONObject(); + ret.put("isDebuggable", getIsDebuggable() ? "true" : "false"); + EventDispatcher.sendResponse(message, ret); } else if (event.equals("SystemUI:Visibility")) { setSystemUiVisible(message.getBoolean("visible")); } @@ -702,12 +700,6 @@ public abstract class GeckoApp } } - public String getResponse(JSONObject origMessage) { - String res = mCurrentResponse; - mCurrentResponse = ""; - return res; - } - void onStatePurged() { } /** diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index d7a8a7359fc..fee94a081a2 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -18,7 +18,6 @@ import org.mozilla.gecko.mozglue.JNITarget; import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.prompts.PromptService; import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ProxySelector; @@ -1427,20 +1426,6 @@ public class GeckoAppShell getGeckoInterface().setFullScreen(fullscreen); } - @WrapElementForJNI(stubName = "ShowFilePickerForExtensionsWrapper") - public static String showFilePickerForExtensions(String aExtensions) { - if (getGeckoInterface() != null) - return sActivityHelper.showFilePicker(getGeckoInterface().getActivity(), getMimeTypeFromExtensions(aExtensions)); - return ""; - } - - @WrapElementForJNI(stubName = "ShowFilePickerForMimeTypeWrapper") - public static String showFilePickerForMimeType(String aMimeType) { - if (getGeckoInterface() != null) - return sActivityHelper.showFilePicker(getGeckoInterface().getActivity(), aMimeType); - return ""; - } - @WrapElementForJNI public static void performHapticFeedback(boolean aIsLongPress) { // Don't perform haptic feedback if a vibration is currently playing, @@ -2345,8 +2330,8 @@ public class GeckoAppShell } @WrapElementForJNI(stubName = "HandleGeckoMessageWrapper") - public static String handleGeckoMessage(String message) { - return sEventDispatcher.dispatchEvent(message); + public static void handleGeckoMessage(String message) { + sEventDispatcher.dispatchEvent(message); } @WrapElementForJNI @@ -2630,17 +2615,6 @@ public class GeckoAppShell return true; } - static native void notifyFilePickerResult(String filePath, long id); - - @WrapElementForJNI(stubName = "ShowFilePickerAsyncWrapper") - public static void showFilePickerAsync(String aMimeType, final long id) { - sActivityHelper.showFilePickerAsync(getGeckoInterface().getActivity(), aMimeType, new ActivityHandlerHelper.FileResultHandler() { - public void gotFile(String filename) { - GeckoAppShell.notifyFilePickerResult(filename, id); - } - }); - } - @WrapElementForJNI public static void notifyWakeLockChanged(String topic, String state) { if (getGeckoInterface() != null) diff --git a/mobile/android/base/JavaAddonManager.java b/mobile/android/base/JavaAddonManager.java index fa59760d6d5..21e15050035 100644 --- a/mobile/android/base/JavaAddonManager.java +++ b/mobile/android/base/JavaAddonManager.java @@ -5,9 +5,7 @@ package org.mozilla.gecko; -import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.GeckoEventResponder; import org.json.JSONException; import org.json.JSONObject; @@ -140,7 +138,7 @@ class JavaAddonManager implements GeckoEventListener { } } - private static class CallbackWrapper implements GeckoEventResponder { + private static class CallbackWrapper implements GeckoEventListener { private final Handler.Callback mDelegate; private Bundle mBundle; @@ -184,16 +182,14 @@ class JavaAddonManager implements GeckoEventListener { Message msg = new Message(); msg.setData(mBundle); mDelegate.handleMessage(msg); + + JSONObject obj = new JSONObject(); + obj.put("response", mBundle.getString("response")); + EventDispatcher.sendResponse(json, obj); + mBundle = null; } catch (Exception e) { Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e); } } - - @Override - public String getResponse(JSONObject origMessage) { - String response = mBundle.getString("response"); - mBundle = null; - return response; - } } } diff --git a/mobile/android/base/OrderedBroadcastHelper.java b/mobile/android/base/OrderedBroadcastHelper.java index 57236980c90..e0b0979c367 100644 --- a/mobile/android/base/OrderedBroadcastHelper.java +++ b/mobile/android/base/OrderedBroadcastHelper.java @@ -6,7 +6,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONException; diff --git a/mobile/android/base/SharedPreferencesHelper.java b/mobile/android/base/SharedPreferencesHelper.java index 696bc4587dd..da66208f227 100644 --- a/mobile/android/base/SharedPreferencesHelper.java +++ b/mobile/android/base/SharedPreferencesHelper.java @@ -5,8 +5,8 @@ package org.mozilla.gecko; -import org.mozilla.gecko.util.EventDispatcher; -import org.mozilla.gecko.util.GeckoEventResponder; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONArray; import org.json.JSONException; @@ -24,14 +24,12 @@ import java.util.HashMap; * Helper class to get, set, and observe Android Shared Preferences. */ public final class SharedPreferencesHelper - implements GeckoEventResponder + implements GeckoEventListener { public static final String LOGTAG = "GeckoAndSharedPrefs"; protected final Context mContext; - protected String mResponse; - // mListeners is not synchronized because it is only updated in // handleObserve, which is called from Gecko serially. protected final Map mListeners; @@ -117,7 +115,7 @@ public final class SharedPreferencesHelper * must include a String name, and a String type in ["bool", "int", * "string"]. */ - private String handleGet(JSONObject message) throws JSONException { + private JSONArray handleGet(JSONObject message) throws JSONException { if (!message.has("branch")) { Log.e(LOGTAG, "No branch specified for SharedPreference:Get; aborting."); return null; @@ -156,7 +154,7 @@ public final class SharedPreferencesHelper jsonValues.put(jsonValue); } - return jsonValues.toString(); + return jsonValues; } private static class ChangeListener @@ -225,8 +223,6 @@ public final class SharedPreferencesHelper public void handleMessage(String event, JSONObject message) { // Everything here is synchronous and serial, so we need not worry about // overwriting an in-progress response. - mResponse = null; - try { if (event.equals("SharedPreferences:Set")) { if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { @@ -237,9 +233,9 @@ public final class SharedPreferencesHelper if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { Log.v(LOGTAG, "Got SharedPreferences:Get message."); } - // Synchronous and serial, so we are the only consumer of - // mResponse and can write to it freely. - mResponse = handleGet(message); + JSONObject obj = new JSONObject(); + obj.put("values", handleGet(message)); + EventDispatcher.sendResponse(message, obj); } else if (event.equals("SharedPreferences:Observe")) { if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { Log.v(LOGTAG, "Got SharedPreferences:Observe message."); @@ -254,9 +250,4 @@ public final class SharedPreferencesHelper return; } } - - @Override - public String getResponse(JSONObject origMessage) { - return mResponse; - } } diff --git a/mobile/android/base/TextSelection.java b/mobile/android/base/TextSelection.java index 983c43ad47c..3a2b4071d3c 100644 --- a/mobile/android/base/TextSelection.java +++ b/mobile/android/base/TextSelection.java @@ -10,7 +10,7 @@ import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.GeckoMenuItem; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.FloatUtils; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; diff --git a/mobile/android/base/db/BrowserContract.java b/mobile/android/base/db/BrowserContract.java index 1a743cfae06..9f9d4e4c88d 100644 --- a/mobile/android/base/db/BrowserContract.java +++ b/mobile/android/base/db/BrowserContract.java @@ -101,6 +101,8 @@ public class BrowserContract { 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"; @@ -112,6 +114,8 @@ public class BrowserContract { 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"; @@ -122,6 +126,10 @@ public class BrowserContract { 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; @@ -173,6 +181,11 @@ public class BrowserContract { @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"; @@ -183,6 +196,11 @@ public class BrowserContract { @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; @@ -316,4 +334,47 @@ public class BrowserContract { 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"; + } } diff --git a/mobile/android/base/db/BrowserDatabaseHelper.java b/mobile/android/base/db/BrowserDatabaseHelper.java new file mode 100644 index 00000000000..47de4c1d929 --- /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 subFolders = new ArrayList(); + List invalidSpecialEntries = new ArrayList(); + + 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 diff --git a/mobile/android/base/db/BrowserProvider.java b/mobile/android/base/db/BrowserProvider.java index 471e6cd8911..9f80e44fc36 100644 --- a/mobile/android/base/db/BrowserProvider.java +++ b/mobile/android/base/db/BrowserProvider.java @@ -5,10 +5,11 @@ 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; @@ -19,18 +20,9 @@ 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; @@ -43,35 +35,13 @@ 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 { private static final String LOGTAG = "GeckoBrowserProvider"; - private Context mContext; - - private PerProfileDatabases 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; @@ -94,20 +64,15 @@ public class BrowserProvider extends ContentProvider { // 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; static final String VIEW_FLAGS = "flags"; @@ -151,14 +116,6 @@ public class BrowserProvider extends ContentProvider { 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 BOOKMARKS_PROJECTION_MAP; @@ -169,43 +126,6 @@ public class BrowserProvider extends ContentProvider { static final Map FAVICONS_PROJECTION_MAP; static final Map 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 map; @@ -321,24 +241,6 @@ public class BrowserProvider extends ContentProvider { 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; } @@ -370,1618 +272,6 @@ public class BrowserProvider extends ContentProvider { } } - 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 subFolders = new ArrayList(); - List invalidSpecialEntries = new ArrayList(); - - 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); @@ -2083,7 +373,7 @@ public class BrowserProvider extends ContentProvider { 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 ( " + @@ -2100,11 +390,6 @@ public class BrowserProvider extends ContentProvider { 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); @@ -2120,24 +405,6 @@ public class BrowserProvider extends ContentProvider { return Boolean.parseBoolean(incrementVisits); } - @Override - public boolean onCreate() { - debug("Creating BrowserProvider"); - - synchronized (this) { - mContext = getContext(); - mDatabases = new PerProfileDatabases( - getContext(), DATABASE_NAME, new DatabaseHelperFactory() { - @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); @@ -2170,37 +437,11 @@ public class BrowserProvider extends ContentProvider { return null; } - @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(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) { + @Override + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { trace("Calling delete in transaction on URI: " + uri); - + final SQLiteDatabase db = getWritableDatabase(uri); final int match = URI_MATCHER.match(uri); int deleted = 0; @@ -2284,37 +525,6 @@ public class BrowserProvider extends ContentProvider { } @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); @@ -2358,35 +568,8 @@ public class BrowserProvider extends ContentProvider { return null; } - @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") + @Override public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { trace("Calling update in transaction on URI: " + uri); @@ -2629,7 +812,7 @@ public class BrowserProvider extends ContentProvider { 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; } @@ -2649,7 +832,7 @@ public class BrowserProvider extends ContentProvider { if (hasFaviconsInProjection(projection)) qb.setTables(VIEW_COMBINED_WITH_FAVICONS); else - qb.setTables(VIEW_COMBINED); + qb.setTables(Combined.VIEW_NAME); break; } @@ -3002,13 +1185,15 @@ public class BrowserProvider extends ContentProvider { } 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)) { @@ -3057,7 +1242,7 @@ public class BrowserProvider extends ContentProvider { 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)) { @@ -3111,7 +1296,7 @@ public class BrowserProvider extends ContentProvider { trace("Inserting thumbnail for URL: " + url); - stripEmptyByteArray(values, Thumbnails.DATA); + DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); return db.insertOrThrow(TABLE_THUMBNAILS, null, values); } @@ -3134,7 +1319,7 @@ public class BrowserProvider extends ContentProvider { int updated = 0; final SQLiteDatabase db = getWritableDatabase(uri); - stripEmptyByteArray(values, Thumbnails.DATA); + DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); trace("Updating thumbnail for URL: " + url); @@ -3149,21 +1334,6 @@ public class BrowserProvider extends ContentProvider { 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); @@ -3322,31 +1492,13 @@ public class BrowserProvider extends ContentProvider { } @Override - public int bulkInsert(Uri uri, ContentValues[] values) { - if (values == null) - return 0; + protected BrowserDatabaseHelper createDatabaseHelper( + Context context, String databasePath) { + return new BrowserDatabaseHelper(context, databasePath); + } - int numValues = values.length; - int successes = 0; - - final SQLiteDatabase db = getWritableDatabase(uri); - - db.beginTransaction(); - - 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; } } diff --git a/mobile/android/base/db/DBUtils.java b/mobile/android/base/db/DBUtils.java index 71689c4a6b2..65817151340 100644 --- a/mobile/android/base/db/DBUtils.java +++ b/mobile/android/base/db/DBUtils.java @@ -81,4 +81,19 @@ public class DBUtils { 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); + } + } + } } diff --git a/mobile/android/base/db/TransactionalProvider.java b/mobile/android/base/db/TransactionalProvider.java new file mode 100644 index 00000000000..897c75b9879 --- /dev/null +++ b/mobile/android/base/db/TransactionalProvider.java @@ -0,0 +1,270 @@ +/* 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 extends ContentProvider { + private static final String LOGTAG = "GeckoTransProvider"; + protected Context mContext; + protected PerProfileDatabases 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( + getContext(), getDatabaseName(), new DatabaseHelperFactory() { + @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 SQLiteDatabase db = getWritableDatabase(uri); + + db.beginTransaction(); + + 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; + } + + 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); + } + } +} diff --git a/mobile/android/base/gfx/GeckoLayerClient.java b/mobile/android/base/gfx/GeckoLayerClient.java index 8d7dccbc7ca..572a2f42f6b 100644 --- a/mobile/android/base/gfx/GeckoLayerClient.java +++ b/mobile/android/base/gfx/GeckoLayerClient.java @@ -12,7 +12,7 @@ import org.mozilla.gecko.Tabs; import org.mozilla.gecko.ZoomConstraints; import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.FloatUtils; import android.content.Context; diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index 95673aee07c..4b8b9fbbd88 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -11,7 +11,7 @@ import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tabs; import org.mozilla.gecko.ZoomConstraints; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.FloatUtils; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.GeckoEventListener; diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index 4b3c238f3c3..b71bec03686 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -16,7 +16,7 @@ import org.mozilla.gecko.TouchEventInterceptor; import org.mozilla.gecko.ZoomConstraints; import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; import org.mozilla.gecko.mozglue.RobocopTarget; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import android.content.Context; import android.graphics.Bitmap; diff --git a/mobile/android/base/gfx/NativePanZoomController.java b/mobile/android/base/gfx/NativePanZoomController.java index b85c27b9e66..41f4b3976c3 100644 --- a/mobile/android/base/gfx/NativePanZoomController.java +++ b/mobile/android/base/gfx/NativePanZoomController.java @@ -8,7 +8,7 @@ package org.mozilla.gecko.gfx; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoThread; import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONObject; diff --git a/mobile/android/base/gfx/PanZoomController.java b/mobile/android/base/gfx/PanZoomController.java index 5257c7e38f9..ed4f6b9a2cf 100644 --- a/mobile/android/base/gfx/PanZoomController.java +++ b/mobile/android/base/gfx/PanZoomController.java @@ -6,7 +6,7 @@ package org.mozilla.gecko.gfx; import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import android.graphics.PointF; import android.view.KeyEvent; diff --git a/mobile/android/base/gfx/SubdocumentScrollHelper.java b/mobile/android/base/gfx/SubdocumentScrollHelper.java index 549569aea42..e1f5dd9e0b0 100644 --- a/mobile/android/base/gfx/SubdocumentScrollHelper.java +++ b/mobile/android/base/gfx/SubdocumentScrollHelper.java @@ -7,7 +7,7 @@ package org.mozilla.gecko.gfx; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONException; diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index d41f45810ec..bb85262aeae 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -26,7 +26,7 @@ import org.mozilla.gecko.background.healthreport.HealthReportStorage.Measurement import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; import org.mozilla.gecko.background.healthreport.ProfileInformationCache; -import org.mozilla.gecko.util.EventDispatcher; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; diff --git a/mobile/android/base/home/BrowserSearch.java b/mobile/android/base/home/BrowserSearch.java index 9c735032fa8..2fb3022d492 100644 --- a/mobile/android/base/home/BrowserSearch.java +++ b/mobile/android/base/home/BrowserSearch.java @@ -95,7 +95,6 @@ public class BrowserSearch extends HomeFragment private HomeListView mList; // Client that performs search suggestion queries - @RobocopTarget private volatile SuggestClient mSuggestClient; // List of search engines from gecko @@ -519,8 +518,8 @@ public class BrowserSearch extends HomeFragment // set yet. e.g. Robocop tests might set it directly before search // engines are loaded. if (mSuggestClient == null && !isPrivate) { - mSuggestClient = new SuggestClient(getActivity(), suggestTemplate, - SUGGESTION_TIMEOUT, SUGGESTION_MAX); + setSuggestClient(new SuggestClient(getActivity(), suggestTemplate, + SUGGESTION_TIMEOUT, SUGGESTION_MAX)); } } else { searchEngines.add(engine); @@ -545,6 +544,20 @@ public class BrowserSearch extends HomeFragment filterSuggestions(); } + /** + * Sets the private SuggestClient instance. Should only be called if the suggestClient is + * null (i.e. has not yet been initialized or has been nulled). Non-private access is + * for testing purposes only. + */ + @RobocopTarget + public void setSuggestClient(final SuggestClient client) { + if (mSuggestClient != null) { + throw new IllegalStateException("Can only set the SuggestClient if it has not " + + "yet been initialized!"); + } + mSuggestClient = client; + } + private void showSuggestionsOptIn() { // Return if the ViewStub was already inflated - an inflated ViewStub is removed from the // View hierarchy so a second call to findViewById will return null. diff --git a/mobile/android/base/home/SuggestClient.java b/mobile/android/base/home/SuggestClient.java index 813618e6010..158d982aa1a 100644 --- a/mobile/android/base/home/SuggestClient.java +++ b/mobile/android/base/home/SuggestClient.java @@ -26,7 +26,7 @@ import java.util.ArrayList; /** * Use network-based search suggestions. */ -class SuggestClient { +public class SuggestClient { private static final String LOGTAG = "GeckoSuggestClient"; private static final String USER_AGENT = GeckoAppShell.getGeckoInterface().getDefaultUAString(); @@ -39,7 +39,7 @@ class SuggestClient { // the maximum number of suggestions to return private final int mMaxResults; - // used by robocop for testing; referenced via reflection + // used by robocop for testing private boolean mCheckNetwork; // used to make suggestions appear instantly after opt-in diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index e0a271519d4..21358ac3e3e 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -41,12 +41,10 @@ gujar.sources += [ 'util/ActivityResultHandler.java', 'util/ActivityResultHandlerMap.java', 'util/Clipboard.java', - 'util/EventDispatcher.java', 'util/FloatUtils.java', 'util/GamepadUtils.java', 'util/GeckoBackgroundThread.java', 'util/GeckoEventListener.java', - 'util/GeckoEventResponder.java', 'util/GeckoJarReader.java', 'util/HardwareUtils.java', 'util/INIParser.java', @@ -112,6 +110,7 @@ gbjar.sources += [ 'CustomEditText.java', 'DataReportingNotification.java', 'db/BrowserContract.java', + 'db/BrowserDatabaseHelper.java', 'db/BrowserDB.java', 'db/BrowserProvider.java', 'db/DBUtils.java', @@ -122,9 +121,11 @@ gbjar.sources += [ 'db/PerProfileDatabases.java', 'db/SQLiteBridgeContentProvider.java', 'db/TabsProvider.java', + 'db/TransactionalProvider.java', 'Distribution.java', 'DoorHangerPopup.java', 'EditBookmarkDialog.java', + 'EventDispatcher.java', 'favicons/cache/FaviconCache.java', 'favicons/cache/FaviconCacheElement.java', 'favicons/cache/FaviconsForURL.java', diff --git a/mobile/android/base/prompts/Prompt.java b/mobile/android/base/prompts/Prompt.java index 32e015c1320..658b55aeda8 100644 --- a/mobile/android/base/prompts/Prompt.java +++ b/mobile/android/base/prompts/Prompt.java @@ -5,7 +5,6 @@ package org.mozilla.gecko.prompts; -import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.widget.DateTimePicker; @@ -54,7 +53,6 @@ import android.widget.TimePicker; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener { @@ -66,7 +64,6 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis private AlertDialog mDialog; private final LayoutInflater mInflater; - private ConcurrentLinkedQueue mPromptQueue; private final Context mContext; private PromptCallback mCallback; private String mGuid; @@ -80,16 +77,9 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis private static int mInputPaddingSize; private static int mMinRowSize; - public Prompt(Context context, ConcurrentLinkedQueue queue) { - this(context); - mCallback = null; - mPromptQueue = queue; - } - public Prompt(Context context, PromptCallback callback) { this(context); mCallback = callback; - mPromptQueue = null; } private Prompt(Context context) { @@ -415,10 +405,6 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis aReturn.put("guid", mGuid); } catch(JSONException ex) { } - if (mPromptQueue != null) { - mPromptQueue.offer(aReturn.toString()); - } - // poke the Gecko thread in case it's waiting for new events GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); diff --git a/mobile/android/base/prompts/PromptInput.java b/mobile/android/base/prompts/PromptInput.java index 66c7dfd618b..1d5c7efe7d1 100644 --- a/mobile/android/base/prompts/PromptInput.java +++ b/mobile/android/base/prompts/PromptInput.java @@ -5,7 +5,6 @@ package org.mozilla.gecko.prompts; -import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.widget.AllCapsTextView; import org.mozilla.gecko.widget.DateTimePicker; diff --git a/mobile/android/base/prompts/PromptService.java b/mobile/android/base/prompts/PromptService.java index 3682832990a..c3875953848 100644 --- a/mobile/android/base/prompts/PromptService.java +++ b/mobile/android/base/prompts/PromptService.java @@ -5,27 +5,28 @@ package org.mozilla.gecko.prompts; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONObject; +import org.json.JSONException; import android.content.Context; +import android.util.Log; import java.util.concurrent.ConcurrentLinkedQueue; -public class PromptService implements GeckoEventResponder { +public class PromptService implements GeckoEventListener { private static final String LOGTAG = "GeckoPromptService"; - private final ConcurrentLinkedQueue mPromptQueue; private final Context mContext; public PromptService(Context context) { GeckoAppShell.getEventDispatcher().registerEventListener("Prompt:Show", this); GeckoAppShell.getEventDispatcher().registerEventListener("Prompt:ShowTop", this); - mPromptQueue = new ConcurrentLinkedQueue(); mContext = context; } @@ -41,11 +42,7 @@ public class PromptService implements GeckoEventResponder { @Override public void run() { Prompt p; - if (callback != null) { - p = new Prompt(mContext, callback); - } else { - p = new Prompt(mContext, mPromptQueue); - } + p = new Prompt(mContext, callback); p.show(aTitle, aText, aMenuList, aMultipleSelection); } }); @@ -58,35 +55,18 @@ public class PromptService implements GeckoEventResponder { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { - boolean isAsync = message.optBoolean("async"); Prompt p; - if (isAsync) { - p = new Prompt(mContext, new Prompt.PromptCallback() { - public void onPromptFinished(String jsonResult) { - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", jsonResult)); + p = new Prompt(mContext, new Prompt.PromptCallback() { + public void onPromptFinished(String jsonResult) { + try { + EventDispatcher.sendResponse(message, new JSONObject(jsonResult)); + } catch(JSONException ex) { + Log.i(LOGTAG, "Error building json response", ex); } - }); - } else { - p = new Prompt(mContext, mPromptQueue); - } + } + }); p.show(message); } }); } - - // GeckoEventResponder implementation - @Override - public String getResponse(final JSONObject origMessage) { - if (origMessage.optBoolean("async")) { - return ""; - } - - // we only handle one kind of message in handleMessage, and this is the - // response we provide for that message - String result; - while (null == (result = mPromptQueue.poll())) { - GeckoAppShell.processNextNativeEvent(true); - } - return result; - } } diff --git a/mobile/android/base/tests/UITest.java b/mobile/android/base/tests/UITest.java index 507851a2ff1..f6140d0159c 100644 --- a/mobile/android/base/tests/UITest.java +++ b/mobile/android/base/tests/UITest.java @@ -58,6 +58,7 @@ abstract class UITest extends ActivityInstrumentationTestCase2 private String mBaseIpUrl; protected AboutHomeComponent mAboutHome; + protected AppMenuComponent mAppMenu; protected ToolbarComponent mToolbar; static { @@ -120,6 +121,7 @@ abstract class UITest extends ActivityInstrumentationTestCase2 private void initComponents() { mAboutHome = new AboutHomeComponent(this); + mAppMenu = new AppMenuComponent(this); mToolbar = new ToolbarComponent(this); } @@ -163,6 +165,9 @@ abstract class UITest extends ActivityInstrumentationTestCase2 case ABOUTHOME: return mAboutHome; + case APPMENU: + return mAppMenu; + case TOOLBAR: return mToolbar; diff --git a/mobile/android/base/tests/UITestContext.java b/mobile/android/base/tests/UITestContext.java index 47a40f415f9..431b510c41e 100644 --- a/mobile/android/base/tests/UITestContext.java +++ b/mobile/android/base/tests/UITestContext.java @@ -21,6 +21,7 @@ public interface UITestContext { public static enum ComponentType { ABOUTHOME, + APPMENU, TOOLBAR } diff --git a/mobile/android/base/tests/components/AppMenuComponent.java b/mobile/android/base/tests/components/AppMenuComponent.java new file mode 100644 index 00000000000..36828314bc8 --- /dev/null +++ b/mobile/android/base/tests/components/AppMenuComponent.java @@ -0,0 +1,143 @@ +/* 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.tests.components; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import org.mozilla.gecko.menu.MenuItemActionBar; +import org.mozilla.gecko.menu.MenuItemDefault; +import org.mozilla.gecko.tests.helpers.*; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.util.HardwareUtils; +import org.mozilla.gecko.R; + +import com.jayway.android.robotium.solo.Condition; +import com.jayway.android.robotium.solo.RobotiumUtils; +import com.jayway.android.robotium.solo.Solo; + +import android.view.View; +import java.util.List; + +/** + * A class representing any interactions that take place on the app menu. + */ +public class AppMenuComponent extends BaseComponent { + public enum MenuItem { + FORWARD(R.string.forward), + NEW_TAB(R.string.new_tab); + + private final int resourceID; + private String stringResource; + + MenuItem(final int resourceID) { + this.resourceID = resourceID; + } + + public String getString(final Solo solo) { + if (stringResource == null) { + stringResource = solo.getString(resourceID); + } + + return stringResource; + } + }; + + public AppMenuComponent(final UITestContext testContext) { + super(testContext); + } + + private void assertMenuIsNotOpen() { + assertFalse("Menu is not open", isMenuOpen()); + } + + private View getOverflowMenuButtonView() { + return mSolo.getView(R.id.menu); + } + + /** + * Try to find a MenuItemActionBar/MenuItemDefault with the given text set as contentDescription / text. + * + * Will return null when the Android legacy menu is in use. + * + * This method is dependent on not having two views with equivalent contentDescription / text. + */ + private View findAppMenuItemView(String text) { + final List views = mSolo.getViews(); + + final List menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views); + for (MenuItemActionBar menuItem : menuItemActionBarList) { + if (menuItem.getContentDescription().equals(text)) { + return menuItem; + } + } + + final List menuItemDefaultList = RobotiumUtils.filterViews(MenuItemDefault.class, views); + for (MenuItemDefault menuItem : menuItemDefaultList) { + if (menuItem.getText().equals(text)) { + return menuItem; + } + } + + return null; + } + + public void pressMenuItem(MenuItem menuItem) { + openAppMenu(); + + final String text = menuItem.getString(mSolo); + final View menuItemView = findAppMenuItemView(text); + + if (menuItemView != null) { + assertTrue("The menu item is enabled", menuItemView.isEnabled()); + assertEquals("The menu item is visible", View.VISIBLE, menuItemView.getVisibility()); + + mSolo.clickOnView(menuItemView); + } else { + // We could not find a view representing this menu item: Let's let Robotium try to + // locate and click it in the legacy Android menu (devices with Android 2.x). + // + // Even though we already opened the menu to see if we can locate the menu item, + // Robotium will also try to open the menu if it doesn't find an open dialog (Does + // not happen in this case). + mSolo.clickOnMenuItem(text, true); + } + } + + private void openAppMenu() { + assertMenuIsNotOpen(); + + if (HardwareUtils.hasMenuButton()) { + mSolo.sendKey(Solo.MENU); + } else { + pressOverflowMenuButton(); + } + + waitForMenuOpen(); + } + + private void pressOverflowMenuButton() { + final View overflowMenuButton = getOverflowMenuButtonView(); + + assertTrue("The overflow menu button is enabled", overflowMenuButton.isEnabled()); + assertEquals("The overflow menu button is visible", View.VISIBLE, overflowMenuButton.getVisibility()); + + mSolo.clickOnView(overflowMenuButton, true); + } + + private boolean isMenuOpen() { + // The presence of the "New tab" menu item is our best guess about whether + // the menu is open or not. + return mSolo.searchText(MenuItem.NEW_TAB.getString(mSolo)); + } + + private void waitForMenuOpen() { + WaitHelper.waitFor("menu to open", new Condition() { + @Override + public boolean isSatisfied() { + return isMenuOpen(); + } + }); + } +} diff --git a/mobile/android/base/tests/helpers/NavigationHelper.java b/mobile/android/base/tests/helpers/NavigationHelper.java index c8d9b192044..0db3b6119f6 100644 --- a/mobile/android/base/tests/helpers/NavigationHelper.java +++ b/mobile/android/base/tests/helpers/NavigationHelper.java @@ -6,6 +6,7 @@ package org.mozilla.gecko.tests.helpers; import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; +import org.mozilla.gecko.tests.components.AppMenuComponent; import org.mozilla.gecko.tests.components.ToolbarComponent; import org.mozilla.gecko.tests.UITestContext; import org.mozilla.gecko.tests.UITestContext.ComponentType; @@ -22,12 +23,14 @@ final public class NavigationHelper { private static UITestContext sContext; private static Solo sSolo; + private static AppMenuComponent sAppMenu; private static ToolbarComponent sToolbar; protected static void init(final UITestContext context) { sContext = context; sSolo = context.getSolo(); + sAppMenu = (AppMenuComponent) context.getComponent(ComponentType.APPMENU); sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR); } @@ -81,8 +84,7 @@ final public class NavigationHelper { WaitHelper.waitForPageLoad(new Runnable() { @Override public void run() { - // TODO: Press forward with APPMENU component - throw new UnsupportedOperationException("Not yet implemented."); + sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.FORWARD); } }); } diff --git a/mobile/android/base/tests/roboextender/robocop_home_banner.html b/mobile/android/base/tests/roboextender/robocop_home_banner.html index 9ddaff91bfd..9b95e81830c 100644 --- a/mobile/android/base/tests/roboextender/robocop_home_banner.html +++ b/mobile/android/base/tests/roboextender/robocop_home_banner.html @@ -4,13 +4,10 @@ diff --git a/toolkit/content/widgets/findbar.xml b/toolkit/content/widgets/findbar.xml index 1812a507f84..72e54e18d0d 100644 --- a/toolkit/content/widgets/findbar.xml +++ b/toolkit/content/widgets/findbar.xml @@ -110,6 +110,13 @@ findbar.browser.finder.enableSelection(); ]]> +#ifdef XP_MACOSX + +#endif + +#ifdef XP_MACOSX + + + + + + + + +#endif +