diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index d8c010a1410d..1b98184173b8 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -12,13 +12,13 @@ - + - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index d3c18c3d7024..db64220d842e 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -11,10 +11,10 @@ - + - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index d8c010a1410d..1b98184173b8 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -12,13 +12,13 @@ - + - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 276defb6962a..f19f1324509f 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "a4d9c73e176e7dfc9a32d362f0f5e5cb5c21e323", + "revision": "cea79abbb7a97c0bd67051087bcdf40d25611930", "repo_path": "/integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index c8d7a7031cbb..de2bdbd7ff06 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -11,12 +11,12 @@ - + - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index d37ae23616c1..d6436870f58b 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -10,7 +10,7 @@ - + diff --git a/b2g/config/inari/sources.xml b/b2g/config/inari/sources.xml index 8ac9fafdde4b..24ef56030566 100644 --- a/b2g/config/inari/sources.xml +++ b/b2g/config/inari/sources.xml @@ -12,12 +12,12 @@ - + - + diff --git a/b2g/config/leo/sources.xml b/b2g/config/leo/sources.xml index ee606c90c835..423f36da597b 100644 --- a/b2g/config/leo/sources.xml +++ b/b2g/config/leo/sources.xml @@ -11,12 +11,12 @@ - + - + diff --git a/b2g/config/mako/sources.xml b/b2g/config/mako/sources.xml index 260f35b8163b..bdae239e0e40 100644 --- a/b2g/config/mako/sources.xml +++ b/b2g/config/mako/sources.xml @@ -11,10 +11,10 @@ - + - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 389e5d1ec6d8..e04570049877 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -11,12 +11,12 @@ - + - + diff --git a/browser/base/content/browser-fxaccounts.js b/browser/base/content/browser-fxaccounts.js index 95df6c6b5d7d..b3a170cb72b4 100644 --- a/browser/base/content/browser-fxaccounts.js +++ b/browser/base/content/browser-fxaccounts.js @@ -213,7 +213,6 @@ let gFxAccounts = { }, openSignInAgainPage: function () { - // FIXME: This should actually show the pre-filled username version of about:accounts? - switchToTabHavingURI("about:accounts?signin=true", true); + switchToTabHavingURI("about:accounts?action=reauth", true); } }; diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index adafd6a68133..7533c5abb122 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -173,6 +173,7 @@ toolbar[customizing] > .overflow-button { } %ifdef CAN_DRAW_IN_TITLEBAR +#main-window:not([chromemargin]) > #titlebar, #main-window[inFullscreen] > #titlebar, #main-window[inFullscreen] .titlebar-placeholder, #main-window:not([tabsintitlebar]) .titlebar-placeholder { diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 088b61e91703..ed5691b92ecc 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -4481,6 +4481,9 @@ var TabsInTitlebar = { // Try to avoid reflows in this code by calculating dimensions first and // then later set the properties affecting layout together in a batch. + // Get the full height of the tabs toolbar: + let tabsToolbar = $("TabsToolbar"); + let fullTabsHeight = rect(tabsToolbar).height; // Buttons first: let captionButtonsBoxWidth = rect($("titlebar-buttonbox")).width; #ifdef XP_MACOSX @@ -4495,11 +4498,9 @@ var TabsInTitlebar = { let menuHeight = rect(menubar).height; let menuStyles = window.getComputedStyle(menubar); let fullMenuHeight = verticalMargins(menuStyles) + menuHeight; -#endif - // Get the full height of the tabs toolbar: - let tabsToolbar = $("TabsToolbar"); let tabsStyles = window.getComputedStyle(tabsToolbar); - let fullTabsHeight = rect(tabsToolbar).height + verticalMargins(tabsStyles); + fullTabsHeight += verticalMargins(tabsStyles); +#endif // If the navbar overlaps the tabbar using negative margins, we need to take those into // account so we don't overlap it @@ -4509,16 +4510,6 @@ var TabsInTitlebar = { // And get the height of what's in the titlebar: let titlebarContentHeight = rect(titlebarContent).height; - // Padding surrounds the tab-view-deck when we are in customization mode, - // so take that into account: - let areCustomizing = document.documentElement.hasAttribute("customizing") || - document.documentElement.hasAttribute("customize-exiting"); - let customizePadding = 0; - if (areCustomizing) { - let deckStyle = window.getComputedStyle($("tab-view-deck")); - customizePadding = parseFloat(deckStyle.paddingTop); - } - // Begin setting CSS properties which will cause a reflow // If the menubar is around (menuHeight is non-zero), try to adjust @@ -4551,10 +4542,6 @@ var TabsInTitlebar = { // Next, we calculate how much we need to stretch the titlebar down to // go all the way to the bottom of the tab strip, if necessary. let tabAndMenuHeight = fullTabsHeight + fullMenuHeight; - // Oh, and don't forget customization mode: - if (areCustomizing) { - tabAndMenuHeight += customizePadding; - } if (tabAndMenuHeight > titlebarContentHeight) { // We need to increase the titlebar content's outer height (ie including margins) @@ -4566,12 +4553,6 @@ var TabsInTitlebar = { // On non-OSX, we can just use bottom margin: #ifndef XP_MACOSX titlebarContent.style.marginBottom = extraMargin + "px"; -#else - // Otherwise, center the content. This means taking the titlebar's - // padding into account: - let halfMargin = (extraMargin - titlebarPadding) / 2; - titlebarContent.style.marginTop = halfMargin + "px"; - titlebarContent.style.marginBottom = (titlebarPadding + halfMargin) + "px"; #endif titlebarContentHeight += extraMargin; } @@ -4606,6 +4587,7 @@ var TabsInTitlebar = { updateTitlebarDisplay(); // Reset the margins and padding that might have been modified: + titlebarContent.style.marginTop = ""; titlebarContent.style.marginBottom = ""; titlebar.style.marginBottom = ""; menubar.style.paddingBottom = ""; @@ -4630,16 +4612,37 @@ var TabsInTitlebar = { #ifdef CAN_DRAW_IN_TITLEBAR function updateTitlebarDisplay() { - document.getElementById("titlebar").hidden = !TabsInTitlebar.enabled; + +#ifdef XP_MACOSX + // OS X and the other platforms differ enough to necessitate this kind of + // special-casing. Like the other platforms where we CAN_DRAW_IN_TITLEBAR, + // we draw in the OS X titlebar when putting the tabs up there. However, OS X + // also draws in the titlebar when a lightweight theme is applied, regardless + // of whether or not the tabs are drawn in the titlebar. + if (TabsInTitlebar.enabled) { + document.documentElement.setAttribute("chromemargin-nonlwtheme", "0,-1,-1,-1"); + document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1"); + document.documentElement.removeAttribute("drawtitle"); + } else { + // We set chromemargin-nonlwtheme to "" instead of removing it as a way of + // making sure that LightweightThemeConsumer doesn't take it upon itself to + // detect this value again if and when we do a lwtheme state change. + document.documentElement.setAttribute("chromemargin-nonlwtheme", ""); + let isCustomizing = document.documentElement.hasAttribute("customizing"); + let hasLWTheme = document.documentElement.hasAttribute("lwtheme"); + if (!hasLWTheme || isCustomizing) { + document.documentElement.removeAttribute("chromemargin"); + } + document.documentElement.setAttribute("drawtitle", "true"); + } + +#else if (TabsInTitlebar.enabled) -#ifdef XP_WIN document.documentElement.setAttribute("chromemargin", "0,2,2,2"); -#else - document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1"); -#endif else document.documentElement.removeAttribute("chromemargin"); +#endif } #endif diff --git a/browser/base/content/test/chrome/test_aboutCrashed.xul b/browser/base/content/test/chrome/test_aboutCrashed.xul index f7512e94b23a..7a68076f185e 100644 --- a/browser/base/content/test/chrome/test_aboutCrashed.xul +++ b/browser/base/content/test/chrome/test_aboutCrashed.xul @@ -65,7 +65,7 @@ frame1.docShell.chromeEventHandler.removeAttribute("crashedPageTitle"); SimpleTest.is(frame1.contentDocument.documentURI, - "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/1&c=UTF-8&d=pageTitle&f=regular", + "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/1&c=UTF-8&f=regular&d=pageTitle", "Correct about:tabcrashed displayed for page with title."); errorPageReady = waitForErrorPage(frame2); @@ -74,7 +74,7 @@ yield errorPageReady; SimpleTest.is(frame2.contentDocument.documentURI, - "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/2&c=UTF-8&d=%20&f=regular", + "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/2&c=UTF-8&f=regular&d=%20", "Correct about:tabcrashed displayed for page with no title."); SimpleTest.finish(); diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 5a1323dec682..90ec8a50aa8f 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -313,7 +313,8 @@ skip-if = true # disabled until the tree view is added [browser_tab_dragdrop.js] [browser_tab_dragdrop2.js] [browser_tabbar_big_widgets.js] -skip-if = os == "linux" # No tabs in titlebar on linux +skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux + # Disabled on OS X because of bug 967917 [browser_tabfocus.js] [browser_tabopen_reflows.js] [browser_tabs_isActive.js] diff --git a/browser/components/customizableui/src/CustomizableWidgets.jsm b/browser/components/customizableui/src/CustomizableWidgets.jsm index 01726c543e0b..0341c528dee9 100644 --- a/browser/components/customizableui/src/CustomizableWidgets.jsm +++ b/browser/components/customizableui/src/CustomizableWidgets.jsm @@ -123,9 +123,6 @@ const CustomizableWidgets = [{ item.setAttribute("label", title || uri); item.setAttribute("targetURI", uri); item.setAttribute("class", "subviewbutton"); - item.addEventListener("command", function (aEvent) { - onHistoryVisit(uri, aEvent, item); - }); item.addEventListener("click", function (aEvent) { onHistoryVisit(uri, aEvent, item); }); diff --git a/browser/devtools/markupview/test/browser.ini b/browser/devtools/markupview/test/browser.ini index 1ac25dc5b7e8..f7e35ffed831 100644 --- a/browser/devtools/markupview/test/browser.ini +++ b/browser/devtools/markupview/test/browser.ini @@ -13,6 +13,8 @@ support-files = # Bug 916763 - too many intermittent failures skip-if = true [browser_inspector_markup_edit.js] +# Bug 904953 - too many intermittent failures on Linux +skip-if = os == "linux" [browser_inspector_markup_edit_outerhtml.js] [browser_inspector_markup_edit_outerhtml2.js] [browser_inspector_markup_mutation.js] diff --git a/browser/metro/base/content/browser-ui.js b/browser/metro/base/content/browser-ui.js index d72dfa0d9e0d..282651bf928c 100644 --- a/browser/metro/base/content/browser-ui.js +++ b/browser/metro/base/content/browser-ui.js @@ -1143,8 +1143,7 @@ var BrowserUI = { confirmSanitizeDialog: function () { let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); let title = bundle.GetStringFromName("clearPrivateData.title2"); - let options = bundle.GetStringFromName("optionsCharm"); - let message = bundle.GetStringFromName("clearPrivateData.message2").replace("#1", options); + let message = bundle.GetStringFromName("clearPrivateData.message3"); let clearbutton = bundle.GetStringFromName("clearPrivateData.clearButton"); let prefsClearButton = document.getElementById("prefs-clear-data"); diff --git a/browser/metro/base/tests/mochitest/browser_ui_telemetry.js b/browser/metro/base/tests/mochitest/browser_ui_telemetry.js index f636ca99b7c6..7c5c3965d1a4 100644 --- a/browser/metro/base/tests/mochitest/browser_ui_telemetry.js +++ b/browser/metro/base/tests/mochitest/browser_ui_telemetry.js @@ -9,16 +9,18 @@ function test() { runTests(); } -function getSimpleMeasurementsFromTelemetryPing() { +function getTelemetryPayload() { return Cu.import("resource://gre/modules/TelemetryPing.jsm", {}). - TelemetryPing.getPayload().simpleMeasurements; + TelemetryPing.getPayload(); } gTests.push({ desc: "Test browser-ui telemetry", run: function testBrowserUITelemetry() { // startup should have registered simple measures function - let simpleMeasurements = getSimpleMeasurementsFromTelemetryPing(); + is(getTelemetryPayload().info.appName, "MetroFirefox"); + + let simpleMeasurements = getTelemetryPayload().simpleMeasurements; ok(simpleMeasurements, "simpleMeasurements are truthy"); ok(simpleMeasurements.UITelemetry["metro-ui"]["window-width"], "window-width measurement was captured"); ok(simpleMeasurements.UITelemetry["metro-ui"]["window-height"], "window-height measurement was captured"); diff --git a/browser/metro/locales/en-US/chrome/browser.properties b/browser/metro/locales/en-US/chrome/browser.properties index 029055229293..4f1d1cd0e122 100644 --- a/browser/metro/locales/en-US/chrome/browser.properties +++ b/browser/metro/locales/en-US/chrome/browser.properties @@ -39,8 +39,8 @@ contextAppbar2.clear=Clear selection # Clear private data clearPrivateData.clearButton=Clear clearPrivateData.title2=Clear private data -# LOCALIZATION NOTE (clearPrivateData.message2): #1 is optionsCharm -clearPrivateData.message2=This will permanently delete the private data you have selected in #1 +# LOCALIZATION NOTE (clearPrivateData.message3): "Options" is the optionsCharm. +clearPrivateData.message3=This will permanently delete the private data you have selected in "Options". # Settings Charms aboutCharm1=About diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index 96b7a5c89090..da4b4c219116 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -73,6 +73,7 @@ browser.jar: skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) skin/classic/browser/customizableui/customizeFavicon.ico (../shared/customizableui/customizeFavicon.ico) + skin/classic/browser/customizableui/menuPanel-customizeFinish.png (../shared/customizableui/menuPanel-customizeFinish.png) * skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) skin/classic/browser/customizableui/subView-arrow-back-inverted.png (../shared/customizableui/subView-arrow-back-inverted.png) skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css) diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 0f434c2d08c7..f0ae131cdf89 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -59,7 +59,7 @@ } } -#main-window[chromehidden~="toolbar"] > #titlebar { +#main-window[chromehidden~="toolbar"]:not(:-moz-lwtheme) > #titlebar { padding-top: 22px; } @@ -2669,7 +2669,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { box-shadow: @focusRingShadow@; } -#titlebar { +#main-window:not(:-moz-lwtheme) > #titlebar { padding-top: @spaceAboveTabbar@; min-height: @tabHeight@; } @@ -4104,13 +4104,26 @@ window > chatbox { %include ../shared/customizableui/customizeMode.inc.css -#main-window[customize-entered] #titlebar { +#main-window[customize-entered] > #titlebar { padding-top: 0; } -#main-window[tabsintitlebar][customize-entered] #titlebar-content { - margin-bottom: 0px !important; - margin-top: 11px !important; +#main-window[tabsintitlebar]:not([customizing]):not(:-moz-lwtheme) > #titlebar > #titlebar-content, +#main-window[tabsintitlebar][customize-entering] > #titlebar > #titlebar-content, +#main-window[tabsintitlebar][customize-exiting] > #titlebar > #titlebar-content { + margin-top: 2px; + margin-bottom: 11px; +} + +#main-window[tabsintitlebar][customize-entered] > #titlebar > #titlebar-content, +#main-window:not([tabsintitlebar]):not(:-moz-lwtheme) > #titlebar > #titlebar-content { + margin-top: 11px; + margin-bottom: 0px; +} + +#main-window[tabsintitlebar]:-moz-lwtheme > #titlebar > #titlebar-content { + margin-top: 11px; + margin-bottom: 11px; } #main-window[customize-entered] #tab-view-deck { @@ -4135,8 +4148,11 @@ window > chatbox { border-bottom-width: 0; } -#main-window[customize-entered] #TabsToolbar { +#main-window[tabsintitlebar][customize-entered] #TabsToolbar { margin-top: 9px; +} + +#main-window[customize-entered] #TabsToolbar { background-clip: padding-box; border-right: 3px solid transparent; border-left: 3px solid transparent; diff --git a/browser/themes/osx/customizableui/panelUIOverlay.css b/browser/themes/osx/customizableui/panelUIOverlay.css index 3b93f5cff889..a1f2915d872b 100644 --- a/browser/themes/osx/customizableui/panelUIOverlay.css +++ b/browser/themes/osx/customizableui/panelUIOverlay.css @@ -15,6 +15,10 @@ list-style-image: url(chrome://browser/skin/menuPanel-customize@2x.png); } + #main-window[customize-entered] #PanelUI-customize { + list-style-image: url(chrome://browser/skin/customizableui/menuPanel-customizeFinish@2x.png); + } + #PanelUI-help { list-style-image: url(chrome://browser/skin/menuPanel-help@2x.png); } diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index 411af517906a..209d78c4102b 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -120,6 +120,8 @@ browser.jar: skin/classic/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) + skin/classic/browser/customizableui/menuPanel-customizeFinish.png (../shared/customizableui/menuPanel-customizeFinish.png) + skin/classic/browser/customizableui/menuPanel-customizeFinish@2x.png (../shared/customizableui/menuPanel-customizeFinish@2x.png) skin/classic/browser/customizableui/subView-arrow-back-inverted.png (../shared/customizableui/subView-arrow-back-inverted.png) skin/classic/browser/customizableui/subView-arrow-back-inverted@2x.png (../shared/customizableui/subView-arrow-back-inverted@2x.png) * skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) diff --git a/browser/themes/shared/customizableui/menuPanel-customizeFinish.png b/browser/themes/shared/customizableui/menuPanel-customizeFinish.png new file mode 100644 index 000000000000..0251ed747087 Binary files /dev/null and b/browser/themes/shared/customizableui/menuPanel-customizeFinish.png differ diff --git a/browser/themes/shared/customizableui/menuPanel-customizeFinish@2x.png b/browser/themes/shared/customizableui/menuPanel-customizeFinish@2x.png new file mode 100644 index 000000000000..593e1df43d30 Binary files /dev/null and b/browser/themes/shared/customizableui/menuPanel-customizeFinish@2x.png differ diff --git a/browser/themes/shared/customizableui/panelUIOverlay.inc.css b/browser/themes/shared/customizableui/panelUIOverlay.inc.css index 5c11f54fc688..63f4ba6ac888 100644 --- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css +++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css @@ -322,6 +322,10 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { list-style-image: url(chrome://browser/skin/menuPanel-customize.png); } +#customization-panelHolder #PanelUI-customize { + list-style-image: url(chrome://browser/skin/customizableui/menuPanel-customizeFinish.png); +} + #PanelUI-help { list-style-image: url(chrome://browser/skin/menuPanel-help.png); } @@ -378,16 +382,15 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { background-color: #ad3434; } -#main-window[customize-entered] #PanelUI-customize { +#customization-panelHolder #PanelUI-customize { color: white; - background-image: linear-gradient(rgb(41,123,204), rgb(40,133,203)); - box-shadow: inset 0 1px 1px rgba(0,0,0,0.5), 0 2px rgba(255,255,255,0.2); - text-shadow: 0 1px 0 rgba(0,0,0,0.4); + background-color: rgb(116,191,67); + text-shadow: none; } -#main-window[customize-entered] #PanelUI-customize:hover, -#main-window[customize-entered] #PanelUI-customize:hover:active { - background-image: linear-gradient(rgb(38,115,191), rgb(38,125,191)); +#customization-panelHolder #PanelUI-customize:hover, +#customization-panelHolder #PanelUI-customize:hover:active { + background-color: rgb(105,173,61); } #customization-palette .toolbarbutton-multiline-text, diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 5e7c4d4f196a..2da7cf889eee 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -92,6 +92,7 @@ browser.jar: skin/classic/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) + skin/classic/browser/customizableui/menuPanel-customizeFinish.png (../shared/customizableui/menuPanel-customizeFinish.png) * skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) skin/classic/browser/customizableui/subView-arrow-back-inverted.png (../shared/customizableui/subView-arrow-back-inverted.png) * skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css) @@ -404,6 +405,7 @@ browser.jar: skin/classic/aero/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/aero/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/aero/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) + skin/classic/aero/browser/customizableui/menuPanel-customizeFinish.png (../shared/customizableui/menuPanel-customizeFinish.png) * skin/classic/aero/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) skin/classic/aero/browser/customizableui/subView-arrow-back-inverted.png (../shared/customizableui/subView-arrow-back-inverted.png) * skin/classic/aero/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay-aero.css) diff --git a/content/media/AudioCompactor.cpp b/content/media/AudioCompactor.cpp new file mode 100644 index 000000000000..1164f862aa46 --- /dev/null +++ b/content/media/AudioCompactor.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "AudioCompactor.h" +#if defined(MOZ_MEMORY) +# include "mozmemory.h" +#endif + +namespace mozilla { + +static size_t +MallocGoodSize(size_t aSize) +{ +# if defined(MOZ_MEMORY) + return malloc_good_size(aSize); +# else + return aSize; +# endif +} + +static size_t +TooMuchSlop(size_t aSize, size_t aAllocSize, size_t aMaxSlop) +{ + // If the allocated size is less then our target size, then we + // are chunking. This means it will be completely filled with + // zero slop. + size_t slop = (aAllocSize > aSize) ? (aAllocSize - aSize) : 0; + return slop > aMaxSlop; +} + +uint32_t +AudioCompactor::GetChunkSamples(uint32_t aFrames, uint32_t aChannels, + size_t aMaxSlop) +{ + size_t size = AudioDataSize(aFrames, aChannels); + size_t chunkSize = MallocGoodSize(size); + + // Reduce the chunk size until we meet our slop goal or the chunk + // approaches an unreasonably small size. + while (chunkSize > 64 && TooMuchSlop(size, chunkSize, aMaxSlop)) { + chunkSize = MallocGoodSize(chunkSize / 2); + } + + // Calculate the number of samples based on expected malloc size + // in order to allow as many frames as possible to be packed. + return chunkSize / sizeof(AudioDataValue); +} + +uint32_t +AudioCompactor::NativeCopy::operator()(AudioDataValue *aBuffer, size_t aSamples) +{ + NS_ASSERTION(aBuffer, "cannot copy to null buffer pointer"); + NS_ASSERTION(aSamples, "cannot copy zero values"); + + size_t bufferBytes = aSamples * sizeof(AudioDataValue); + size_t maxBytes = std::min(bufferBytes, mSourceBytes - mNextByte); + uint32_t frames = maxBytes / BytesPerFrame(mChannels); + size_t bytes = frames * BytesPerFrame(mChannels); + + NS_ASSERTION((mNextByte + bytes) <= mSourceBytes, + "tried to copy beyond source buffer"); + NS_ASSERTION(bytes <= bufferBytes, "tried to copy beyond destination buffer"); + + memcpy(aBuffer, mSource + mNextByte, bytes); + + mNextByte += bytes; + return frames; +} + +} // namespace mozilla diff --git a/content/media/AudioCompactor.h b/content/media/AudioCompactor.h new file mode 100644 index 000000000000..99e89508a08b --- /dev/null +++ b/content/media/AudioCompactor.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#if !defined(AudioCompactor_h) +#define AudioCompactor_h + +#include "MediaQueue.h" +#include "MediaData.h" +#include "VideoUtils.h" + +namespace mozilla { + +class AudioCompactor +{ +public: + AudioCompactor(MediaQueue& aQueue) + : mQueue(aQueue) + { } + + // Push audio data into the underlying queue with minimal heap allocation + // slop. This method is responsible for allocating AudioDataValue[] buffers. + // The caller must provide a functor to copy the data into the buffers. The + // functor must provide the following signature: + // + // uint32_t operator()(AudioDataValue *aBuffer, size_t aSamples); + // + // The functor must copy as many complete frames as possible to the provided + // buffer given its length (in AudioDataValue elements). The number of frames + // copied must be returned. This copy functor must support being called + // multiple times in order to copy the audio data fully. The copy functor + // must copy full frames as partial frames will be ignored. + template + bool Push(int64_t aOffset, int64_t aTime, int32_t aSampleRate, + uint32_t aFrames, uint32_t aChannels, CopyFunc aCopyFunc) + { + // If we are losing more than a reasonable amount to padding, try to chunk + // the data. + size_t maxSlop = AudioDataSize(aFrames, aChannels) / MAX_SLOP_DIVISOR; + + while (aFrames > 0) { + uint32_t samples = GetChunkSamples(aFrames, aChannels, maxSlop); + nsAutoArrayPtr buffer(new AudioDataValue[samples]); + + // Copy audio data to buffer using caller-provided functor. + uint32_t framesCopied = aCopyFunc(buffer, samples); + + NS_ASSERTION(framesCopied <= aFrames, "functor copied too many frames"); + + CheckedInt64 duration = FramesToUsecs(framesCopied, aSampleRate); + if (!duration.isValid()) { + return false; + } + + mQueue.Push(new AudioData(aOffset, + aTime, + duration.value(), + framesCopied, + buffer.forget(), + aChannels)); + + // Remove the frames we just pushed into the queue and loop if there is + // more to be done. + aTime += duration.value(); + aFrames -= framesCopied; + + // NOTE: No need to update aOffset as its only an approximation anyway. + } + + return true; + } + + // Copy functor suitable for copying audio samples already in the + // AudioDataValue format/layout expected by AudioStream on this platform. + class NativeCopy + { + public: + NativeCopy(const uint8_t* aSource, size_t aSourceBytes, + uint32_t aChannels) + : mSource(aSource) + , mSourceBytes(aSourceBytes) + , mChannels(aChannels) + , mNextByte(0) + { } + + uint32_t operator()(AudioDataValue *aBuffer, size_t aSamples); + + private: + const uint8_t* const mSource; + const size_t mSourceBytes; + const uint32_t mChannels; + size_t mNextByte; + }; + + // Allow 12.5% slop before chunking kicks in. Public so that the gtest can + // access it. + static const size_t MAX_SLOP_DIVISOR = 8; + +private: + // Compute the number of AudioDataValue samples that will be fit the most + // frames while keeping heap allocation slop less than the given threshold. + static uint32_t + GetChunkSamples(uint32_t aFrames, uint32_t aChannels, size_t aMaxSlop); + + static size_t BytesPerFrame(uint32_t aChannels) + { + return sizeof(AudioDataValue) * aChannels; + } + + static size_t AudioDataSize(uint32_t aFrames, uint32_t aChannels) + { + return aFrames * BytesPerFrame(aChannels); + } + + MediaQueue &mQueue; +}; + +} // namespace mozilla + +#endif // AudioCompactor_h diff --git a/content/media/MediaDecoderReader.cpp b/content/media/MediaDecoderReader.cpp index 0a6200dcc28f..6856bc0004e8 100644 --- a/content/media/MediaDecoderReader.cpp +++ b/content/media/MediaDecoderReader.cpp @@ -45,7 +45,8 @@ void* MediaDecoderReader::VideoQueueMemoryFunctor::operator()(void* anObject) { } MediaDecoderReader::MediaDecoderReader(AbstractMediaDecoder* aDecoder) - : mDecoder(aDecoder), + : mAudioCompactor(mAudioQueue), + mDecoder(aDecoder), mIgnoreAudioOutputFormat(false) { MOZ_COUNT_CTOR(MediaDecoderReader); @@ -280,4 +281,3 @@ MediaDecoderReader::GetBuffered(mozilla::dom::TimeRanges* aBuffered, } } // namespace mozilla - diff --git a/content/media/MediaDecoderReader.h b/content/media/MediaDecoderReader.h index 0176048ac49a..957c5a07760e 100644 --- a/content/media/MediaDecoderReader.h +++ b/content/media/MediaDecoderReader.h @@ -10,6 +10,7 @@ #include "MediaInfo.h" #include "MediaData.h" #include "MediaQueue.h" +#include "AudioCompactor.h" namespace mozilla { @@ -105,6 +106,12 @@ protected: // the decoder, state machine, and main threads. MediaQueue mVideoQueue; + // An adapter to the audio queue which first copies data to buffers with + // minimal allocation slop and then pushes them to the queue. This is + // useful for decoders working with formats that give awkward numbers of + // frames such as mp3. + AudioCompactor mAudioCompactor; + public: // Populates aBuffered with the time ranges which are buffered. aStartTime // must be the presentation time of the first frame in the media, e.g. diff --git a/content/media/MediaDecoderStateMachine.cpp b/content/media/MediaDecoderStateMachine.cpp index dff3b9481bcb..8f4386a8edb6 100644 --- a/content/media/MediaDecoderStateMachine.cpp +++ b/content/media/MediaDecoderStateMachine.cpp @@ -45,6 +45,13 @@ extern PRLogModuleInfo* gMediaDecoderLog; #define DECODER_LOG(type, msg) #endif +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount() and conflicts with MediaDecoderStateMachine::GetCurrentTime +// implementation. With unified builds, putting this in headers is not enough. +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + // Wait this number of seconds when buffering, then leave and play // as best as we can if the required amount of data hasn't been // retrieved. diff --git a/content/media/MediaDecoderStateMachine.h b/content/media/MediaDecoderStateMachine.h index dab2fc111c30..64bf7582c80a 100644 --- a/content/media/MediaDecoderStateMachine.h +++ b/content/media/MediaDecoderStateMachine.h @@ -92,6 +92,13 @@ namespace mozilla { class AudioSegment; class VideoSegment; +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount() and conflicts with MediaDecoderStateMachine::GetCurrentTime +// implementation. +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + /* The state machine class. This manages the decoding and seeking in the MediaDecoderReader on the decode thread, and A/V sync on the shared diff --git a/content/media/apple/AppleMP3Reader.cpp b/content/media/apple/AppleMP3Reader.cpp index 50f5827e6724..d2fea06425eb 100644 --- a/content/media/apple/AppleMP3Reader.cpp +++ b/content/media/apple/AppleMP3Reader.cpp @@ -12,8 +12,12 @@ #define AUDIO_READ_BYTES 4096 // Maximum number of audio frames we will accept from the audio decoder in one -// go. -#define MAX_AUDIO_FRAMES 4096 +// go. Carefully select this to work well with both the mp3 1152 max frames +// per block and power-of-2 allocation sizes. Since we must pre-allocate the +// buffer we cannot use AudioCompactor without paying for an additional +// allocation and copy. Therefore, choosing a value that divides exactly into +// 1152 is most memory efficient. +#define MAX_AUDIO_FRAMES 128 namespace mozilla { @@ -201,7 +205,8 @@ AppleMP3Reader::AudioSampleCallback(UInt32 aNumBytes, LOGD("got %u bytes, %u packets\n", aNumBytes, aNumPackets); // 1 frame per packet * num channels * 32-bit float - uint32_t decodedSize = MAX_AUDIO_FRAMES * mAudioChannels * 4; + uint32_t decodedSize = MAX_AUDIO_FRAMES * mAudioChannels * + sizeof(AudioDataValue); // descriptions for _decompressed_ audio packets. ignored. nsAutoArrayPtr @@ -238,6 +243,14 @@ AppleMP3Reader::AudioSampleCallback(UInt32 aNumBytes, break; } + // If we decoded zero frames then AudiOConverterFillComplexBuffer is out + // of data to provide. We drained its internal buffer completely on the + // last pass. + if (numFrames == 0 && rv == kNeedMoreData) { + LOGD("FillComplexBuffer out of data exactly\n"); + break; + } + int64_t time = FramesToUsecs(mCurrentAudioFrame, mAudioSampleRate).value(); int64_t duration = FramesToUsecs(numFrames, mAudioSampleRate).value(); diff --git a/content/media/directshow/DirectShowReader.cpp b/content/media/directshow/DirectShowReader.cpp index 260ec960940e..7830da318c36 100644 --- a/content/media/directshow/DirectShowReader.cpp +++ b/content/media/directshow/DirectShowReader.cpp @@ -1,4 +1,5 @@ -/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @@ -247,6 +248,48 @@ DirectShowReader::Finish(HRESULT aStatus) return false; } +class DirectShowCopy +{ +public: + DirectShowCopy(uint8_t *aSource, uint32_t aBytesPerSample, + uint32_t aSamples, uint32_t aChannels) + : mSource(aSource) + , mBytesPerSample(aBytesPerSample) + , mSamples(aSamples) + , mChannels(aChannels) + , mNextSample(0) + { } + + uint32_t operator()(AudioDataValue *aBuffer, size_t aSamples) + { + size_t maxSamples = std::min(aSamples, mSamples - mNextSample); + uint32_t frames = maxSamples / mChannels; + size_t byteOffset = mNextSample * mBytesPerSample; + if (mBytesPerSample == 1) { + for (uint32_t i = 0; i < maxSamples; ++i) { + uint8_t *sample = mSource + byteOffset; + aBuffer[i] = UnsignedByteToAudioSample(*sample); + byteOffset += mBytesPerSample; + } + } else if (mBytesPerSample == 2) { + for (uint32_t i = 0; i < maxSamples; ++i) { + int16_t *sample = reinterpret_cast(mSource + byteOffset); + aBuffer[i] = AudioSampleToFloat(*sample); + byteOffset += mBytesPerSample; + } + } + mNextSample = maxSamples; + return frames; + } + +private: + uint8_t * const mSource; + const uint32_t mBytesPerSample; + const uint32_t mSamples; + const uint32_t mChannels; + uint32_t mNextSample; +}; + bool DirectShowReader::DecodeAudioData() { @@ -281,26 +324,15 @@ DirectShowReader::DecodeAudioData() hr = sample->GetPointer(&data); NS_ENSURE_TRUE(SUCCEEDED(hr), Finish(hr)); - nsAutoArrayPtr buffer(new AudioDataValue[numSamples]); - AudioDataValue* dst = buffer.get(); - if (mBytesPerSample == 1) { - uint8_t* src = reinterpret_cast(data); - for (int32_t i = 0; i < numSamples; ++i) { - dst[i] = UnsignedByteToAudioSample(src[i]); - } - } else if (mBytesPerSample == 2) { - int16_t* src = reinterpret_cast(data); - for (int32_t i = 0; i < numSamples; ++i) { - dst[i] = AudioSampleToFloat(src[i]); - } - } - - mAudioQueue.Push(new AudioData(mDecoder->GetResource()->Tell(), - RefTimeToUsecs(start), - RefTimeToUsecs(end - start), - numFrames, - buffer.forget(), - mNumChannels)); + mAudioCompactor.Push(mDecoder->GetResource()->Tell(), + RefTimeToUsecs(start), + mInfo.mAudio.mRate, + numFrames, + mNumChannels, + DirectShowCopy(reinterpret_cast(data), + mBytesPerSample, + numSamples, + mNumChannels)); return true; } diff --git a/content/media/gstreamer/GStreamerReader.cpp b/content/media/gstreamer/GStreamerReader.cpp index 32d8425f556e..98451491309d 100644 --- a/content/media/gstreamer/GStreamerReader.cpp +++ b/content/media/gstreamer/GStreamerReader.cpp @@ -532,20 +532,20 @@ bool GStreamerReader::DecodeAudioData() timestamp = gst_segment_to_stream_time(&mAudioSegment, GST_FORMAT_TIME, timestamp); timestamp = GST_TIME_AS_USECONDS(timestamp); - int64_t duration = 0; - if (GST_CLOCK_TIME_IS_VALID(GST_BUFFER_DURATION(buffer))) - duration = GST_TIME_AS_USECONDS(GST_BUFFER_DURATION(buffer)); int64_t offset = GST_BUFFER_OFFSET(buffer); unsigned int size = GST_BUFFER_SIZE(buffer); int32_t frames = (size / sizeof(AudioDataValue)) / mInfo.mAudio.mChannels; - ssize_t outSize = static_cast(size / sizeof(AudioDataValue)); - nsAutoArrayPtr data(new AudioDataValue[outSize]); - memcpy(data, GST_BUFFER_DATA(buffer), GST_BUFFER_SIZE(buffer)); - AudioData* audio = new AudioData(offset, timestamp, duration, - frames, data.forget(), mInfo.mAudio.mChannels); - mAudioQueue.Push(audio); + typedef AudioCompactor::NativeCopy GstCopy; + mAudioCompactor.Push(offset, + timestamp, + mInfo.mAudio.mRate, + frames, + mInfo.mAudio.mChannels, + GstCopy(GST_BUFFER_DATA(buffer), + size, + mInfo.mAudio.mChannels)); gst_buffer_unref(buffer); return true; diff --git a/content/media/gtest/TestAudioCompactor.cpp b/content/media/gtest/TestAudioCompactor.cpp new file mode 100644 index 000000000000..40b86992c150 --- /dev/null +++ b/content/media/gtest/TestAudioCompactor.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "gtest/gtest.h" +#include "AudioCompactor.h" +#include "MediaDecoderReader.h" + +using mozilla::AudioCompactor; +using mozilla::AudioData; +using mozilla::AudioDataValue; +using mozilla::MediaDecoderReader; +using mozilla::MediaQueue; + +class TestCopy +{ +public: + TestCopy(uint32_t aFrames, uint32_t aChannels, + uint32_t &aCallCount, uint32_t &aFrameCount) + : mFrames(aFrames) + , mChannels(aChannels) + , mCallCount(aCallCount) + , mFrameCount(aFrameCount) + { } + + uint32_t operator()(AudioDataValue *aBuffer, uint32_t aSamples) + { + mCallCount += 1; + uint32_t frames = std::min(mFrames - mFrameCount, aSamples / mChannels); + mFrameCount += frames; + return frames; + } + +private: + const uint32_t mFrames; + const uint32_t mChannels; + uint32_t &mCallCount; + uint32_t &mFrameCount; +}; + +static void TestAudioCompactor(size_t aBytes) +{ + MediaQueue queue; + AudioCompactor compactor(queue); + + uint64_t offset = 0; + uint64_t time = 0; + uint32_t sampleRate = 44000; + uint32_t channels = 2; + uint32_t frames = aBytes / (channels * sizeof(AudioDataValue)); + size_t maxSlop = aBytes / AudioCompactor::MAX_SLOP_DIVISOR; + + uint32_t callCount = 0; + uint32_t frameCount = 0; + + compactor.Push(offset, time, sampleRate, frames, channels, + TestCopy(frames, channels, callCount, frameCount)); + + EXPECT_GT(callCount, 0U) << "copy functor never called"; + EXPECT_EQ(frames, frameCount) << "incorrect number of frames copied"; + + MediaDecoderReader::AudioQueueMemoryFunctor memoryFunc; + queue.LockedForEach(memoryFunc); + size_t allocSize = memoryFunc.mSize - (callCount * sizeof(AudioData)); + size_t slop = allocSize - aBytes; + EXPECT_LE(slop, maxSlop) << "allowed too much allocation slop"; +} + +TEST(Media, AudioCompactor_4000) +{ + TestAudioCompactor(4000); +} + +TEST(Media, AudioCompactor_4096) +{ + TestAudioCompactor(4096); +} + +TEST(Media, AudioCompactor_5000) +{ + TestAudioCompactor(5000); +} + +TEST(Media, AudioCompactor_5256) +{ + TestAudioCompactor(5256); +} + +TEST(Media, AudioCompactor_NativeCopy) +{ + const uint32_t channels = 2; + const size_t srcBytes = 32; + const uint32_t srcSamples = srcBytes / sizeof(AudioDataValue); + const uint32_t srcFrames = srcSamples / channels; + uint8_t src[srcBytes]; + + for (uint32_t i = 0; i < srcBytes; ++i) { + src[i] = i; + } + + AudioCompactor::NativeCopy copy(src, srcBytes, channels); + + const uint32_t dstSamples = srcSamples * 2; + AudioDataValue dst[dstSamples]; + + const AudioDataValue notCopied = 0xffff; + for (uint32_t i = 0; i < dstSamples; ++i) { + dst[i] = notCopied; + } + + const uint32_t copyCount = 8; + uint32_t copiedFrames = 0; + uint32_t nextSample = 0; + for (uint32_t i = 0; i < copyCount; ++i) { + uint32_t copySamples = dstSamples / copyCount; + copiedFrames += copy(dst + nextSample, copySamples); + nextSample += copySamples; + } + + EXPECT_EQ(srcFrames, copiedFrames) << "copy exact number of source frames"; + + // Verify that the only the correct bytes were copied. + for (uint32_t i = 0; i < dstSamples; ++i) { + if (i < srcSamples) { + EXPECT_NE(notCopied, dst[i]) << "should have copied over these bytes"; + } else { + EXPECT_EQ(notCopied, dst[i]) << "should not have copied over these bytes"; + } + } +} diff --git a/content/media/gtest/moz.build b/content/media/gtest/moz.build index 85c816a44496..a5d80ff5c8c6 100644 --- a/content/media/gtest/moz.build +++ b/content/media/gtest/moz.build @@ -7,6 +7,7 @@ LIBRARY_NAME = 'media_gtest' UNIFIED_SOURCES += [ + 'TestAudioCompactor.cpp', 'TestTrackEncoder.cpp', ] diff --git a/content/media/moz.build b/content/media/moz.build index 4315929a476b..528126b353d7 100644 --- a/content/media/moz.build +++ b/content/media/moz.build @@ -58,6 +58,7 @@ EXPORTS += [ 'AbstractMediaDecoder.h', 'AudioAvailableEventManager.h', 'AudioChannelFormat.h', + 'AudioCompactor.h', 'AudioEventTimeline.h', 'AudioNodeEngine.h', 'AudioNodeExternalInputStream.h', @@ -115,6 +116,7 @@ EXPORTS.mozilla.dom += [ UNIFIED_SOURCES += [ 'AudioAvailableEventManager.cpp', 'AudioChannelFormat.cpp', + 'AudioCompactor.cpp', 'AudioNodeEngine.cpp', 'AudioNodeExternalInputStream.cpp', 'AudioNodeStream.cpp', diff --git a/content/media/omx/MediaOmxReader.cpp b/content/media/omx/MediaOmxReader.cpp index d8342e4c17a4..db4e2de1a631 100644 --- a/content/media/omx/MediaOmxReader.cpp +++ b/content/media/omx/MediaOmxReader.cpp @@ -312,33 +312,29 @@ bool MediaOmxReader::DecodeAudioData() int64_t pos = mDecoder->GetResource()->Tell(); // Read next frame - MPAPI::AudioFrame frame; - if (!mOmxDecoder->ReadAudio(&frame, mAudioSeekTimeUs)) { + MPAPI::AudioFrame source; + if (!mOmxDecoder->ReadAudio(&source, mAudioSeekTimeUs)) { return false; } mAudioSeekTimeUs = -1; // Ignore empty buffer which stagefright media read will sporadically return - if (frame.mSize == 0) { + if (source.mSize == 0) { return true; } - nsAutoArrayPtr buffer(new AudioDataValue[frame.mSize/2] ); - memcpy(buffer.get(), frame.mData, frame.mSize); + uint32_t frames = source.mSize / (source.mAudioChannels * + sizeof(AudioDataValue)); - uint32_t frames = frame.mSize / (2 * frame.mAudioChannels); - CheckedInt64 duration = FramesToUsecs(frames, frame.mAudioSampleRate); - if (!duration.isValid()) { - return false; - } - - mAudioQueue.Push(new AudioData(pos, - frame.mTimeUs, - duration.value(), - frames, - buffer.forget(), - frame.mAudioChannels)); - return true; + typedef AudioCompactor::NativeCopy OmxCopy; + return mAudioCompactor.Push(pos, + source.mTimeUs, + source.mAudioSampleRate, + frames, + source.mAudioChannels, + OmxCopy(static_cast(source.mData), + source.mSize, + source.mAudioChannels)); } nsresult MediaOmxReader::Seek(int64_t aTarget, int64_t aStartTime, int64_t aEndTime, int64_t aCurrentTime) diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index b867192a47d0..51b6141e5496 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -4776,8 +4776,10 @@ nsDocShell::LoadErrorPage(nsIURI *aURI, const char16_t *aURL, } errorPageUrl.AppendLiteral("&c="); errorPageUrl.AppendASCII(escapedCharset.get()); - errorPageUrl.AppendLiteral("&d="); - errorPageUrl.AppendASCII(escapedDescription.get()); + + nsAutoCString frameType(FrameTypeToString(mFrameType)); + errorPageUrl.AppendLiteral("&f="); + errorPageUrl.AppendASCII(frameType.get()); // Append the manifest URL if the error comes from an app. nsString manifestURL; @@ -4791,9 +4793,10 @@ nsDocShell::LoadErrorPage(nsIURI *aURI, const char16_t *aURL, errorPageUrl.AppendASCII(manifestParam.get()); } - nsAutoCString frameType(FrameTypeToString(mFrameType)); - errorPageUrl.AppendLiteral("&f="); - errorPageUrl.AppendASCII(frameType.get()); + // netError.xhtml's getDescription only handles the "d" parameter at the + // end of the URL, so append it last. + errorPageUrl.AppendLiteral("&d="); + errorPageUrl.AppendASCII(escapedDescription.get()); nsCOMPtr errorPageURI; rv = NS_NewURI(getter_AddRefs(errorPageURI), errorPageUrl); diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index db6cdc3c88dc..da89db13d829 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -6,6 +6,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.favicons.OnFaviconLoadedListener; +import org.mozilla.gecko.favicons.decoders.FaviconDecoder; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.GeckoLayerClient; import org.mozilla.gecko.gfx.LayerView; @@ -771,51 +772,67 @@ public class GeckoAppShell createShortcut(aTitle, aURI, aURI, aIconData, aType); } - // for non-webapps + // For non-webapps. public static void createShortcut(String aTitle, String aURI, Bitmap aBitmap, String aType) { createShortcut(aTitle, aURI, aURI, aBitmap, aType); } - // internal, for webapps - static void createShortcut(String aTitle, String aURI, String aUniqueURI, String aIconData, String aType) { - createShortcut(aTitle, aURI, aUniqueURI, BitmapUtils.getBitmapFromDataURI(aIconData), aType); - } - - public static void createShortcut(final String aTitle, final String aURI, final String aUniqueURI, - final Bitmap aIcon, final String aType) - { + // Internal, for webapps. + static void createShortcut(final String aTitle, final String aURI, final String aUniqueURI, final String aIconData, final String aType) { ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - // the intent to be launched by the shortcut - Intent shortcutIntent; - if (aType.equalsIgnoreCase(SHORTCUT_TYPE_WEBAPP)) { - shortcutIntent = getWebAppIntent(aURI, aUniqueURI, aTitle, aIcon); - } else { - shortcutIntent = new Intent(); - shortcutIntent.setAction(GeckoApp.ACTION_BOOKMARK); - shortcutIntent.setData(Uri.parse(aURI)); - shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, - AppConstants.BROWSER_INTENT_CLASS); - } - - Intent intent = new Intent(); - intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - if (aTitle != null) - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aTitle); - else - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI); - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, aType)); - - // Do not allow duplicate items - intent.putExtra("duplicate", false); - - intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - getContext().sendBroadcast(intent); + // TODO: use the cache. Bug 961600. + Bitmap icon = FaviconDecoder.getMostSuitableBitmapFromDataURI(aIconData, getPreferredIconSize()); + GeckoAppShell.doCreateShortcut(aTitle, aURI, aURI, icon, aType); } }); } + public static void createShortcut(final String aTitle, final String aURI, final String aUniqueURI, + final Bitmap aIcon, final String aType) { + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + GeckoAppShell.doCreateShortcut(aTitle, aURI, aUniqueURI, aIcon, aType); + } + }); + } + + /** + * Call this method only on the background thread. + */ + private static void doCreateShortcut(final String aTitle, final String aURI, final String aUniqueURI, + final Bitmap aIcon, final String aType) { + // The intent to be launched by the shortcut. + Intent shortcutIntent; + if (aType.equalsIgnoreCase(SHORTCUT_TYPE_WEBAPP)) { + shortcutIntent = getWebAppIntent(aURI, aUniqueURI, aTitle, aIcon); + } else { + shortcutIntent = new Intent(); + shortcutIntent.setAction(GeckoApp.ACTION_BOOKMARK); + shortcutIntent.setData(Uri.parse(aURI)); + shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, + AppConstants.BROWSER_INTENT_CLASS); + } + + Intent intent = new Intent(); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, aType)); + + if (aTitle != null) { + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aTitle); + } else { + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI); + } + + // Do not allow duplicate items. + intent.putExtra("duplicate", false); + + intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + getContext().sendBroadcast(intent); + } + public static void removeShortcut(final String aTitle, final String aURI, final String aType) { removeShortcut(aTitle, aURI, null, aType); } diff --git a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java index 87af864f93b9..b506d112a1f3 100644 --- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java +++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java @@ -538,7 +538,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { this.fieldID = integerQuery("named_fields", "field_id", "measurement_name = ? AND measurement_version = ? AND field_name = ?", new String[] {measurementName, measurementVersion, fieldName}, - -1); + UNKNOWN_TYPE_OR_FIELD_ID); if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) { throw new IllegalStateException("No field with name " + fieldName + " (" + measurementName + ", " + measurementVersion + ")"); @@ -552,7 +552,9 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { // store differently stable kinds of data, hence type difference. // Note that we don't pre-populate the environment cache. We'll typically only // handle one per session. - private final ConcurrentHashMap envs = new ConcurrentHashMap(); + // + // protected for testing purposes only. + protected final ConcurrentHashMap envs = new ConcurrentHashMap(); /** * An {@link Environment} that knows how to persist to and from our database. @@ -855,6 +857,12 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { private HashMap fields = new HashMap(); private boolean fieldsCacheUpdated = false; + private void invalidateFieldsCache() { + synchronized (this.fields) { + fieldsCacheUpdated = false; + } + } + private String getFieldKey(String mName, int mVersion, String fieldName) { return mVersion + "." + mName + "/" + fieldName; } @@ -1014,9 +1022,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { notifyMeasurementVersionUpdated(measurement, version); // Let's be easy for now. - synchronized (fields) { - fieldsCacheUpdated = false; - } + invalidateFieldsCache(); } /** @@ -1152,10 +1158,16 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { final SQLiteDatabase db = this.helper.getWritableDatabase(); putValue(v, value); - try { - db.insertOrThrow(table, null, v); - } catch (SQLiteConstraintException e) { - throw new IllegalStateException("Event did not reference existing an environment or field.", e); + + // Using SQLiteDatabase.insertOrThrow throws SQLiteConstraintException we cannot catch for + // unknown reasons (bug 961526 comment 13). We believe these are thrown because we attempt to + // record events using environment IDs removed from the database by the prune service. We + // invalidate the currentEnvironment ID after pruning, preventing further propagation, + // however, any event recording waiting for the prune service to complete on the background + // thread may carry an invalid ID: we expect an insertion failure and drop these events here. + final long res = db.insert(table, null, v); + if (res == -1) { + Logger.error(LOG_TAG, "Unable to record daily discrete event. Ignoring."); } } @@ -1516,6 +1528,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { try { // Cascade will clear the rest. db.delete("measurements", null, null); + + // Clear measurements and fields cache, because some of their IDs are now invalid. + invalidateFieldsCache(); // Let it repopulate on its own. + populateMeasurementVersionsCache(db); // Performed at Storage init so repopulate now. + db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -1524,7 +1541,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { /** * Prunes the given number of least-recently used environments. Note that orphaned environments - * are not removed. + * are not removed and the environment cache is cleared. */ public void pruneEnvironments(final int numToPrune) { final SQLiteDatabase db = this.helper.getWritableDatabase(); @@ -1538,6 +1555,9 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { " LIMIT " + numToPrune + ")", null); db.setTransactionSuccessful(); + + // Clear environment cache, because some of their IDs are now invalid. + this.envs.clear(); } finally { db.endTransaction(); } diff --git a/mobile/android/base/background/healthreport/prune/PrunePolicyDatabaseStorage.java b/mobile/android/base/background/healthreport/prune/PrunePolicyDatabaseStorage.java index 2522b6ba0806..459130aaa92f 100644 --- a/mobile/android/base/background/healthreport/prune/PrunePolicyDatabaseStorage.java +++ b/mobile/android/base/background/healthreport/prune/PrunePolicyDatabaseStorage.java @@ -43,6 +43,11 @@ public class PrunePolicyDatabaseStorage implements PrunePolicyStorage { public void pruneEnvironments(final int count) { getStorage().pruneEnvironments(count); + + // Re-populate the DB and environment cache with the current environment in the unlikely event + // that it was deleted. + this.currentEnvironmentID = -1; + getCurrentEnvironmentID(); } /** diff --git a/mobile/android/base/favicons/Favicons.java b/mobile/android/base/favicons/Favicons.java index eda9ce17b8b0..3e1ba5d51463 100644 --- a/mobile/android/base/favicons/Favicons.java +++ b/mobile/android/base/favicons/Favicons.java @@ -7,6 +7,7 @@ package org.mozilla.gecko.favicons; import org.mozilla.gecko.AboutPages; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.R; import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tabs; @@ -465,15 +466,19 @@ public class Favicons { } /** - * Sidestep the cache and get, from either the database or the internet, the largest available - * Favicon for the given page URL. Useful for creating homescreen shortcuts without being limited - * by possibly low-resolution values in the cache. - * Deduces the favicon URL from the history database and, ultimately, guesses. + * Sidestep the cache and get, from either the database or the internet, a favicon + * suitable for use as an app icon for the provided URL. * - * @param url Page URL to get a large favicon image fro. - * @param onFaviconLoadedListener Listener to call back with the result. + * Useful for creating homescreen shortcuts without being limited + * by possibly low-resolution values in the cache. + * + * Deduces the favicon URL from the browser database, guessing if necessary. + * + * @param url page URL to get a large favicon image for. + * @param onFaviconLoadedListener listener to call back with the result. */ - public static void getLargestFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) { - loadUncachedFavicon(url, null, 0, -1, onFaviconLoadedListener); + public static void getPreferredSizeFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) { + int preferredSize = GeckoAppShell.getPreferredIconSize(); + loadUncachedFavicon(url, null, 0, preferredSize, onFaviconLoadedListener); } } diff --git a/mobile/android/base/favicons/LoadFaviconTask.java b/mobile/android/base/favicons/LoadFaviconTask.java index 44b53bd4adac..68f2e52fc658 100644 --- a/mobile/android/base/favicons/LoadFaviconTask.java +++ b/mobile/android/base/favicons/LoadFaviconTask.java @@ -474,7 +474,6 @@ public class LoadFaviconTask extends UiAsyncTask { private void processResult(Bitmap image) { Favicons.removeLoadTask(mId); - Bitmap scaled = image; // Notify listeners, scaling if required. diff --git a/mobile/android/base/favicons/cache/FaviconCache.java b/mobile/android/base/favicons/cache/FaviconCache.java index ad94b4136b31..faa91e76d146 100644 --- a/mobile/android/base/favicons/cache/FaviconCache.java +++ b/mobile/android/base/favicons/cache/FaviconCache.java @@ -332,7 +332,8 @@ public class FaviconCache { FaviconCacheElement cacheElement; - int cacheElementIndex = container.getNextHighestIndex(targetSize); + // If targetSize is -1, it means we want the largest possible icon. + int cacheElementIndex = (targetSize == -1) ? -1 : container.getNextHighestIndex(targetSize); // cacheElementIndex now holds either the index of the next least largest bitmap from // targetSize, or -1 if targetSize > all bitmaps. @@ -362,12 +363,16 @@ public class FaviconCache { // If there is no such primary, we'll upscale the next least smaller one instead. cacheElement = container.getNextPrimary(cacheElementIndex); - if (cacheElement == null) { // The primary has been invalidated! Fail! Need to get it back from the database. return null; } + if (targetSize == -1) { + // We got the biggest primary, so that's what we'll return. + return cacheElement.mFaviconPayload; + } + // Having got this far, we'll be needing to write the new secondary to the cache, which // involves us falling through to the next try block. This flag lets us do this (Other // paths prior to this end in returns.) diff --git a/mobile/android/base/favicons/decoders/FaviconDecoder.java b/mobile/android/base/favicons/decoders/FaviconDecoder.java index b2ad25a0d092..caaff343b758 100644 --- a/mobile/android/base/favicons/decoders/FaviconDecoder.java +++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java @@ -145,6 +145,50 @@ public class FaviconDecoder { return decodeFavicon(buffer, 0, buffer.length); } + /** + * Returns the smallest bitmap in the icon represented by the provided + * data: URI that's larger than the desired width, or the largest if + * there is no larger icon. + * + * Returns null if no bitmap could be extracted. + * + * Bug 961600: we shouldn't be doing all of this work. The favicon cache + * should be used, and will give us the right size icon. + */ + public static Bitmap getMostSuitableBitmapFromDataURI(String iconURI, int desiredWidth) { + LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI); + if (result == null) { + // Nothing we can do. + Log.w(LOG_TAG, "Unable to decode icon URI."); + return null; + } + + final Iterator bitmaps = result.getBitmaps(); + if (!bitmaps.hasNext()) { + Log.w(LOG_TAG, "No bitmaps in decoded icon."); + return null; + } + + Bitmap bitmap = bitmaps.next(); + if (!bitmaps.hasNext()) { + // We're done! There was only one, so this is as big as it gets. + return bitmap; + } + + // Find a bitmap of the most suitable size. + int currentWidth = bitmap.getWidth(); + while ((currentWidth < desiredWidth) && + bitmaps.hasNext()) { + final Bitmap b = bitmaps.next(); + if (b.getWidth() > currentWidth) { + currentWidth = b.getWidth(); + bitmap = b; + } + } + + return bitmap; + } + /** * Iterator to hold a single bitmap. */ diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index 503438b4ac0c..e49b3044e013 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -37,9 +37,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -758,11 +762,11 @@ public class BrowserHealthRecorder implements GeckoEventListener { public static final String MEASUREMENT_NAME_SEARCH_COUNTS = "org.mozilla.searches.counts"; public static final int MEASUREMENT_VERSION_SEARCH_COUNTS = 5; - public static final String[] SEARCH_LOCATIONS = { + public static final Set SEARCH_LOCATIONS = Collections.unmodifiableSet(new HashSet(Arrays.asList(new String[] { "barkeyword", "barsuggest", "bartext", - }; + }))); private void initializeSearchProvider() { this.storage.ensureMeasurementInitialized( @@ -771,7 +775,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { new MeasurementFields() { @Override public Iterable getFields() { - ArrayList out = new ArrayList(SEARCH_LOCATIONS.length); + ArrayList out = new ArrayList(SEARCH_LOCATIONS.size()); for (String location : SEARCH_LOCATIONS) { // We're not using a counter, because the set of engine // identifiers is potentially unbounded, and thus our @@ -803,19 +807,31 @@ public class BrowserHealthRecorder implements GeckoEventListener { return; } + final int env = this.env; + + if (env == -1) { + Log.d(LOG_TAG, "No environment: not recording search."); + return; + } + if (location == null) { throw new IllegalArgumentException("location must be provided for search."); } + // Ensure that we don't throw when trying to look up the field for an + // unknown location. If you add a search location, you must extend the + // list of search locations *and update the measurement version*. + if (!SEARCH_LOCATIONS.contains(location)) { + throw new IllegalArgumentException("Unexpected location: " + location); + } + final int day = storage.getDay(); - final int env = this.env; final String key = (engineID == null) ? "other" : engineID; - final BrowserHealthRecorder self = this; ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - final HealthReportDatabaseStorage storage = self.storage; + final HealthReportDatabaseStorage storage = BrowserHealthRecorder.this.storage; if (storage == null) { Log.d(LOG_TAG, "No storage: not recording search. Shutting down?"); return; diff --git a/mobile/android/base/home/HomeFragment.java b/mobile/android/base/home/HomeFragment.java index ed25594ddbce..b608fa079abc 100644 --- a/mobile/android/base/home/HomeFragment.java +++ b/mobile/android/base/home/HomeFragment.java @@ -137,8 +137,8 @@ abstract class HomeFragment extends Fragment { return false; } - // Fetch the largest cacheable icon size. - Favicons.getLargestFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); + // Fetch an icon big enough for use as a home screen icon. + Favicons.getPreferredSizeFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); return true; } diff --git a/mobile/android/base/home/TopSitesPanel.java b/mobile/android/base/home/TopSitesPanel.java index 1e14c0a1ea10..1ae8881304cc 100644 --- a/mobile/android/base/home/TopSitesPanel.java +++ b/mobile/android/base/home/TopSitesPanel.java @@ -381,8 +381,8 @@ public class TopSitesPanel extends HomeFragment { return false; } - // Fetch the largest cacheable icon size. - Favicons.getLargestFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); + // Fetch an icon big enough for use as a home screen icon. + Favicons.getPreferredSizeFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); return true; } diff --git a/mobile/android/base/preferences/SearchEnginePreference.java b/mobile/android/base/preferences/SearchEnginePreference.java index e7398a4aba97..6da088973472 100644 --- a/mobile/android/base/preferences/SearchEnginePreference.java +++ b/mobile/android/base/preferences/SearchEnginePreference.java @@ -135,31 +135,6 @@ public class SearchEnginePreference extends CustomListPreference { final String iconURI = geckoEngineJSON.getString("iconURI"); // Keep a reference to the bitmap - we'll need it later in onBindView. try { - - // Bug 961600: we shouldn't be doing all of this work. The favicon cache - // should be used, and will give us the right size icon. - - LoadFaviconResult result = FaviconDecoder.decodeDataURI(iconURI); - if (result == null) { - // Nothing we can do. - Log.w(LOGTAG, "Unable to decode icon URI."); - return; - } - - Iterator bitmaps = result.getBitmaps(); - if (!bitmaps.hasNext()) { - Log.w(LOGTAG, "No bitmaps in decoded icon."); - return; - } - - mIconBitmap = bitmaps.next(); - - if (!bitmaps.hasNext()) { - // We're done! There was only one, so this is as big as it gets. - return; - } - - // Find a bitmap of a more suitable size. final int desiredWidth; if (mFaviconView != null) { desiredWidth = mFaviconView.getWidth(); @@ -174,15 +149,8 @@ public class SearchEnginePreference extends CustomListPreference { } } - int currentWidth = mIconBitmap.getWidth(); - while ((currentWidth < desiredWidth) && - bitmaps.hasNext()) { - Bitmap b = bitmaps.next(); - if (b.getWidth() > currentWidth) { - currentWidth = b.getWidth(); - mIconBitmap = b; - } - } + // TODO: use the cache. Bug 961600. + mIconBitmap = FaviconDecoder.getMostSuitableBitmapFromDataURI(iconURI, desiredWidth); } catch (IllegalArgumentException e) { Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e); diff --git a/mobile/android/base/webapp/EventListener.java b/mobile/android/base/webapp/EventListener.java index d745a15e689b..0adc35b48724 100644 --- a/mobile/android/base/webapp/EventListener.java +++ b/mobile/android/base/webapp/EventListener.java @@ -8,6 +8,7 @@ package org.mozilla.gecko.webapp; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.favicons.decoders.FaviconDecoder; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.util.ActivityResultHandler; import org.mozilla.gecko.util.EventDispatcher; @@ -114,7 +115,9 @@ public class EventListener implements GeckoEventListener { int index = allocator.getIndexForApp(aOriginalOrigin); assert aIconURL != null; - Bitmap icon = BitmapUtils.getBitmapFromDataURI(aIconURL); + + final int preferredSize = GeckoAppShell.getPreferredIconSize(); + Bitmap icon = FaviconDecoder.getMostSuitableBitmapFromDataURI(aIconURL, preferredSize); assert aOrigin != null && index != -1; allocator.updateAppAllocation(aOrigin, index, icon); diff --git a/mobile/android/components/build/nsIShellService.idl b/mobile/android/components/build/nsIShellService.idl index 7d61dfba09c2..64557461de07 100644 --- a/mobile/android/components/build/nsIShellService.idl +++ b/mobile/android/components/build/nsIShellService.idl @@ -19,7 +19,7 @@ interface nsIShellService : nsISupports * * @param aTitle the user-friendly name of the shortcut. * @param aURI the URI to open. - * @param aIconData a base64 encoded representation of the shortcut's icon. + * @param aIconData a base64-encoded data: URI representation of the shortcut's icon, as accepted by the favicon decoder. * @param aIntent how the URI should be opened. Examples: "default", "bookmark" and "webapp" */ void createShortcut(in AString aTitle, in AString aURI, in AString aIconData, in AString aIntent); diff --git a/mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java b/mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java index 14e6d19ee44c..5c5b50b5e56d 100644 --- a/mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java +++ b/mobile/android/tests/background/junit3/src/healthreport/MockHealthReportDatabaseStorage.java @@ -5,6 +5,7 @@ package org.mozilla.gecko.background.healthreport; import java.io.File; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; import org.json.JSONObject; import org.mozilla.gecko.background.common.GlobalConstants; @@ -42,6 +43,10 @@ public class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage return now - numDays * GlobalConstants.MILLISECONDS_PER_DAY; } + public ConcurrentHashMap getEnvironmentCache() { + return this.envs; + } + public MockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) { super(context, fakeProfileDirectory); } diff --git a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java index 0b4915569809..858de40e25ca 100644 --- a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java +++ b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportDatabaseStorage.java @@ -326,10 +326,6 @@ public class TestHealthReportDatabaseStorage extends FakeProfileTestCase { storage.incrementDailyCount(nonExistentEnvID, storage.getToday(), counterFieldID); fail("Should throw - event_integer(env) references environments(id), which is given as a non-existent value."); } catch (IllegalStateException e) { } - try { - storage.recordDailyDiscrete(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu"); - fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value."); - } catch (IllegalStateException e) { } try { storage.recordDailyLast(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu"); fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value."); @@ -339,14 +335,30 @@ public class TestHealthReportDatabaseStorage extends FakeProfileTestCase { storage.incrementDailyCount(envID, storage.getToday(), nonExistentFieldID); fail("Should throw - event_integer(field) references fields(id), which is given as a non-existent value."); } catch (IllegalStateException e) { } - try { - storage.recordDailyDiscrete(envID, storage.getToday(), nonExistentFieldID, "iu"); - fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value."); - } catch (IllegalStateException e) { } try { storage.recordDailyLast(envID, storage.getToday(), nonExistentFieldID, "iu"); fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value."); } catch (IllegalStateException e) { } + + // Test dropped events due to constraint violations that do not throw (see bug 961526). + final String eventValue = "a value not in the database"; + assertFalse(isEventInDB(db, eventValue)); // Better safe than sorry. + + storage.recordDailyDiscrete(nonExistentEnvID, storage.getToday(), discreteFieldID, eventValue); + assertFalse(isEventInDB(db, eventValue)); + + storage.recordDailyDiscrete(envID, storage.getToday(), nonExistentFieldID, "iu"); + assertFalse(isEventInDB(db, eventValue)); + } + + private static boolean isEventInDB(final SQLiteDatabase db, final String value) { + final Cursor c = db.query("events_textual", new String[] {"value"}, "value = ?", + new String[] {value}, null, null, null); + try { + return c.getCount() > 0; + } finally { + c.close(); + } } // Largely taken from testDeleteEnvAndEventsBefore and testDeleteOrphanedAddons. @@ -553,7 +565,10 @@ public class TestHealthReportDatabaseStorage extends FakeProfileTestCase { new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory, 2); final SQLiteDatabase db = storage.getDB(); assertEquals(5, DBHelpers.getRowCount(db, "environments")); + assertEquals(5, storage.getEnvironmentCache().size()); + storage.pruneEnvironments(1); + assertEquals(0, storage.getEnvironmentCache().size()); assertTrue(!getEnvAppVersions(db).contains("v3")); storage.pruneEnvironments(2); assertTrue(!getEnvAppVersions(db).contains("v2")); diff --git a/services/common/hawk.js b/services/common/hawk.js index a13298d16520..3c1d4d8e4b9e 100644 --- a/services/common/hawk.js +++ b/services/common/hawk.js @@ -45,7 +45,7 @@ Cu.import("resource://gre/modules/Promise.jsm"); * @param host * The url of the host */ -function HawkClient(host) { +this.HawkClient = function(host) { this.host = host; // Clock offset in milliseconds between our client's clock and the date @@ -53,7 +53,7 @@ function HawkClient(host) { this._localtimeOffsetMsec = 0; } -HawkClient.prototype = { +this.HawkClient.prototype = { /* * Construct an error message for a response. Private. diff --git a/testing/mochitest/b2g-debug.json b/testing/mochitest/b2g-debug.json index 13f3c2940a8f..6b0d0b6e7ac6 100644 --- a/testing/mochitest/b2g-debug.json +++ b/testing/mochitest/b2g-debug.json @@ -381,6 +381,7 @@ "dom/tests/mochitest/localstorage/test_localStorageReplace.html":"", "dom/tests/mochitest/sessionstorage/test_cookieSession.html":"", "dom/tests/mochitest/sessionstorage/test_sessionStorageBaseSessionOnly.html":"", + "dom/tests/mochitest/sessionstorage/test_sessionStorageClone.html":"bug 968051", "dom/tests/mochitest/pointerlock/test_pointerlock-api.html":"window.open focus issues (using fullscreen)", "dom/tests/mochitest/sessionstorage/test_sessionStorageBase.html":"", diff --git a/testing/mochitest/b2g.json b/testing/mochitest/b2g.json index 5a2e8093980f..e9c7e03ec5f6 100644 --- a/testing/mochitest/b2g.json +++ b/testing/mochitest/b2g.json @@ -358,6 +358,7 @@ "dom/tests/mochitest/localstorage/test_localStorageQuotaSessionOnly2.html":"needs https support", "dom/tests/mochitest/sessionstorage/test_cookieSession.html":"4 failures", "dom/tests/mochitest/sessionstorage/test_sessionStorageBase.html":"no storage chrome event received", + "dom/tests/mochitest/sessionstorage/test_sessionStorageBaseSessionOnly.html":"bug 967737", "dom/tests/mochitest/sessionstorage/test_sessionStorageHttpHttps.html":"needs https to work", "dom/tests/mochitest/pointerlock/test_pointerlock-api.html":"window.open focus issues (using fullscreen)", diff --git a/toolkit/components/telemetry/TelemetryPing.jsm b/toolkit/components/telemetry/TelemetryPing.jsm index 20864c43ae9b..54183cc2ca5d 100644 --- a/toolkit/components/telemetry/TelemetryPing.jsm +++ b/toolkit/components/telemetry/TelemetryPing.jsm @@ -457,6 +457,14 @@ let Impl = { revision: HISTOGRAMS_FILE_VERSION, locale: getLocale() }; + + // In order to share profile data, the appName used for Metro Firefox is "Firefox", + // (the same as desktop Firefox). We set it to "MetroFirefox" here in order to + // differentiate telemetry pings sent by desktop vs. metro Firefox. + if(Services.metro && Services.metro.immersive) { + ret.appName = "MetroFirefox"; + } + if (this._previousBuildID) { ret.previousBuildID = this._previousBuildID; } diff --git a/toolkit/modules/LightweightThemeConsumer.jsm b/toolkit/modules/LightweightThemeConsumer.jsm index 0e3f8fbd5940..611b640579b2 100644 --- a/toolkit/modules/LightweightThemeConsumer.jsm +++ b/toolkit/modules/LightweightThemeConsumer.jsm @@ -42,9 +42,7 @@ LightweightThemeConsumer.prototype = { _lastScreenWidth: null, _lastScreenHeight: null, _enabled: true, -#ifdef XP_MACOSX - _chromemarginDefault: undefined, -#endif + _active: false, enable: function() { this._enabled = true; @@ -100,8 +98,9 @@ LightweightThemeConsumer.prototype = { if (!this._enabled) return; - var root = this._doc.documentElement; - var active = !!aData.headerURL; + let root = this._doc.documentElement; + let active = !!aData.headerURL; + let stateChanging = (active != this._active); if (active) { root.style.color = aData.textcolor || "black"; @@ -117,6 +116,8 @@ LightweightThemeConsumer.prototype = { root.removeAttribute("lwtheme"); } + this._active = active; + _setImage(root, active, aData.headerURL); if (this._footerId) { let footer = this._doc.getElementById(this._footerId); @@ -129,20 +130,26 @@ LightweightThemeConsumer.prototype = { } #ifdef XP_MACOSX - // Sample whether or not we draw in the titlebar by default the first time we update. - // If the root has no chromemargin attribute, getAttribute will return null, and - // we'll remove the attribute when the lw-theme is deactivated. - if (this._chromemarginDefault === undefined) - this._chromemarginDefault = root.getAttribute("chromemargin"); + // On OS X, we extend the lightweight theme into the titlebar, which means setting + // the chromemargin attribute. Some XUL applications already draw in the titlebar, + // so we need to save the chromemargin value before we overwrite it with the value + // that lets us draw in the titlebar. We stash this value on the root attribute so + // that XUL applications have the ability to invalidate the saved value. + if (stateChanging) { + if (!root.hasAttribute("chromemargin-nonlwtheme")) { + root.setAttribute("chromemargin-nonlwtheme", root.getAttribute("chromemargin")); + } - if (active) { - root.setAttribute("chromemargin", "0,-1,-1,-1"); - } - else { - if (this._chromemarginDefault) - root.setAttribute("chromemargin", this._chromemarginDefault); - else - root.removeAttribute("chromemargin"); + if (active) { + root.setAttribute("chromemargin", "0,-1,-1,-1"); + } else { + let defaultChromemargin = root.getAttribute("chromemargin-nonlwtheme"); + if (defaultChromemargin) { + root.setAttribute("chromemargin", defaultChromemargin); + } else { + root.removeAttribute("chromemargin"); + } + } } #endif }