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
}