diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 7f77c145755a..3ca73b7bae54 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 52d9beae1882..39798ff479e1 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index f9b9939bd87c..91215b76d45e 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 7f77c145755a..3ca73b7bae54 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 0a89820afe88..913b9b78f107 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 03b25a571648..7a926808cdb9 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "7f0af1e164a39efb732c0c341c2a8e51f681d913", + "revision": "c3d40600c0090c5ca6ca4427f3a870ff443a109d", "repo_path": "/integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 9284bc57a774..b1d5bf83a546 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index be2109e1a11d..3f59d2f4181d 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 01483d247122..15e68ab6e846 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 1199de03df88..1b149f836deb 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/browser/base/content/highlighter.css b/browser/base/content/highlighter.css index cb29d4119626..7e61b8224c1d 100644 --- a/browser/base/content/highlighter.css +++ b/browser/base/content/highlighter.css @@ -45,6 +45,7 @@ svg|line.box-model-guide-bottom[hidden] { html|*.highlighter-nodeinfobar-id, html|*.highlighter-nodeinfobar-classes, html|*.highlighter-nodeinfobar-pseudo-classes, +html|*.highlighter-nodeinfobar-dimensions, html|*.highlighter-nodeinfobar-tagname { -moz-user-select: text; -moz-user-focus: normal; diff --git a/browser/base/content/newtab/page.js b/browser/base/content/newtab/page.js index 8a81780fd021..b99f76456dc8 100644 --- a/browser/base/content/newtab/page.js +++ b/browser/base/content/newtab/page.js @@ -210,7 +210,19 @@ let gPage = { for (let site of gGrid.sites) { if (site) { site.captureIfMissing(); - let {type} = site.link; + + // Record which tile index a directory link was shown + let {directoryIndex, type} = site.link; + if (directoryIndex !== undefined) { + let tileIndex = site.cell.index; + // For telemetry, only handle the first 9 links in the first 9 cells + if (directoryIndex < 9) { + let shownId = "NEWTAB_PAGE_DIRECTORY_LINK" + directoryIndex + "_SHOWN"; + Services.telemetry.getHistogramById(shownId).add(Math.min(9, tileIndex)); + } + } + + // Aggregate tile impression counts into directory types if (type in directoryCount) { directoryCount[type]++; } diff --git a/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js b/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js index 432a57f022ed..a7e58abf0ed4 100644 --- a/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js +++ b/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js @@ -12,6 +12,70 @@ function test() { } function spawnTest() { + let lines = [ + 'Manifest has a character encoding of ISO-8859-1. Manifests must have the ' + + 'utf-8 character encoding.', + 'The first line of the manifest must be "CACHE MANIFEST" at line 1.', + '"CACHE MANIFEST" is only valid on the first line but was found at line 3.', + 'images/sound-icon.png points to a resource that is not available at line 9.', + 'images/background.png points to a resource that is not available at line 10.', + '/checking.cgi points to a resource that is not available at line 13.', + 'Asterisk (*) incorrectly used in the NETWORK section at line 14. If a line ' + + 'in the NETWORK section contains only a single asterisk character, then any ' + + 'URI not listed in the manifest will be treated as if the URI was listed in ' + + 'the NETWORK section. Otherwise such URIs will be treated as unavailable. ' + + 'Other uses of the * character are prohibited', + '../rel.html points to a resource that is not available at line 17.', + '../../rel.html points to a resource that is not available at line 18.', + '../../../rel.html points to a resource that is not available at line 19.', + '../../../../rel.html points to a resource that is not available at line 20.', + '../../../../../rel.html points to a resource that is not available at line 21.', + '/../ is not a valid URI prefix at line 22.', + '/test.css points to a resource that is not available at line 23.', + '/test.js points to a resource that is not available at line 24.', + 'test.png points to a resource that is not available at line 25.', + '/main/features.js points to a resource that is not available at line 27.', + '/main/settings/index.css points to a resource that is not available at line 28.', + 'http://example.com/scene.jpg points to a resource that is not available at line 29.', + '/section1/blockedbyfallback.html points to a resource that is not available at line 30.', + 'http://example.com/images/world.jpg points to a resource that is not available at line 31.', + '/section2/blockedbyfallback.html points to a resource that is not available at line 32.', + '/main/home points to a resource that is not available at line 34.', + 'main/app.js points to a resource that is not available at line 35.', + '/settings/home points to a resource that is not available at line 37.', + '/settings/app.js points to a resource that is not available at line 38.', + 'The file http://sub1.test1.example.com/browser/browser/devtools/' + + 'commandline/test/browser_cmd_appcache_invalid_page3.html was modified ' + + 'after http://sub1.test1.example.com/browser/browser/devtools/' + + 'commandline/test/browser_cmd_appcache_invalid_appcache.appcache. Unless ' + + 'the text in the manifest file is changed the cached version will be used ' + + 'instead at line 39.', + 'browser_cmd_appcache_invalid_page3.html has cache-control set to no-store. ' + + 'This will prevent the application cache from storing the file at line 39.', + 'http://example.com/logo.png points to a resource that is not available at line 40.', + 'http://example.com/check.png points to a resource that is not available at line 41.', + 'Spaces in URIs need to be replaced with % at line 42.', + 'http://example.com/cr oss.png points to a resource that is not available at line 42.', + 'Asterisk (*) incorrectly used in the CACHE section at line 43. If a line ' + + 'in the NETWORK section contains only a single asterisk character, then ' + + 'any URI not listed in the manifest will be treated as if the URI was ' + + 'listed in the NETWORK section. Otherwise such URIs will be treated as ' + + 'unavailable. Other uses of the * character are prohibited', + 'The SETTINGS section may only contain a single value, "prefer-online" or "fast" at line 47.', + 'FALLBACK section line 50 (/section1/ /offline1.html) prevents caching of ' + + 'line 30 (/section1/blockedbyfallback.html) in the CACHE section.', + '/offline1.html points to a resource that is not available at line 50.', + 'FALLBACK section line 51 (/section2/ offline2.html) prevents caching of ' + + 'line 32 (/section2/blockedbyfallback.html) in the CACHE section.', + 'offline2.html points to a resource that is not available at line 51.', + 'Only two URIs separated by spaces are allowed in the FALLBACK section at line 52.', + 'Asterisk (*) incorrectly used in the FALLBACK section at line 53. URIs ' + + 'in the FALLBACK section simply need to match a prefix of the request URI.', + 'offline3.html points to a resource that is not available at line 53.', + 'Invalid section name (BLAH) at line 55.', + 'Only two URIs separated by spaces are allowed in the FALLBACK section at line 55.' + ]; + let options = yield helpers.openTab(TEST_URI); info("window open"); @@ -25,7 +89,8 @@ function spawnTest() { // Pages containing an appcache the notification bar gives options to allow // or deny permission for the app to save data offline. Let's click Allow. let notificationID = "offline-app-requested-sub1.test1.example.com"; - let notification = PopupNotifications.getNotification(notificationID, gBrowser.selectedBrowser); + let notification = + PopupNotifications.getNotification(notificationID, gBrowser.selectedBrowser); if (notification) { info("Authorizing offline storage."); @@ -45,52 +110,7 @@ function spawnTest() { args: {} }, exec: { - output: [ - /Manifest has a character encoding of ISO-8859-1\. Manifests must have the utf-8 character encoding\./, - /The first line of the manifest must be "CACHE MANIFEST" at line 1\./, - /"CACHE MANIFEST" is only valid on the first line but was found at line 3\./, - /images\/sound-icon\.png points to a resource that is not available at line 9\./, - /images\/background\.png points to a resource that is not available at line 10\./, - /NETWORK section line 13 \(\/checking\.cgi\) prevents caching of line 13 \(\/checking\.cgi\) in the NETWORK section\./, - /\/checking\.cgi points to a resource that is not available at line 13\./, - /Asterisk \(\*\) incorrectly used in the NETWORK section at line 14\. If a line in the NETWORK section contains only a single asterisk character, then any URI not listed in the manifest will be treated as if the URI was listed in the NETWORK section\. Otherwise such URIs will be treated as unavailable\. Other uses of the \* character are prohibited/, - /\.\.\/rel\.html points to a resource that is not available at line 17\./, - /\.\.\/\.\.\/rel\.html points to a resource that is not available at line 18\./, - /\.\.\/\.\.\/\.\.\/rel\.html points to a resource that is not available at line 19\./, - /\.\.\/\.\.\/\.\.\/\.\.\/rel\.html points to a resource that is not available at line 20\./, - /\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/rel\.html points to a resource that is not available at line 21\./, - /\/\.\.\/ is not a valid URI prefix at line 22\./, - /\/test\.css points to a resource that is not available at line 23\./, - /\/test\.js points to a resource that is not available at line 24\./, - /test\.png points to a resource that is not available at line 25\./, - /\/main\/features\.js points to a resource that is not available at line 27\./, - /\/main\/settings\/index\.css points to a resource that is not available at line 28\./, - /http:\/\/example\.com\/scene\.jpg points to a resource that is not available at line 29\./, - /\/section1\/blockedbyfallback\.html points to a resource that is not available at line 30\./, - /http:\/\/example\.com\/images\/world\.jpg points to a resource that is not available at line 31\./, - /\/section2\/blockedbyfallback\.html points to a resource that is not available at line 32\./, - /\/main\/home points to a resource that is not available at line 34\./, - /main\/app\.js points to a resource that is not available at line 35\./, - /\/settings\/home points to a resource that is not available at line 37\./, - /\/settings\/app\.js points to a resource that is not available at line 38\./, - /The file http:\/\/sub1\.test1\.example\.com\/browser\/browser\/devtools\/commandline\/test\/browser_cmd_appcache_invalid_page3\.html was modified after http:\/\/sub1\.test1\.example\.com\/browser\/browser\/devtools\/commandline\/test\/browser_cmd_appcache_invalid_appcache\.appcache\. Unless the text in the manifest file is changed the cached version will be used instead at line 39\./, - /browser_cmd_appcache_invalid_page3\.html has cache-control set to no-store\. This will prevent the application cache from storing the file at line 39\./, - /http:\/\/example\.com\/logo\.png points to a resource that is not available at line 40\./, - /http:\/\/example\.com\/check\.png points to a resource that is not available at line 41\./, - /Spaces in URIs need to be replaced with % at line 42\./, - /http:\/\/example\.com\/cr oss\.png points to a resource that is not available at line 42\./, - /Asterisk \(\*\) incorrectly used in the CACHE section at line 43\. If a line in the NETWORK section contains only a single asterisk character, then any URI not listed in the manifest will be treated as if the URI was listed in the NETWORK section\. Otherwise such URIs will be treated as unavailable\. Other uses of the \* character are prohibited/, - /The SETTINGS section may only contain a single value, "prefer-online" or "fast" at line 47\./, - /FALLBACK section line 50 \(\/section1\/ \/offline1\.html\) prevents caching of line 30 \(\/section1\/blockedbyfallback\.html\) in the CACHE section\./, - /\/offline1\.html points to a resource that is not available at line 50\./, - /FALLBACK section line 51 \(\/section2\/ offline2\.html\) prevents caching of line 32 \(\/section2\/blockedbyfallback\.html\) in the CACHE section\./, - /offline2\.html points to a resource that is not available at line 51\./, - /Only two URIs separated by spaces are allowed in the FALLBACK section at line 52\./, - /Asterisk \(\*\) incorrectly used in the FALLBACK section at line 53\. URIs in the FALLBACK section simply need to match a prefix of the request URI\./, - /offline3\.html points to a resource that is not available at line 53\./, - /Invalid section name \(BLAH\) at line 55\./, - /Only two URIs separated by spaces are allowed in the FALLBACK section at line 55\./ - ] + output: lines.map(getRegexForString) }, }, ]); diff --git a/browser/devtools/commandline/test/head.js b/browser/devtools/commandline/test/head.js index 602198769ff0..fbe0d1a7997c 100644 --- a/browser/devtools/commandline/test/head.js +++ b/browser/devtools/commandline/test/head.js @@ -27,6 +27,20 @@ function whenDelayedStartupFinished(aWindow, aCallback) { }, "browser-delayed-startup-finished", false); } +/** + * Creates a regular expression that matches a string. This greatly simplifies + * matching and debugging long strings. + * + * @param {String} text + * Text to convert + * @return {RegExp} + * Regular expression matching text + */ +function getRegexForString(str) { + str = str.replace(/(\.|\\|\/|\(|\)|\[|\]|\*|\+|\?|\$|\^|\|)/g, "\\$1"); + return new RegExp(str); +} + /** * Force GC on shutdown, because it seems that GCLI can outrun the garbage * collector in some situations, which causes test failures in later tests diff --git a/browser/devtools/inspector/test/browser_inspector_highlighter.js b/browser/devtools/inspector/test/browser_inspector_highlighter.js index c2c8401e8003..46016dce8765 100644 --- a/browser/devtools/inspector/test/browser_inspector_highlighter.js +++ b/browser/devtools/inspector/test/browser_inspector_highlighter.js @@ -5,106 +5,71 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ let doc; -let h1; +let div; +let rotated; let inspector; +let contentViewer; function createDocument() { - let div = doc.createElement("div"); - h1 = doc.createElement("h1"); - let p1 = doc.createElement("p"); - let p2 = doc.createElement("p"); - let div2 = doc.createElement("div"); - let p3 = doc.createElement("p"); - doc.title = "Inspector Highlighter Meatballs"; - h1.textContent = "Inspector Tree Selection Test"; - p1.textContent = "This is some example text"; - p2.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " + - "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + - "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + - "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + - "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + - "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + - "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; - p3.textContent = "Lorem ipsum dolor sit amet, consectetur adipisicing " + - "elit, sed do eiusmod tempor incididunt ut labore et dolore magna " + - "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + - "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " + - "dolor in reprehenderit in voluptate velit esse cillum dolore eu " + - "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + - "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; - let div3 = doc.createElement("div"); - div3.id = "checkOutThisWickedSpread"; - div3.setAttribute("style", "position: absolute; top: 20px; right: 20px; height: 20px; width: 20px; background-color: yellow; border: 1px dashed black;"); - let p4 = doc.createElement("p"); - p4.setAttribute("style", "font-weight: 200; font-size: 8px; text-align: center;"); - p4.textContent = "Smörgåsbord!"; - div.appendChild(h1); - div.appendChild(p1); - div.appendChild(p2); - div2.appendChild(p3); - div3.appendChild(p4); + div = doc.createElement("div"); + div.setAttribute("style", + "padding:5px; border:7px solid red; margin: 9px; " + + "position:absolute; top:30px; left:150px;"); + let textNode = doc.createTextNode("Gort! Klaatu barada nikto!"); + rotated = doc.createElement("div"); + rotated.setAttribute("style", + "padding:5px; border:7px solid red; margin: 9px; " + + "transform:rotate(45deg); " + + "position:absolute; top:30px; left:80px;"); + div.appendChild(textNode); doc.body.appendChild(div); - doc.body.appendChild(div2); - doc.body.appendChild(div3); + doc.body.appendChild(rotated); openInspector(aInspector => { inspector = aInspector; inspector.selection.setNode(div, null); - inspector.once("inspector-updated", () => { - inspector.toolbox.highlighterUtils.startPicker().then(testMouseOverH1Highlights); - }); + inspector.once("inspector-updated", testMouseOverDivHighlights); }); } -function testMouseOverH1Highlights() { +function testMouseOverDivHighlights() { + ok(isHighlighting(), "Highlighter is shown"); + is(getHighlitNode(), div, "Highlighter's outline correspond to the non-rotated div"); + testNonTransformedBoxModelDimensionsNoZoom(); +} + +function testNonTransformedBoxModelDimensionsNoZoom() { + info("Highlighted the non-rotated div"); + isNodeCorrectlyHighlighted(div, "non-zoomed"); + + inspector.toolbox.once("highlighter-ready", testNonTransformedBoxModelDimensionsZoomed); + contentViewer = gBrowser.selectedBrowser.docShell.contentViewer + .QueryInterface(Ci.nsIMarkupDocumentViewer); + contentViewer.fullZoom = 2; +} + +function testNonTransformedBoxModelDimensionsZoomed() { + info("Highlighted the zoomed, non-rotated div"); + isNodeCorrectlyHighlighted(div, "zoomed"); + + inspector.toolbox.once("highlighter-ready", testMouseOverRotatedHighlights); + contentViewer.fullZoom = 1; +} + +function testMouseOverRotatedHighlights() { inspector.toolbox.once("highlighter-ready", () => { ok(isHighlighting(), "Highlighter is shown"); - is(getHighlitNode(), h1, "Highlighter's outline correspond to the selected node"); - testBoxModelDimensions(); + info("Highlighted the rotated div"); + isNodeCorrectlyHighlighted(rotated, "rotated"); + + executeSoon(finishUp); }); - - EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content); -} - -function testBoxModelDimensions() { - let h1Dims = h1.getBoundingClientRect(); - let h1Width = Math.ceil(h1Dims.width); - let h1Height = Math.ceil(h1Dims.height); - - let outlineDims = getSimpleBorderRect(); - let outlineWidth = Math.ceil(outlineDims.width); - let outlineHeight = Math.ceil(outlineDims.height); - - // Disabled due to bug 716245 - is(outlineWidth, h1Width, "outline width matches dimensions of element (no zoom)"); - is(outlineHeight, h1Height, "outline height matches dimensions of element (no zoom)"); - - // zoom the page by a factor of 2 - let contentViewer = gBrowser.selectedBrowser.docShell.contentViewer - .QueryInterface(Ci.nsIMarkupDocumentViewer); - contentViewer.fullZoom = 2; - - // simulate the zoomed dimensions of the div element - let h1Dims = h1.getBoundingClientRect(); - // There seems to be some very minor differences in the floats, so let's - // floor the values - let h1Width = Math.floor(h1Dims.width * contentViewer.fullZoom); - let h1Height = Math.floor(h1Dims.height * contentViewer.fullZoom); - - let outlineDims = getSimpleBorderRect(); - let outlineWidth = Math.floor(outlineDims.width); - let outlineHeight = Math.floor(outlineDims.height); - - is(outlineWidth, h1Width, "outline width matches dimensions of element (zoomed)"); - - is(outlineHeight, h1Height, "outline height matches dimensions of element (zoomed)"); - - executeSoon(finishUp); + inspector.selection.setNode(rotated); } function finishUp() { inspector.toolbox.highlighterUtils.stopPicker().then(() => { - doc = h1 = inspector = null; + doc = div = rotated = inspector = contentViewer = null; let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); gBrowser.removeCurrentTab(); diff --git a/browser/devtools/inspector/test/browser_inspector_infobar.js b/browser/devtools/inspector/test/browser_inspector_infobar.js index 2aabae619ae8..cdc61ea0c8a0 100644 --- a/browser/devtools/inspector/test/browser_inspector_infobar.js +++ b/browser/devtools/inspector/test/browser_inspector_infobar.js @@ -17,19 +17,58 @@ function test() { waitForFocus(setupInfobarTest, content); }, true); - let style = "body{width:100%;height: 100%} div {position: absolute;height: 100px;width: 500px}#bottom {bottom: 0px}#vertical {height: 100%}#farbottom{bottom: -200px}"; - let html = "
" + let style = "body{width:100%;height: 100%} div {position: absolute;" + + "height: 100px;width: 500px}#bottom {bottom: 0px}#vertical {"+ + "height: 100%}#farbottom{bottom: -200px}"; + let html = "
" + + "
" + + "
" - content.location = "data:text/html," + encodeURIComponent(html); + content.location = "data:text/html;charset=utf-8," + encodeURIComponent(html); function setupInfobarTest() { nodes = [ - {node: doc.querySelector("#top"), position: "bottom", tag: "DIV", id: "#top", classes: ".class1.class2"}, - {node: doc.querySelector("#vertical"), position: "overlap", tag: "DIV", id: "#vertical", classes: ""}, - {node: doc.querySelector("#bottom"), position: "top", tag: "DIV", id: "#bottom", classes: ""}, - {node: doc.querySelector("body"), position: "overlap", tag: "BODY", id: "", classes: ""}, - {node: doc.querySelector("#farbottom"), position: "top", tag: "DIV", id: "#farbottom", classes: ""}, - ] + { + node: doc.querySelector("#top"), + position: "bottom", + tag: "DIV", + id: "#top", + classes: ".class1.class2", + dims: "500 x 100" + }, + { + node: doc.querySelector("#vertical"), + position: "overlap", + tag: "DIV", + id: "#vertical", + classes: "" + // No dims as they will vary between computers + }, + { + node: doc.querySelector("#bottom"), + position: "top", + tag: "DIV", + id: "#bottom", + classes: "", + dims: "500 x 100" + }, + { + node: doc.querySelector("body"), + position: "overlap", + tag: "BODY", + id: "", + classes: "" + // No dims as they will vary between computers + }, + { + node: doc.querySelector("#farbottom"), + position: "top", + tag: "DIV", + id: "#farbottom", + classes: "", + dims: "500 x 100" + }, + ]; for (let i = 0; i < nodes.length; i++) { ok(nodes[i].node, "node " + i + " found"); @@ -74,16 +113,24 @@ function test() { let stack = browser.parentNode; let container = stack.querySelector(".highlighter-nodeinfobar-positioner"); - is(container.getAttribute("position"), nodes[cursor].position, "node " + cursor + ": position matches."); + is(container.getAttribute("position"), + nodes[cursor].position, "node " + cursor + ": position matches."); let tagNameLabel = stack.querySelector(".highlighter-nodeinfobar-tagname"); - is(tagNameLabel.textContent, nodes[cursor].tag, "node " + cursor + ": tagName matches."); + is(tagNameLabel.textContent, nodes[cursor].tag, + "node " + cursor + ": tagName matches."); let idLabel = stack.querySelector(".highlighter-nodeinfobar-id"); is(idLabel.textContent, nodes[cursor].id, "node " + cursor + ": id matches."); let classesBox = stack.querySelector(".highlighter-nodeinfobar-classes"); - is(classesBox.textContent, nodes[cursor].classes, "node " + cursor + ": classes match."); + is(classesBox.textContent, nodes[cursor].classes, + "node " + cursor + ": classes match."); + + if (nodes[cursor].dims) { + let dimBox = stack.querySelector(".highlighter-nodeinfobar-dimensions"); + is(dimBox.textContent, nodes[cursor].dims, "node " + cursor + ": dims match."); + } } function finishUp() { diff --git a/browser/devtools/inspector/test/head.js b/browser/devtools/inspector/test/head.js index e3e3c66b74c7..cf3a10469c18 100644 --- a/browser/devtools/inspector/test/head.js +++ b/browser/devtools/inspector/test/head.js @@ -400,7 +400,7 @@ function focusSearchBoxUsingShortcut(panelWin, callback) { altKey: modifiersAttr.match("alt"), metaKey: modifiersAttr.match("meta"), accelKey: modifiersAttr.match("accel") - } + }; let searchBox = panelWin.document.getElementById("inspector-searchbox"); searchBox.addEventListener("focus", function onFocus() { @@ -425,6 +425,60 @@ function getComputedPropertyValue(aName) } } +function isNodeCorrectlyHighlighted(node, prefix="") { + let boxModel = getBoxModelStatus(); + let helper = new LayoutHelpers(window.content); + + prefix += (prefix ? " " : "") + node.nodeName; + prefix += (node.id ? "#" + node.id : ""); + prefix += (node.classList.length ? "." + [...node.classList].join(".") : ""); + prefix += " "; + + let quads = helper.getAdjustedQuads(node, "content"); + let {p1:cp1, p2:cp2, p3:cp3, p4:cp4} = boxModel.content.points; + is(cp1.x, quads.p1.x, prefix + "content point 1 x co-ordinate is correct"); + is(cp1.y, quads.p1.y, prefix + "content point 1 y co-ordinate is correct"); + is(cp2.x, quads.p2.x, prefix + "content point 2 x co-ordinate is correct"); + is(cp2.y, quads.p2.y, prefix + "content point 2 y co-ordinate is correct"); + is(cp3.x, quads.p3.x, prefix + "content point 3 x co-ordinate is correct"); + is(cp3.y, quads.p3.y, prefix + "content point 3 y co-ordinate is correct"); + is(cp4.x, quads.p4.x, prefix + "content point 4 x co-ordinate is correct"); + is(cp4.y, quads.p4.y, prefix + "content point 4 y co-ordinate is correct"); + + quads = helper.getAdjustedQuads(node, "padding"); + let {p1:pp1, p2:pp2, p3:pp3, p4:pp4} = boxModel.padding.points; + is(pp1.x, quads.p1.x, prefix + "padding point 1 x co-ordinate is correct"); + is(pp1.y, quads.p1.y, prefix + "padding point 1 y co-ordinate is correct"); + is(pp2.x, quads.p2.x, prefix + "padding point 2 x co-ordinate is correct"); + is(pp2.y, quads.p2.y, prefix + "padding point 2 y co-ordinate is correct"); + is(pp3.x, quads.p3.x, prefix + "padding point 3 x co-ordinate is correct"); + is(pp3.y, quads.p3.y, prefix + "padding point 3 y co-ordinate is correct"); + is(pp4.x, quads.p4.x, prefix + "padding point 4 x co-ordinate is correct"); + is(pp4.y, quads.p4.y, prefix + "padding point 4 y co-ordinate is correct"); + + quads = helper.getAdjustedQuads(node, "border"); + let {p1:bp1, p2:bp2, p3:bp3, p4:bp4} = boxModel.border.points; + is(bp1.x, quads.p1.x, prefix + "border point 1 x co-ordinate is correct"); + is(bp1.y, quads.p1.y, prefix + "border point 1 y co-ordinate is correct"); + is(bp2.x, quads.p2.x, prefix + "border point 2 x co-ordinate is correct"); + is(bp2.y, quads.p2.y, prefix + "border point 2 y co-ordinate is correct"); + is(bp3.x, quads.p3.x, prefix + "border point 3 x co-ordinate is correct"); + is(bp3.y, quads.p3.y, prefix + "border point 3 y co-ordinate is correct"); + is(bp4.x, quads.p4.x, prefix + "border point 4 x co-ordinate is correct"); + is(bp4.y, quads.p4.y, prefix + "border point 4 y co-ordinate is correct"); + + quads = helper.getAdjustedQuads(node, "margin"); + let {p1:mp1, p2:mp2, p3:mp3, p4:mp4} = boxModel.margin.points; + is(mp1.x, quads.p1.x, prefix + "margin point 1 x co-ordinate is correct"); + is(mp1.y, quads.p1.y, prefix + "margin point 1 y co-ordinate is correct"); + is(mp2.x, quads.p2.x, prefix + "margin point 2 x co-ordinate is correct"); + is(mp2.y, quads.p2.y, prefix + "margin point 2 y co-ordinate is correct"); + is(mp3.x, quads.p3.x, prefix + "margin point 3 x co-ordinate is correct"); + is(mp3.y, quads.p3.y, prefix + "margin point 3 y co-ordinate is correct"); + is(mp4.x, quads.p4.x, prefix + "margin point 4 x co-ordinate is correct"); + is(mp4.y, quads.p4.y, prefix + "margin point 4 y co-ordinate is correct"); +} + function getContainerForRawNode(markupView, rawNode) { let front = markupView.walker.frontForRawNode(rawNode); diff --git a/browser/devtools/shared/AppCacheUtils.jsm b/browser/devtools/shared/AppCacheUtils.jsm index 98a68e71b4fa..2b6d066dc64c 100644 --- a/browser/devtools/shared/AppCacheUtils.jsm +++ b/browser/devtools/shared/AppCacheUtils.jsm @@ -118,7 +118,8 @@ AppCacheUtils.prototype = { for (let neturi of parsed.uris) { if (neturi.section == "NETWORK") { for (let parsedUri of parsed.uris) { - if (parsedUri.uri.startsWith(neturi.uri)) { + if (parsedUri.section !== "NETWORK" && + parsedUri.uri.startsWith(neturi.uri)) { this._addError(neturi.line, "networkBlocksURI", neturi.line, neturi.original, parsedUri.line, parsedUri.original, parsedUri.section); @@ -164,7 +165,7 @@ AppCacheUtils.prototype = { this._addError(parsedUri.line, "cacheControlNoStore", parsedUri.original, parsedUri.line); } - } else { + } else if (parsedUri.original !== "*") { this._addError(parsedUri.line, "notAvailable", parsedUri.original, parsedUri.line); } @@ -182,7 +183,6 @@ AppCacheUtils.prototype = { let inputStream = Cc["@mozilla.org/scriptableinputstream;1"] .createInstance(Ci.nsIScriptableInputStream); let deferred = promise.defer(); - let channelCharset = ""; let buffer = ""; let channel = Services.io.newChannel(uri, null, null); @@ -203,7 +203,7 @@ AppCacheUtils.prototype = { }, onStopRequest: function onStartRequest(request, context, statusCode) { - if (statusCode == 0) { + if (statusCode === 0) { request.QueryInterface(Ci.nsIHttpChannel); let result = { @@ -279,7 +279,7 @@ AppCacheUtils.prototype = { } }); - if (entries.length == 0) { + if (entries.length === 0) { throw new Error(l10n.GetStringFromName("noResults")); } return entries; @@ -320,17 +320,23 @@ AppCacheUtils.prototype = { _getManifestURI: function ACU__getManifestURI() { let deferred = promise.defer(); - let getURI = node => { + let getURI = () => { let htmlNode = this.doc.querySelector("html[manifest]"); if (htmlNode) { let pageUri = this.doc.location ? this.doc.location.href : this.uri; let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1); - return origin + htmlNode.getAttribute("manifest"); + let manifestURI = htmlNode.getAttribute("manifest"); + + if (manifestURI.startsWith("/")) { + manifestURI = manifestURI.substr(1); + } + + return origin + manifestURI; } }; if (this.doc) { - let uri = getURI(this.doc); + let uri = getURI(); return promise.resolve(uri); } else { this._getURIInfo(this.uri).then(uriInfo => { @@ -338,7 +344,7 @@ AppCacheUtils.prototype = { let html = uriInfo.text; let parser = _DOMParser; this.doc = parser.parseFromString(html, "text/html"); - let uri = getURI(this.doc); + let uri = getURI(); deferred.resolve(uri); } else { this.errors.push({ @@ -394,10 +400,10 @@ ManifestParser.prototype = { this.currSection = "CACHE"; for (let i = 0; i < lines.length; i++) { - let text = this.text = lines[i].replace(/^\s+|\s+$/g); + let text = this.text = lines[i].trim(); this.currentLine = i + 1; - if (i == 0 && text != "CACHE MANIFEST") { + if (i === 0 && text !== "CACHE MANIFEST") { this._addError(1, "firstLineMustBeCacheManifest", 1); } @@ -453,7 +459,7 @@ ManifestParser.prototype = { if (/\s/.test(text)) { this._addError(this.currentLine, "escapeSpaces", this.currentLine); - text = text.replace(/\s/g, "%20") + text = text.replace(/\s/g, "%20"); } if (text[0] == "/") { @@ -506,7 +512,7 @@ ManifestParser.prototype = { if (/\s/.test(namespace)) { this._addError(this.currentLine, "escapeSpaces", this.currentLine); - namespace = namespace.replace(/\s/g, "%20") + namespace = namespace.replace(/\s/g, "%20"); } if (namespace.substr(0, 4) == "/../") { diff --git a/browser/devtools/styleeditor/StyleEditorUI.jsm b/browser/devtools/styleeditor/StyleEditorUI.jsm index 9ad06fb50939..df28eea64c74 100644 --- a/browser/devtools/styleeditor/StyleEditorUI.jsm +++ b/browser/devtools/styleeditor/StyleEditorUI.jsm @@ -64,7 +64,7 @@ function StyleEditorUI(debuggee, target, panelDoc) { this.selectedEditor = null; this.savedLocations = {}; - this._updateContextMenu = this._updateContextMenu.bind(this); + this._updateOptionsMenu = this._updateOptionsMenu.bind(this); this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this); this._onNewDocument = this._onNewDocument.bind(this); this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this); @@ -142,36 +142,26 @@ StyleEditorUI.prototype = { this._importFromFile(this._mockImportFile || null, this._window); }.bind(this)); - this._contextMenu = this._panelDoc.getElementById("sidebar-context"); - this._contextMenu.addEventListener("popupshowing", - this._updateContextMenu); + this._optionsMenu = this._panelDoc.getElementById("style-editor-options-popup"); + this._optionsMenu.addEventListener("popupshowing", + this._updateOptionsMenu); - this._sourcesItem = this._panelDoc.getElementById("context-origsources"); + this._sourcesItem = this._panelDoc.getElementById("options-origsources"); this._sourcesItem.addEventListener("command", this._toggleOrigSources); - this._mediaItem = this._panelDoc.getElementById("context-show-media"); + this._mediaItem = this._panelDoc.getElementById("options-show-media"); this._mediaItem.addEventListener("command", this._toggleMediaSidebar); }, /** - * Update text of context menu option to reflect current preference - * settings + * Update options menu items to reflect current preference settings. */ - _updateContextMenu: function() { - let sourceString = "showOriginalSources"; - if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { - sourceString = "showCSSSources"; - } - this._sourcesItem.setAttribute("label", _(sourceString + ".label")); - this._sourcesItem.setAttribute("accesskey", _(sourceString + ".accesskey")); - - let mediaString = "showMediaSidebar" - if (Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR)) { - mediaString = "hideMediaSidebar"; - } - this._mediaItem.setAttribute("label", _(mediaString + ".label")); - this._mediaItem.setAttribute("accesskey", _(mediaString + ".accesskey")); + _updateOptionsMenu: function() { + this._sourcesItem.setAttribute("checked", + Services.prefs.getBoolPref(PREF_ORIG_SOURCES)); + this._mediaItem.setAttribute("checked", + Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR)); }, /** @@ -771,6 +761,9 @@ StyleEditorUI.prototype = { destroy: function() { this._clearStyleSheetEditors(); + this._optionsMenu.removeEventListener("popupshowing", + this._updateOptionsMenu); + this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument); this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged); this._prefObserver.destroy(); diff --git a/browser/devtools/styleeditor/styleeditor.css b/browser/devtools/styleeditor/styleeditor.css index 3574da0b0e4f..e8de73c3384b 100644 --- a/browser/devtools/styleeditor/styleeditor.css +++ b/browser/devtools/styleeditor/styleeditor.css @@ -15,6 +15,10 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message { display: block; } +.devtools-toolbar > spacer { + -moz-box-flex: 1; +} + .splitview-nav > li, .stylesheet-info, .stylesheet-more { diff --git a/browser/devtools/styleeditor/styleeditor.xul b/browser/devtools/styleeditor/styleeditor.xul index 5fea157fd3a7..0e59e59dd00b 100644 --- a/browser/devtools/styleeditor/styleeditor.xul +++ b/browser/devtools/styleeditor/styleeditor.xul @@ -61,9 +61,16 @@ key="key_gotoLine" command="cmd_gotoLine"/> - - - + + + @@ -91,6 +98,11 @@ accesskey="&importButton.accesskey;" tooltiptext="&importButton.tooltip;" label="&importButton.label;"/> + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties b/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties index cd4483c2163e..1d7d64521992 100644 --- a/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties +++ b/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties @@ -68,38 +68,6 @@ open.accesskey=l # conjunction with accel (Command on Mac or Ctrl on other platforms) to Save saveStyleSheet.commandkey=S -# LOCALIZATION NOTE (showOriginalSources.label): This is the label on the context -# menu item to toggle showing original sources in the editor. -showOriginalSources.label=Show original sources - -# LOCALIZATION NOTE (showOriginalSources.accesskey): This is the access key for -# the menu item to toggle showing original sources in the editor. -showOriginalSources.accesskey=O - -# LOCALIZATION NOTE (showCSSSources.label): This is the label on the context -# menu item to toggle back to showing only CSS sources in the editor. -showCSSSources.label=Show CSS sources - -# LOCALIZATION NOTE (showCSSSources.accesskey): This is the access key for the -# menu item to toggle back to showing only CSS sources in the editor. -showCSSSources.accesskey=C - -# LOCALIZATION NOTE (showMediaSidebar.label): This is the label on the context -# menu item to toggle showing @media rule shortcuts in a sidebar. -showMediaSidebar.label=Show @media sidebar - -# LOCALIZATION NOTE (showMediaSidebar.accesskey): This is the access key for -# the menu item to toggle showing the @media sidebar. -showMediaSidebar.accesskey=M - -# LOCALIZATION NOTE (hideMediaSidebar.label): This is the label on the context -# menu item to stop showing @media rule shortcuts in a sidebar. -hideMediaSidebar.label=Hide @media sidebar - -# LOCALIZATION NOTE (hideMediaSidebar.accesskey): This is the access key for -# the menu item to stop showing the @media sidebar. -hideMediaSidebar.accesskey=H - # LOCALIZATION NOTE (ToolboxStyleEditor.label): # This string is displayed in the title of the tab when the style editor is # displayed inside the developer tools window and in the Developer Tools Menu. diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index 91b432311c8b..2bd1967915e0 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -285,6 +285,7 @@ browser.jar: skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/images/responsivemode/responsiveui-screenshot.png) skin/classic/browser/devtools/responsiveui-screenshot@2x.png (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png) skin/classic/browser/devtools/toggle-tools.png (../shared/devtools/images/toggle-tools.png) + skin/classic/browser/devtools/toggle-tools@2x.png (../shared/devtools/images/toggle-tools@2x.png) skin/classic/browser/devtools/dock-bottom@2x.png (../shared/devtools/images/dock-bottom@2x.png) skin/classic/browser/devtools/dock-side@2x.png (../shared/devtools/images/dock-side@2x.png) skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index 6779dc5ebced..0acf716e2aa9 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -405,6 +405,7 @@ browser.jar: skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/images/responsivemode/responsiveui-screenshot.png) skin/classic/browser/devtools/responsiveui-screenshot@2x.png (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png) skin/classic/browser/devtools/toggle-tools.png (../shared/devtools/images/toggle-tools.png) + skin/classic/browser/devtools/toggle-tools@2x.png (../shared/devtools/images/toggle-tools@2x.png) skin/classic/browser/devtools/dock-bottom@2x.png (../shared/devtools/images/dock-bottom@2x.png) skin/classic/browser/devtools/dock-side@2x.png (../shared/devtools/images/dock-side@2x.png) * skin/classic/browser/devtools/inspector.css (../shared/devtools/inspector.css) diff --git a/browser/themes/shared/devtools/commandline.inc.css b/browser/themes/shared/devtools/commandline.inc.css index bb1368bf8748..990a9ffcb439 100644 --- a/browser/themes/shared/devtools/commandline.inc.css +++ b/browser/themes/shared/devtools/commandline.inc.css @@ -28,6 +28,12 @@ margin: auto 10px; } +.developer-toolbar-button > .toolbarbutton-icon, +#developer-toolbar-closebutton > .toolbarbutton-icon { + width: 16px; + height: 16px; +} + #developer-toolbar-toolbox-button { list-style-image: url("chrome://browser/skin/devtools/toggle-tools.png"); -moz-image-region: rect(0px, 16px, 16px, 0px); @@ -49,6 +55,25 @@ -moz-image-region: rect(0px, 64px, 16px, 48px); } +@media (min-resolution: 2dppx) { + #developer-toolbar-toolbox-button { + list-style-image: url("chrome://browser/skin/devtools/toggle-tools@2x.png"); + -moz-image-region: rect(0px, 32px, 32px, 0px); + } + + #developer-toolbar-toolbox-button:hover { + -moz-image-region: rect(0px, 64px, 32px, 32px); + } + + #developer-toolbar-toolbox-button:hover:active { + -moz-image-region: rect(0px, 96px, 32px, 64px); + } + + #developer-toolbar-toolbox-button[checked=true] { + -moz-image-region: rect(0px, 128px, 32px, 96px); + } +} + #developer-toolbar-closebutton { list-style-image: url("chrome://browser/skin/devtools/close.png"); -moz-appearance: none; @@ -59,6 +84,12 @@ opacity: 0.6; } +@media (min-resolution: 2dppx) { + #developer-toolbar-closebutton { + list-style-image: url("chrome://browser/skin/devtools/close@2x.png"); + } +} + #developer-toolbar-closebutton > .toolbarbutton-icon { /* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must use evil CSS to give the impression of smaller content */ @@ -109,7 +140,6 @@ html|*#gcli-output-frame { .gclitoolbar-input-node { -moz-appearance: none; color: hsl(210,30%,85%); - padding-left: 20px; background-color: #242b33; background-repeat: no-repeat; background-position: 4px center; @@ -118,14 +148,35 @@ html|*#gcli-output-frame { -1px 0 0 hsla(206,37%,4%,.2) inset; line-height: 32px; outline-style: none; - background-image: -moz-image-rect(url("chrome://browser/skin/devtools/commandline-icon.png"), 0, 16, 16, 0); + padding: 0; } .gclitoolbar-input-node[focused="true"] { - background-image: -moz-image-rect(url("chrome://browser/skin/devtools/commandline-icon.png"), 0, 32, 16, 16); background-color: #232e38; } +.gclitoolbar-input-node::before { + content: ""; + display: inline-block; + -moz-box-ordinal-group: 0; + width: 16px; + height: 16px; + margin: 0 2px; + background-image: url("chrome://browser/skin/devtools/commandline-icon.png"); + background-position: 0 center; + background-size: 32px 16px; +} + +.gclitoolbar-input-node[focused="true"]::before { + background-position: -16px center; +} + +@media (min-resolution: 2dppx) { + .gclitoolbar-input-node::before { + background-image: url("chrome://browser/skin/devtools/commandline-icon@2x.png"); + } +} + .gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection { background-color: hsl(210,30%,85%); color: hsl(210,24%,16%); diff --git a/browser/themes/shared/devtools/highlighter.inc.css b/browser/themes/shared/devtools/highlighter.inc.css index 25dfddd82ebc..1eadceb93de6 100644 --- a/browser/themes/shared/devtools/highlighter.inc.css +++ b/browser/themes/shared/devtools/highlighter.inc.css @@ -74,6 +74,13 @@ html|*.highlighter-nodeinfobar-pseudo-classes { color: hsl(200,74%,57%); } +html|*.highlighter-nodeinfobar-dimensions { + color: hsl(210,30%,85%); + -moz-border-start: 1px solid #5a6169; + -moz-margin-start: 6px; + -moz-padding-start: 6px; +} + /* Highlighter - Node Infobar - box & arrow */ .highlighter-nodeinfobar-arrow { diff --git a/browser/themes/shared/devtools/images/toggle-tools.png b/browser/themes/shared/devtools/images/toggle-tools.png index 8a40d0d116d7..495439391c7b 100644 Binary files a/browser/themes/shared/devtools/images/toggle-tools.png and b/browser/themes/shared/devtools/images/toggle-tools.png differ diff --git a/browser/themes/shared/devtools/images/toggle-tools@2x.png b/browser/themes/shared/devtools/images/toggle-tools@2x.png new file mode 100644 index 000000000000..971f414316f5 Binary files /dev/null and b/browser/themes/shared/devtools/images/toggle-tools@2x.png differ diff --git a/browser/themes/shared/devtools/styleeditor.css b/browser/themes/shared/devtools/styleeditor.css index fae3315ed188..11d293438bbe 100644 --- a/browser/themes/shared/devtools/styleeditor.css +++ b/browser/themes/shared/devtools/styleeditor.css @@ -119,6 +119,11 @@ background-position: -24px 8px; } +#style-editor-options { + width: 20px; + overflow: hidden; +} + /* Invert all toggle icons but the one in the active row for light theme */ .theme-light .splitview-nav > li:not(.splitview-active) .stylesheet-enabled { filter: url(filters.svg#invert); diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 80e2dce267b9..7027dd4df240 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -323,6 +323,7 @@ browser.jar: skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/images/responsivemode/responsiveui-screenshot.png) skin/classic/browser/devtools/responsiveui-screenshot@2x.png (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png) skin/classic/browser/devtools/toggle-tools.png (../shared/devtools/images/toggle-tools.png) + skin/classic/browser/devtools/toggle-tools@2x.png (../shared/devtools/images/toggle-tools@2x.png) skin/classic/browser/devtools/dock-bottom@2x.png (../shared/devtools/images/dock-bottom@2x.png) skin/classic/browser/devtools/dock-side@2x.png (../shared/devtools/images/dock-side@2x.png) skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) @@ -723,6 +724,7 @@ browser.jar: skin/classic/aero/browser/devtools/responsiveui-screenshot.png (../shared/devtools/images/responsivemode/responsiveui-screenshot.png) skin/classic/aero/browser/devtools/responsiveui-screenshot@2x.png (../shared/devtools/images/responsivemode/responsiveui-screenshot@2x.png) skin/classic/aero/browser/devtools/toggle-tools.png (../shared/devtools/images/toggle-tools.png) + skin/classic/aero/browser/devtools/toggle-tools@2x.png (../shared/devtools/images/toggle-tools@2x.png) skin/classic/aero/browser/devtools/dock-bottom@2x.png (../shared/devtools/images/dock-bottom@2x.png) skin/classic/aero/browser/devtools/dock-side@2x.png (../shared/devtools/images/dock-side@2x.png) skin/classic/aero/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) diff --git a/configure.in b/configure.in index 774aa793be0b..7a3cef67e945 100644 --- a/configure.in +++ b/configure.in @@ -3938,6 +3938,7 @@ MOZ_ANDROID_HISTORY= MOZ_WEBSMS_BACKEND= MOZ_ANDROID_BEAM= MOZ_ANDROID_SYNTHAPKS= +MOZ_LOCALE_SWITCHER= ACCESSIBILITY=1 MOZ_TIME_MANAGER= MOZ_PAY= @@ -4947,6 +4948,13 @@ if test -n "$MOZ_WEBSMS_BACKEND"; then AC_DEFINE(MOZ_WEBSMS_BACKEND) fi +dnl ======================================================== +dnl = Enable runtime locale switching on Android +dnl ======================================================== +if test -n "$MOZ_LOCALE_SWITCHER"; then + AC_DEFINE(MOZ_LOCALE_SWITCHER) +fi + dnl ======================================================== dnl = Enable NFC permission on Android dnl ======================================================== @@ -8567,6 +8575,7 @@ AC_SUBST(MOZ_ANDROID_HISTORY) AC_SUBST(MOZ_WEBSMS_BACKEND) AC_SUBST(MOZ_ANDROID_BEAM) AC_SUBST(MOZ_ANDROID_SYNTHAPKS) +AC_SUBST(MOZ_LOCALE_SWITCHER) AC_SUBST(MOZ_DISABLE_GECKOVIEW) AC_SUBST(ENABLE_STRIP) AC_SUBST(PKG_SKIP_STRIP) diff --git a/dom/bluetooth2/BluetoothAdapter.cpp b/dom/bluetooth2/BluetoothAdapter.cpp index 929a2dc80007..a5c36c5285a2 100644 --- a/dom/bluetooth2/BluetoothAdapter.cpp +++ b/dom/bluetooth2/BluetoothAdapter.cpp @@ -24,6 +24,9 @@ #include "BluetoothService.h" #include "BluetoothUtils.h" +#define ERR_INVALID_ADAPTER_STATE "InvalidAdapterStateError" +#define ERR_CHANGE_ADAPTER_STATE "ChangeAdapterStateError" + using namespace mozilla; using namespace mozilla::dom; @@ -121,6 +124,7 @@ public: BluetoothReplyRunnable::ReleaseMembers(); mAdapterPtr = nullptr; } + private: nsRefPtr mAdapterPtr; }; @@ -156,6 +160,38 @@ public: } }; +class EnableDisableAdapterTask : public BluetoothReplyRunnable +{ +public: + EnableDisableAdapterTask(Promise* aPromise) + : BluetoothReplyRunnable(nullptr) + , mPromise(aPromise) + { } + + bool + ParseSuccessfulReply(JS::MutableHandle aValue) + { + /* + * It is supposed to be Promise according to BluetoothAdapter.webidl, + * but we have to pass "true" since it is mandatory to pass an + * argument while calling MaybeResolve. + */ + mPromise->MaybeResolve(true); + aValue.setUndefined(); + return true; + } + + void + ReleaseMembers() + { + BluetoothReplyRunnable::ReleaseMembers(); + mPromise = nullptr; + } + +private: + nsRefPtr mPromise; +}; + static int kCreatePairedDeviceTimeout = 50000; // unit: msec BluetoothAdapter::BluetoothAdapter(nsPIDOMWindow* aWindow, @@ -164,12 +200,13 @@ BluetoothAdapter::BluetoothAdapter(nsPIDOMWindow* aWindow, , BluetoothPropertyContainer(BluetoothObjectType::TYPE_ADAPTER) , mJsUuids(nullptr) , mJsDeviceAddresses(nullptr) + // TODO: Change to Disabled after Bug 1006309 landed + , mState(BluetoothAdapterState::Enabled) , mDiscoverable(false) , mDiscovering(false) , mPairable(false) , mPowered(false) , mIsRooted(false) - , mState(BluetoothAdapterState::Disabled) { MOZ_ASSERT(aWindow); MOZ_ASSERT(IsDOMBinding()); @@ -231,7 +268,11 @@ BluetoothAdapter::SetPropertyByValue(const BluetoothNamedValue& aValue) { const nsString& name = aValue.name(); const BluetoothValue& value = aValue.value(); - if (name.EqualsLiteral("Name")) { + if (name.EqualsLiteral("State")) { + bool isEnabled = value.get_bool(); + mState = isEnabled ? BluetoothAdapterState::Enabled + : BluetoothAdapterState::Disabled; + } else if (name.EqualsLiteral("Name")) { mName = value.get_nsString(); } else if (name.EqualsLiteral("Address")) { mAddress = value.get_nsString(); @@ -669,19 +710,55 @@ BluetoothAdapter::SetPairingConfirmation(const nsAString& aDeviceAddress, return request.forget(); } -/* - * TODO: Implement Enable/Disable functions - */ +already_AddRefed +BluetoothAdapter::EnableDisable(bool aEnable) +{ + nsCOMPtr global = do_QueryInterface(GetOwner()); + NS_ENSURE_TRUE(global, nullptr); + + nsRefPtr promise = new Promise(global); + + // Make sure BluetoothService is available before modifying adapter state + BluetoothService* bs = BluetoothService::Get(); + if (!bs) { + promise->MaybeReject(ERR_CHANGE_ADAPTER_STATE); + return promise.forget(); + } + + if (aEnable) { + if (mState != BluetoothAdapterState::Disabled) { + promise->MaybeReject(ERR_INVALID_ADAPTER_STATE); + return promise.forget(); + } + mState = BluetoothAdapterState::Enabling; + } else { + if (mState != BluetoothAdapterState::Enabled) { + promise->MaybeReject(ERR_INVALID_ADAPTER_STATE); + return promise.forget(); + } + mState = BluetoothAdapterState::Disabling; + } + + // TODO: Fire attr changed event for this state change + nsRefPtr result = new EnableDisableAdapterTask(promise); + + if(NS_FAILED(bs->EnableDisable(aEnable, result))) { + promise->MaybeReject(ERR_CHANGE_ADAPTER_STATE); + } + + return promise.forget(); +} + already_AddRefed BluetoothAdapter::Enable() { - return nullptr; + return EnableDisable(true); } already_AddRefed BluetoothAdapter::Disable() { - return nullptr; + return EnableDisable(false); } already_AddRefed diff --git a/dom/bluetooth2/BluetoothAdapter.h b/dom/bluetooth2/BluetoothAdapter.h index c3b6686cfdf8..f067905c2700 100644 --- a/dom/bluetooth2/BluetoothAdapter.h +++ b/dom/bluetooth2/BluetoothAdapter.h @@ -122,10 +122,9 @@ public: SetAuthorization(const nsAString& aDeviceAddress, bool aAllow, ErrorResult& aRv); - already_AddRefed - Enable(); - already_AddRefed - Disable(); + already_AddRefed EnableDisable(bool aEnable); + already_AddRefed Enable(); + already_AddRefed Disable(); already_AddRefed Connect(BluetoothDevice& aDevice, diff --git a/dom/bluetooth2/BluetoothService.cpp b/dom/bluetooth2/BluetoothService.cpp index 35cc8f2dc79d..ce030d927d40 100644 --- a/dom/bluetooth2/BluetoothService.cpp +++ b/dom/bluetooth2/BluetoothService.cpp @@ -163,14 +163,8 @@ BluetoothService::ToggleBtAck::Run() sBluetoothService->SetEnabled(mEnabled); sToggleInProgress = false; - nsAutoString signalName; - signalName = mEnabled ? NS_LITERAL_STRING("Enabled") - : NS_LITERAL_STRING("Disabled"); - BluetoothSignal signal(signalName, NS_LITERAL_STRING(KEY_MANAGER), true); - sBluetoothService->DistributeSignal(signal); - - // Event 'AdapterAdded' has to be fired after firing 'Enabled' sBluetoothService->TryFiringAdapterAdded(); + sBluetoothService->FireAdapterStateChanged(mEnabled); return NS_OK; } @@ -220,19 +214,6 @@ BluetoothService::~BluetoothService() Cleanup(); } -PLDHashOperator -RemoveObserversExceptBluetoothManager - (const nsAString& key, - nsAutoPtr& value, - void* arg) -{ - if (!key.EqualsLiteral(KEY_MANAGER)) { - return PL_DHASH_REMOVE; - } - - return PL_DHASH_NEXT; -} - // static BluetoothService* BluetoothService::Create() @@ -384,7 +365,8 @@ BluetoothService::DistributeSignal(const BluetoothSignal& aSignal) } nsresult -BluetoothService::StartBluetooth(bool aIsStartup) +BluetoothService::StartBluetooth(bool aIsStartup, + BluetoothReplyRunnable* aRunnable) { MOZ_ASSERT(NS_IsMainThread()); @@ -405,7 +387,7 @@ BluetoothService::StartBluetooth(bool aIsStartup) */ if (aIsStartup || !sBluetoothService->IsEnabled()) { // Switch Bluetooth on - if (NS_FAILED(sBluetoothService->StartInternal())) { + if (NS_FAILED(sBluetoothService->StartInternal(aRunnable))) { BT_WARNING("Bluetooth service failed to start!"); } } else { @@ -420,7 +402,8 @@ BluetoothService::StartBluetooth(bool aIsStartup) } nsresult -BluetoothService::StopBluetooth(bool aIsStartup) +BluetoothService::StopBluetooth(bool aIsStartup, + BluetoothReplyRunnable* aRunnable) { MOZ_ASSERT(NS_IsMainThread()); @@ -466,7 +449,7 @@ BluetoothService::StopBluetooth(bool aIsStartup) */ if (aIsStartup || sBluetoothService->IsEnabled()) { // Switch Bluetooth off - if (NS_FAILED(sBluetoothService->StopInternal())) { + if (NS_FAILED(sBluetoothService->StopInternal(aRunnable))) { BT_WARNING("Bluetooth service failed to stop!"); } } else { @@ -481,13 +464,15 @@ BluetoothService::StopBluetooth(bool aIsStartup) } nsresult -BluetoothService::StartStopBluetooth(bool aStart, bool aIsStartup) +BluetoothService::StartStopBluetooth(bool aStart, + bool aIsStartup, + BluetoothReplyRunnable* aRunnable) { nsresult rv; if (aStart) { - rv = StartBluetooth(aIsStartup); + rv = StartBluetooth(aIsStartup, aRunnable); } else { - rv = StopBluetooth(aIsStartup); + rv = StopBluetooth(aIsStartup, aRunnable); } return rv; } @@ -504,17 +489,6 @@ BluetoothService::SetEnabled(bool aEnabled) unused << childActors[index]->SendEnabled(aEnabled); } - if (!aEnabled) { - /** - * Remove all handlers except BluetoothManager when turning off bluetooth - * since it is possible that the event 'onAdapterAdded' would be fired after - * BluetoothManagers of child process are registered. Please see Bug 827759 - * for more details. - */ - mBluetoothSignalObserverTable.Enumerate( - RemoveObserversExceptBluetoothManager, nullptr); - } - /** * mEnabled: real status of bluetooth * aEnabled: expected status of bluetooth @@ -553,7 +527,7 @@ nsresult BluetoothService::HandleStartupSettingsCheck(bool aEnable) { MOZ_ASSERT(NS_IsMainThread()); - return StartStopBluetooth(aEnable, true); + return StartStopBluetooth(aEnable, true, nullptr); } nsresult @@ -610,37 +584,6 @@ BluetoothService::HandleSettingsChanged(const nsAString& aData) } SWITCH_BT_DEBUG(value.toBoolean()); - - return NS_OK; - } - - // Second, check if the string is BLUETOOTH_ENABLED_SETTING - if (!JS_StringEqualsAscii(cx, key.toString(), BLUETOOTH_ENABLED_SETTING, &match)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (match) { - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (!value.isBoolean()) { - MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.enabled'!"); - return NS_ERROR_UNEXPECTED; - } - - if (sToggleInProgress || value.toBoolean() == IsEnabled()) { - // Nothing to do here. - return NS_OK; - } - - sToggleInProgress = true; - - nsresult rv = StartStopBluetooth(value.toBoolean(), false); - NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; @@ -704,7 +647,7 @@ BluetoothService::HandleShutdown() } } - if (IsEnabled() && NS_FAILED(StopBluetooth(false))) { + if (IsEnabled() && NS_FAILED(StopBluetooth(false, nullptr))) { MOZ_ASSERT(false, "Failed to deliver stop message!"); } @@ -785,6 +728,38 @@ BluetoothService::AdapterAddedReceived() mAdapterAddedReceived = true; } +/** + * Enable/Disable the local adapter. + * + * There is only one adapter on the mobile in current use cases. + * In addition, bluedroid couldn't enable/disable a single adapter. + * So currently we will turn on/off BT to enable/disable the adapter. + * + * TODO: To support enable/disable single adapter in the future, + * we will need to implement EnableDisableInternal for different stacks. + */ +nsresult +BluetoothService::EnableDisable(bool aEnable, + BluetoothReplyRunnable* aRunnable) +{ + sToggleInProgress = true; + return StartStopBluetooth(aEnable, false, aRunnable); +} + +void +BluetoothService::FireAdapterStateChanged(bool aEnable) +{ + MOZ_ASSERT(NS_IsMainThread()); + + InfallibleTArray props; + BT_APPEND_NAMED_VALUE(props, "State", aEnable); + BluetoothValue value(props); + + BluetoothSignal signal(NS_LITERAL_STRING("PropertyChanged"), + NS_LITERAL_STRING(KEY_ADAPTER), value); + DistributeSignal(signal); +} + void BluetoothService::Notify(const BluetoothSignal& aData) { diff --git a/dom/bluetooth2/BluetoothService.h b/dom/bluetooth2/BluetoothService.h index 04fc0c98ca91..c6e7f59f12ac 100644 --- a/dom/bluetooth2/BluetoothService.h +++ b/dom/bluetooth2/BluetoothService.h @@ -315,6 +315,28 @@ public: void TryFiringAdapterAdded(); void AdapterAddedReceived(); + void FireAdapterStateChanged(bool aEnable); + nsresult EnableDisable(bool aEnable, + BluetoothReplyRunnable* aRunnable); + + /** + * Platform specific startup functions go here. Usually deals with member + * variables, so not static. Guaranteed to be called outside of main thread. + * + * @return NS_OK on correct startup, NS_ERROR_FAILURE otherwise + */ + virtual nsresult + StartInternal(BluetoothReplyRunnable* aRunnable) = 0; + + /** + * Platform specific startup functions go here. Usually deals with member + * variables, so not static. Guaranteed to be called outside of main thread. + * + * @return NS_OK on correct startup, NS_ERROR_FAILURE otherwise + */ + virtual nsresult + StopInternal(BluetoothReplyRunnable* aRunnable) = 0; + protected: BluetoothService() : mEnabled(false) , mAdapterAddedReceived(false) @@ -330,31 +352,15 @@ protected: Cleanup(); nsresult - StartBluetooth(bool aIsStartup); + StartBluetooth(bool aIsStartup, BluetoothReplyRunnable* aRunnable); nsresult - StopBluetooth(bool aIsStartup); + StopBluetooth(bool aIsStartup, BluetoothReplyRunnable* aRunnable); nsresult - StartStopBluetooth(bool aStart, bool aIsStartup); - - /** - * Platform specific startup functions go here. Usually deals with member - * variables, so not static. Guaranteed to be called outside of main thread. - * - * @return NS_OK on correct startup, NS_ERROR_FAILURE otherwise - */ - virtual nsresult - StartInternal() = 0; - - /** - * Platform specific startup functions go here. Usually deals with member - * variables, so not static. Guaranteed to be called outside of main thread. - * - * @return NS_OK on correct startup, NS_ERROR_FAILURE otherwise - */ - virtual nsresult - StopInternal() = 0; + StartStopBluetooth(bool aStart, + bool aIsStartup, + BluetoothReplyRunnable* aRunnable); /** * Called when XPCOM first creates this service. diff --git a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp index 4d6b8b26d93b..21e6975b7a4b 100644 --- a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp +++ b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp @@ -33,34 +33,47 @@ #include "mozilla/StaticPtr.h" #include "mozilla/unused.h" +#define ENSURE_BLUETOOTH_IS_READY(runnable, result) \ + do { \ + if (!sBtInterface || !IsEnabled()) { \ + NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth is not ready"); \ + DispatchBluetoothReply(runnable, BluetoothValue(), errorStr); \ + return result; \ + } \ + } while(0) + using namespace mozilla; using namespace mozilla::ipc; USING_BLUETOOTH_NAMESPACE -/** - * Static variables - */ -static bluetooth_device_t* sBtDevice; -static const bt_interface_t* sBtInterface; -static bool sAdapterDiscoverable = false; -static bool sIsBtEnabled = false; +// TODO: Non thread-safe static variables static nsString sAdapterBdAddress; static nsString sAdapterBdName; -static uint32_t sAdapterDiscoverableTimeout; static InfallibleTArray sAdapterBondedAddressArray; -static InfallibleTArray sRemoteDevicesPack; +static nsTArray > sChangeAdapterStateRunnableArray; + +// Static variables below should only be used on *main thread* +static const bt_interface_t* sBtInterface; static nsTArray > sControllerArray; -static nsTArray > sBondingRunnableArray; -static nsTArray > sChangeDiscoveryRunnableArray; -static nsTArray > sGetDeviceRunnableArray; -static nsTArray > sSetPropertyRunnableArray; -static nsTArray > sUnbondingRunnableArray; static nsTArray sRequestedDeviceCountArray; +static nsTArray > sChangeDiscoveryRunnableArray; +static nsTArray > sSetPropertyRunnableArray; +static nsTArray > sGetDeviceRunnableArray; +static nsTArray > sBondingRunnableArray; +static nsTArray > sUnbondingRunnableArray; + +// Static variables below should only be used on *callback thread* + + +// Atomic static variables +static Atomic sAdapterDiscoverable(false); +static Atomic sAdapterDiscoverableTimeout(0); /** * Classes only used in this file */ -class DistributeBluetoothSignalTask : public nsRunnable { +class DistributeBluetoothSignalTask MOZ_FINAL : public nsRunnable +{ public: DistributeBluetoothSignalTask(const BluetoothSignal& aSignal) : mSignal(aSignal) @@ -84,12 +97,9 @@ private: BluetoothSignal mSignal; }; -class SetupAfterEnabledTask : public nsRunnable +class SetupAfterEnabledTask MOZ_FINAL : public nsRunnable { public: - SetupAfterEnabledTask() - { } - NS_IMETHOD Run() { @@ -135,17 +145,37 @@ public: } }; -class CleanupTask : public nsRunnable +class CleanupTask MOZ_FINAL : public nsRunnable { public: - CleanupTask() - { } - NS_IMETHOD Run() { MOZ_ASSERT(NS_IsMainThread()); + /* + * Cleanup static adapter properties and notify adapter to clean them + * + * TODO: clean up and notify Discovering also + */ + sAdapterBdAddress.Truncate(); + sAdapterBdName.Truncate(); + sAdapterDiscoverable = false; + + InfallibleTArray props; + BT_APPEND_NAMED_VALUE(props, "Name", sAdapterBdName); + BT_APPEND_NAMED_VALUE(props, "Address", sAdapterBdAddress); + BT_APPEND_NAMED_VALUE(props, "Discoverable", + BluetoothValue(sAdapterDiscoverable)); + BluetoothValue value(props); + BluetoothSignal signal(NS_LITERAL_STRING("PropertyChanged"), + NS_LITERAL_STRING(KEY_ADAPTER), value); + + BluetoothService* bs = BluetoothService::Get(); + NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE); + + bs->DistributeSignal(signal); + // Cleanup bluetooth interfaces after BT state becomes BT_STATE_OFF. BluetoothHfpManager::DeinitHfpInterface(); BluetoothA2dpManager::DeinitA2dpInterface(); @@ -270,45 +300,69 @@ PlayStatusStringToControlPlayStatus(const nsAString& aPlayStatus) return playStatus; } -static bool -IsReady() -{ - if (!sBtInterface || !sIsBtEnabled) { - BT_LOGR("Warning! Bluetooth Service is not ready"); - return false; - } - return true; -} - +/** + * Bluedroid HAL callback functions + * + * Several callbacks are dispatched to main thread to avoid racing issues. + */ static void AdapterStateChangeCallback(bt_state_t aStatus) { MOZ_ASSERT(!NS_IsMainThread()); - BT_LOGR("BT_STATE %d", aStatus); + BT_LOGR("BT_STATE: %d", aStatus); - sIsBtEnabled = (aStatus == BT_STATE_ON); + bool isBtEnabled = (aStatus == BT_STATE_ON); - if (!sIsBtEnabled && NS_FAILED(NS_DispatchToMainThread(new CleanupTask()))) { + if (!isBtEnabled && + NS_FAILED(NS_DispatchToMainThread(new CleanupTask()))) { BT_WARNING("Failed to dispatch to main thread!"); + return; } nsRefPtr runnable = - new BluetoothService::ToggleBtAck(sIsBtEnabled); + new BluetoothService::ToggleBtAck(isBtEnabled); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { BT_WARNING("Failed to dispatch to main thread!"); return; } - if (sIsBtEnabled && + if (isBtEnabled && NS_FAILED(NS_DispatchToMainThread(new SetupAfterEnabledTask()))) { BT_WARNING("Failed to dispatch to main thread!"); + return; + } + + // Resolve promise if existed + if(!sChangeAdapterStateRunnableArray.IsEmpty()) { + DispatchBluetoothReply(sChangeAdapterStateRunnableArray[0], + BluetoothValue(true), + EmptyString()); + sChangeAdapterStateRunnableArray.RemoveElementAt(0); } } +class AdapterPropertiesCallbackTask MOZ_FINAL : public nsRunnable +{ +public: + NS_IMETHOD + Run() + { + MOZ_ASSERT(NS_IsMainThread()); + + if (!sSetPropertyRunnableArray.IsEmpty()) { + DispatchBluetoothReply(sSetPropertyRunnableArray[0], + BluetoothValue(true), EmptyString()); + sSetPropertyRunnableArray.RemoveElementAt(0); + } + + return NS_OK; + } +}; + /** * AdapterPropertiesCallback will be called after enable() but before - * AdapterStateChangeCallback sIsBtEnabled get updated. At that moment, both + * AdapterStateChangeCallback is called. At that moment, both * BluetoothManager/BluetoothAdapter does not register observer yet. */ static void @@ -385,14 +439,64 @@ AdapterPropertiesCallback(bt_status_t aStatus, int aNumProperties, BT_WARNING("Failed to dispatch to main thread!"); } - // bluedroid BTU task was stored in the task queue, see GKI_send_msg - if (!sSetPropertyRunnableArray.IsEmpty()) { - DispatchBluetoothReply(sSetPropertyRunnableArray[0], BluetoothValue(true), - EmptyString()); - sSetPropertyRunnableArray.RemoveElementAt(0); - } + // Redirect to main thread to avoid racing problem + NS_DispatchToMainThread(new AdapterPropertiesCallbackTask()); } +class RemoteDevicePropertiesCallbackTask : public nsRunnable +{ + const InfallibleTArray mProps; + nsString mRemoteDeviceBdAddress; +public: + RemoteDevicePropertiesCallbackTask( + const InfallibleTArray& aProps, + const nsAString& aRemoteDeviceBdAddress) + : mProps(aProps) + , mRemoteDeviceBdAddress(aRemoteDeviceBdAddress) + { } + + NS_IMETHOD + Run() + { + MOZ_ASSERT(NS_IsMainThread()); + + if (sRequestedDeviceCountArray.IsEmpty()) { + // This is possible because the callback would be called after turning + // Bluetooth on. + return NS_OK; + } + + // Update to registered BluetoothDevice objects + BluetoothSignal signal(NS_LITERAL_STRING("PropertyChanged"), + mRemoteDeviceBdAddress, mProps); + nsRefPtr + t = new DistributeBluetoothSignalTask(signal); + if (NS_FAILED(NS_DispatchToMainThread(t))) { + BT_WARNING("Failed to dispatch to main thread!"); + return NS_OK; + } + + static InfallibleTArray sRemoteDevicesPack; + + // Use address as the index + sRemoteDevicesPack.AppendElement( + BluetoothNamedValue(mRemoteDeviceBdAddress, mProps)); + + if (--sRequestedDeviceCountArray[0] == 0) { + if (!sGetDeviceRunnableArray.IsEmpty()) { + DispatchBluetoothReply(sGetDeviceRunnableArray[0], + sRemoteDevicesPack, EmptyString()); + sGetDeviceRunnableArray.RemoveElementAt(0); + } + + sRequestedDeviceCountArray.RemoveElementAt(0); + sRemoteDevicesPack.Clear(); + } + + return NS_OK; + } +}; + /** * RemoteDevicePropertiesCallback will be called, as the following conditions: * 1. When BT is turning on, bluedroid automatically execute this callback @@ -404,13 +508,6 @@ RemoteDevicePropertiesCallback(bt_status_t aStatus, bt_bdaddr_t *aBdAddress, { MOZ_ASSERT(!NS_IsMainThread()); - if (sRequestedDeviceCountArray.IsEmpty()) { - MOZ_ASSERT(sGetDeviceRunnableArray.IsEmpty()); - return; - } - - sRequestedDeviceCountArray[0]--; - InfallibleTArray props; nsString remoteDeviceBdAddress; @@ -435,36 +532,9 @@ RemoteDevicePropertiesCallback(bt_status_t aStatus, bt_bdaddr_t *aBdAddress, } } - // Update to registered BluetoothDevice objects - BluetoothSignal signal(NS_LITERAL_STRING("PropertyChanged"), - remoteDeviceBdAddress, props); - nsRefPtr - t = new DistributeBluetoothSignalTask(signal); - if (NS_FAILED(NS_DispatchToMainThread(t))) { - BT_WARNING("Failed to dispatch to main thread!"); - } - - // Use address as the index - sRemoteDevicesPack.AppendElement( - BluetoothNamedValue(remoteDeviceBdAddress, props)); - - if (sRequestedDeviceCountArray[0] == 0) { - MOZ_ASSERT(!sGetDeviceRunnableArray.IsEmpty()); - - if (sGetDeviceRunnableArray.IsEmpty()) { - BT_LOGR("No runnable to return"); - return; - } - - DispatchBluetoothReply(sGetDeviceRunnableArray[0], - sRemoteDevicesPack, EmptyString()); - - // After firing it, clean up cache - sRemoteDevicesPack.Clear(); - - sRequestedDeviceCountArray.RemoveElementAt(0); - sGetDeviceRunnableArray.RemoveElementAt(0); - } + // Redirect to main thread to avoid racing problem + NS_DispatchToMainThread( + new RemoteDevicePropertiesCallbackTask(props, remoteDeviceBdAddress)); } static void @@ -511,18 +581,33 @@ DeviceFoundCallback(int aNumProperties, bt_property_t *aProperties) } } +class DiscoveryStateChangedCallbackTask MOZ_FINAL : public nsRunnable +{ +public: + NS_IMETHOD + Run() + { + MOZ_ASSERT(NS_IsMainThread()); + + if (!sChangeDiscoveryRunnableArray.IsEmpty()) { + BluetoothValue values(true); + DispatchBluetoothReply(sChangeDiscoveryRunnableArray[0], + values, EmptyString()); + + sChangeDiscoveryRunnableArray.RemoveElementAt(0); + } + + return NS_OK; + } +}; + static void DiscoveryStateChangedCallback(bt_discovery_state_t aState) { MOZ_ASSERT(!NS_IsMainThread()); - if (!sChangeDiscoveryRunnableArray.IsEmpty()) { - BluetoothValue values(true); - DispatchBluetoothReply(sChangeDiscoveryRunnableArray[0], - values, EmptyString()); - - sChangeDiscoveryRunnableArray.RemoveElementAt(0); - } + // Redirect to main thread to avoid racing problem + NS_DispatchToMainThread(new DiscoveryStateChangedCallbackTask()); } static void @@ -581,26 +666,75 @@ SspRequestCallback(bt_bdaddr_t* aRemoteBdAddress, bt_bdname_t* aRemoteBdName, } } +class BondStateChangedCallbackTask : public nsRunnable +{ + nsString mRemoteDeviceBdAddress; + bool mBonded; +public: + BondStateChangedCallbackTask(const nsAString& aRemoteDeviceBdAddress, + bool aBonded) + : mRemoteDeviceBdAddress(aRemoteDeviceBdAddress) + , mBonded(aBonded) + { } + + NS_IMETHOD + Run() + { + MOZ_ASSERT(NS_IsMainThread()); + + if (mBonded && !sBondingRunnableArray.IsEmpty()) { + DispatchBluetoothReply(sBondingRunnableArray[0], + BluetoothValue(true), EmptyString()); + + sBondingRunnableArray.RemoveElementAt(0); + } else if (!mBonded && !sUnbondingRunnableArray.IsEmpty()) { + DispatchBluetoothReply(sUnbondingRunnableArray[0], + BluetoothValue(true), EmptyString()); + + sUnbondingRunnableArray.RemoveElementAt(0); + } + + // Update bonding status to gaia + InfallibleTArray propertiesArray; + BT_APPEND_NAMED_VALUE(propertiesArray, "address", mRemoteDeviceBdAddress); + BT_APPEND_NAMED_VALUE(propertiesArray, "status", mBonded); + + BluetoothSignal signal(NS_LITERAL_STRING(PAIRED_STATUS_CHANGED_ID), + NS_LITERAL_STRING(KEY_ADAPTER), + BluetoothValue(propertiesArray)); + NS_DispatchToMainThread(new DistributeBluetoothSignalTask(signal)); + + return NS_OK; + } +}; + static void BondStateChangedCallback(bt_status_t aStatus, bt_bdaddr_t* aRemoteBdAddress, bt_bond_state_t aState) { MOZ_ASSERT(!NS_IsMainThread()); - nsAutoString remoteAddress; - BdAddressTypeToString(aRemoteBdAddress, remoteAddress); + if (aState == BT_BOND_STATE_BONDING) { + // No need to handle bonding state + return; + } + + nsAutoString remoteBdAddress; + BdAddressTypeToString(aRemoteBdAddress, remoteBdAddress); + + if (aState == BT_BOND_STATE_BONDED && + sAdapterBondedAddressArray.Contains(remoteBdAddress)) { + // See bug 940271 for more details about this case. + return; + } - // We don't need to handle bonding state - NS_ENSURE_TRUE_VOID(aState != BT_BOND_STATE_BONDING); - NS_ENSURE_FALSE_VOID(aState == BT_BOND_STATE_BONDED && - sAdapterBondedAddressArray.Contains(remoteAddress)); bool bonded; if (aState == BT_BOND_STATE_NONE) { bonded = false; - sAdapterBondedAddressArray.RemoveElement(remoteAddress); + sAdapterBondedAddressArray.RemoveElement(remoteBdAddress); } else if (aState == BT_BOND_STATE_BONDED) { bonded = true; - sAdapterBondedAddressArray.AppendElement(remoteAddress); + sAdapterBondedAddressArray.AppendElement(remoteBdAddress); } // Update bonded address list to BluetoothAdapter @@ -614,27 +748,9 @@ BondStateChangedCallback(bt_status_t aStatus, bt_bdaddr_t* aRemoteBdAddress, BluetoothValue(propertiesChangeArray)); NS_DispatchToMainThread(new DistributeBluetoothSignalTask(signal)); - // Update bonding status to gaia - InfallibleTArray propertiesArray; - BT_APPEND_NAMED_VALUE(propertiesArray, "address", remoteAddress); - BT_APPEND_NAMED_VALUE(propertiesArray, "status", bonded); - - BluetoothSignal newSignal(NS_LITERAL_STRING(PAIRED_STATUS_CHANGED_ID), - NS_LITERAL_STRING(KEY_ADAPTER), - BluetoothValue(propertiesArray)); - NS_DispatchToMainThread(new DistributeBluetoothSignalTask(newSignal)); - - if (bonded && !sBondingRunnableArray.IsEmpty()) { - DispatchBluetoothReply(sBondingRunnableArray[0], - BluetoothValue(true), EmptyString()); - - sBondingRunnableArray.RemoveElementAt(0); - } else if (!bonded && !sUnbondingRunnableArray.IsEmpty()) { - DispatchBluetoothReply(sUnbondingRunnableArray[0], - BluetoothValue(true), EmptyString()); - - sUnbondingRunnableArray.RemoveElementAt(0); - } + // Redirect to main thread to avoid racing problem + NS_DispatchToMainThread( + new BondStateChangedCallbackTask(remoteBdAddress, bonded)); } static void @@ -673,15 +789,17 @@ EnsureBluetoothHalLoad() { hw_module_t* module; hw_device_t* device; + int err = hw_get_module(BT_HARDWARE_MODULE_ID, (hw_module_t const**)&module); if (err != 0) { BT_LOGR("Error: %s", strerror(err)); return false; } module->methods->open(module, BT_HARDWARE_MODULE_ID, &device); - sBtDevice = (bluetooth_device_t *)device; - NS_ENSURE_TRUE(sBtDevice, false); - sBtInterface = sBtDevice->get_bluetooth_interface(); + bluetooth_device_t* btDevice = (bluetooth_device_t *)device; + NS_ENSURE_TRUE(btDevice, false); + + sBtInterface = btDevice->get_bluetooth_interface(); NS_ENSURE_TRUE(sBtInterface, false); return true; @@ -709,12 +827,16 @@ static nsresult StartStopGonkBluetooth(bool aShouldEnable) { MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE(sBtInterface, NS_ERROR_FAILURE); - if (sIsBtEnabled == aShouldEnable) { + BluetoothService* bs = BluetoothService::Get(); + NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE); + + if (bs->IsEnabled() == aShouldEnable) { // Keep current enable status nsRefPtr runnable = - new BluetoothService::ToggleBtAck(sIsBtEnabled); + new BluetoothService::ToggleBtAck(aShouldEnable); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { BT_WARNING("Failed to dispatch to main thread!"); } @@ -772,10 +894,15 @@ BluetoothServiceBluedroid::~BluetoothServiceBluedroid() } nsresult -BluetoothServiceBluedroid::StartInternal() +BluetoothServiceBluedroid::StartInternal(BluetoothReplyRunnable* aRunnable) { MOZ_ASSERT(NS_IsMainThread()); + // aRunnable will be a nullptr while startup + if(aRunnable) { + sChangeAdapterStateRunnableArray.AppendElement(aRunnable); + } + nsresult ret = StartStopGonkBluetooth(true); if (NS_FAILED(ret)) { nsRefPtr runnable = @@ -790,10 +917,15 @@ BluetoothServiceBluedroid::StartInternal() } nsresult -BluetoothServiceBluedroid::StopInternal() +BluetoothServiceBluedroid::StopInternal(BluetoothReplyRunnable* aRunnable) { MOZ_ASSERT(NS_IsMainThread()); + // aRunnable will be a nullptr during starup and shutdown + if(aRunnable) { + sChangeAdapterStateRunnableArray.AppendElement(aRunnable); + } + nsresult ret = StartStopGonkBluetooth(false); if (NS_FAILED(ret)) { nsRefPtr runnable = @@ -828,6 +960,11 @@ BluetoothServiceBluedroid::GetAdaptersInternal( uint32_t numAdapters = 1; // Bluedroid supports single adapter only for (uint32_t i = 0; i < numAdapters; i++) { + // Since Atomic<*> is not acceptable for BT_APPEND_NAMED_VALUE(), + // create another variable to store data. + bool discoverable = sAdapterDiscoverable; + uint32_t discoverableTimeout = sAdapterDiscoverableTimeout; + BluetoothValue properties = InfallibleTArray(); // TODO: Revise here based on new BluetoothAdapter interface @@ -836,9 +973,9 @@ BluetoothServiceBluedroid::GetAdaptersInternal( BT_APPEND_NAMED_VALUE(properties.get_ArrayOfBluetoothNamedValue(), "Name", sAdapterBdName); BT_APPEND_NAMED_VALUE(properties.get_ArrayOfBluetoothNamedValue(), - "Discoverable", sAdapterDiscoverable); + "Discoverable", discoverable); BT_APPEND_NAMED_VALUE(properties.get_ArrayOfBluetoothNamedValue(), - "DiscoverableTimeout", sAdapterDiscoverableTimeout); + "DiscoverableTimeout", discoverableTimeout); BT_APPEND_NAMED_VALUE(properties.get_ArrayOfBluetoothNamedValue(), "Devices", sAdapterBondedAddressArray); @@ -856,11 +993,7 @@ BluetoothServiceBluedroid::GetConnectedDevicePropertiesInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); BluetoothProfileManagerBase* profile = BluetoothUuidHelper::GetBluetoothProfileManager(aServiceUuid); @@ -910,11 +1043,7 @@ BluetoothServiceBluedroid::GetPairedDevicePropertiesInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); int requestedDeviceCount = aDeviceAddress.Length(); if (requestedDeviceCount == 0) { @@ -947,12 +1076,8 @@ BluetoothServiceBluedroid::StartDiscoveryInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); - return NS_OK; - } int ret = sBtInterface->start_discovery(); if (ret != BT_STATUS_SUCCESS) { ReplyStatusError(aRunnable, ret, NS_LITERAL_STRING("StartDiscovery")); @@ -970,11 +1095,7 @@ BluetoothServiceBluedroid::StopDiscoveryInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); int ret = sBtInterface->cancel_discovery(); if (ret != BT_STATUS_SUCCESS) { @@ -994,12 +1115,7 @@ BluetoothServiceBluedroid::SetProperty(BluetoothObjectType aType, { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); const nsString propName = aValue.name(); bt_property_t prop; @@ -1072,11 +1188,7 @@ BluetoothServiceBluedroid::CreatePairedDeviceInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); bt_bdaddr_t remoteAddress; StringToBdAddressType(aDeviceAddress, &remoteAddress); @@ -1097,11 +1209,7 @@ BluetoothServiceBluedroid::RemoveDeviceInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return NS_OK; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, NS_OK); bt_bdaddr_t remoteAddress; StringToBdAddressType(aDeviceAddress, &remoteAddress); @@ -1124,11 +1232,7 @@ BluetoothServiceBluedroid::SetPinCodeInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return false; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, false); bt_bdaddr_t remoteAddress; StringToBdAddressType(aDeviceAddress, &remoteAddress); @@ -1161,11 +1265,7 @@ BluetoothServiceBluedroid::SetPairingConfirmationInternal( { MOZ_ASSERT(NS_IsMainThread()); - if (!IsReady()) { - NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth service is not ready yet!"); - DispatchBluetoothReply(aRunnable, BluetoothValue(), errorStr); - return false; - } + ENSURE_BLUETOOTH_IS_READY(aRunnable, false); bt_bdaddr_t remoteAddress; StringToBdAddressType(aDeviceAddress, &remoteAddress); diff --git a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.h b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.h index ea66b3f76e74..1e7fb6ea14be 100644 --- a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.h +++ b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.h @@ -22,8 +22,8 @@ public: BluetoothServiceBluedroid(); ~BluetoothServiceBluedroid(); - virtual nsresult StartInternal(); - virtual nsresult StopInternal(); + virtual nsresult StartInternal(BluetoothReplyRunnable* aRunnable); + virtual nsresult StopInternal(BluetoothReplyRunnable* aRunnable); virtual nsresult GetAdaptersInternal(BluetoothReplyRunnable* aRunnable); diff --git a/dom/bluetooth2/bluez/BluetoothDBusService.cpp b/dom/bluetooth2/bluez/BluetoothDBusService.cpp index 0de8fa2e63d8..ce22392805df 100644 --- a/dom/bluetooth2/bluez/BluetoothDBusService.cpp +++ b/dom/bluetooth2/bluez/BluetoothDBusService.cpp @@ -2097,8 +2097,10 @@ public: }; nsresult -BluetoothDBusService::StartInternal() +BluetoothDBusService::StartInternal(BluetoothReplyRunnable* aRunnable) { + MOZ_ASSERT(!aRunnable); + nsRefPtr runnable = new StartBluetoothRunnable(); nsresult rv = DispatchToBtThread(runnable); if (NS_FAILED(rv)) { @@ -2225,8 +2227,10 @@ public: }; nsresult -BluetoothDBusService::StopInternal() +BluetoothDBusService::StopInternal(BluetoothReplyRunnable* aRunnable) { + MOZ_ASSERT(!aRunnable); + nsRefPtr runnable = new StopBluetoothRunnable(); nsresult rv = DispatchToBtThread(runnable); if (NS_FAILED(rv)) { diff --git a/dom/bluetooth2/bluez/BluetoothDBusService.h b/dom/bluetooth2/bluez/BluetoothDBusService.h index 6acbebf7e5f5..8ffb264d8d0b 100644 --- a/dom/bluetooth2/bluez/BluetoothDBusService.h +++ b/dom/bluetooth2/bluez/BluetoothDBusService.h @@ -47,9 +47,9 @@ public: bool IsReady(); - virtual nsresult StartInternal() MOZ_OVERRIDE; + virtual nsresult StartInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; - virtual nsresult StopInternal() MOZ_OVERRIDE; + virtual nsresult StopInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; virtual nsresult GetAdaptersInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; diff --git a/dom/bluetooth2/ipc/BluetoothParent.cpp b/dom/bluetooth2/ipc/BluetoothParent.cpp index eb4bf871a410..ca71a08debe0 100644 --- a/dom/bluetooth2/ipc/BluetoothParent.cpp +++ b/dom/bluetooth2/ipc/BluetoothParent.cpp @@ -192,6 +192,10 @@ BluetoothParent::RecvPBluetoothRequestConstructor( switch (aRequest.type()) { case Request::TGetAdaptersRequest: return actor->DoRequest(aRequest.get_GetAdaptersRequest()); + case Request::TStartBluetoothRequest: + return actor->DoRequest(aRequest.get_StartBluetoothRequest()); + case Request::TStopBluetoothRequest: + return actor->DoRequest(aRequest.get_StopBluetoothRequest()); case Request::TSetPropertyRequest: return actor->DoRequest(aRequest.get_SetPropertyRequest()); case Request::TStartDiscoveryRequest: @@ -321,6 +325,30 @@ BluetoothRequestParent::DoRequest(const GetAdaptersRequest& aRequest) return true; } +bool +BluetoothRequestParent::DoRequest(const StartBluetoothRequest& aRequest) +{ + MOZ_ASSERT(mService); + MOZ_ASSERT(mRequestType == Request::TStartBluetoothRequest); + + nsresult rv = mService->StartInternal(mReplyRunnable.get()); + NS_ENSURE_SUCCESS(rv, false); + + return true; +} + +bool +BluetoothRequestParent::DoRequest(const StopBluetoothRequest& aRequest) +{ + MOZ_ASSERT(mService); + MOZ_ASSERT(mRequestType == Request::TStopBluetoothRequest); + + nsresult rv = mService->StopInternal(mReplyRunnable.get()); + NS_ENSURE_SUCCESS(rv, false); + + return true; +} + bool BluetoothRequestParent::DoRequest(const SetPropertyRequest& aRequest) { diff --git a/dom/bluetooth2/ipc/BluetoothParent.h b/dom/bluetooth2/ipc/BluetoothParent.h index fcf022ce57b4..694f17f33733 100644 --- a/dom/bluetooth2/ipc/BluetoothParent.h +++ b/dom/bluetooth2/ipc/BluetoothParent.h @@ -128,6 +128,12 @@ protected: bool DoRequest(const GetAdaptersRequest& aRequest); + bool + DoRequest(const StartBluetoothRequest& aRequest); + + bool + DoRequest(const StopBluetoothRequest& aRequest); + bool DoRequest(const SetPropertyRequest& aRequest); diff --git a/dom/bluetooth2/ipc/BluetoothServiceChildProcess.cpp b/dom/bluetooth2/ipc/BluetoothServiceChildProcess.cpp index 3f5a33260a23..fe3d6a467d75 100644 --- a/dom/bluetooth2/ipc/BluetoothServiceChildProcess.cpp +++ b/dom/bluetooth2/ipc/BluetoothServiceChildProcess.cpp @@ -103,6 +103,20 @@ BluetoothServiceChildProcess::GetAdaptersInternal( return NS_OK; } +nsresult +BluetoothServiceChildProcess::StartInternal(BluetoothReplyRunnable* aRunnable) +{ + SendRequest(aRunnable, StartBluetoothRequest()); + return NS_OK; +} + +nsresult +BluetoothServiceChildProcess::StopInternal(BluetoothReplyRunnable* aRunnable) +{ + SendRequest(aRunnable, StopBluetoothRequest()); + return NS_OK; +} + nsresult BluetoothServiceChildProcess::GetConnectedDevicePropertiesInternal( uint16_t aServiceUuid, @@ -377,18 +391,6 @@ BluetoothServiceChildProcess::HandleShutdown() return NS_OK; } -nsresult -BluetoothServiceChildProcess::StartInternal() -{ - MOZ_CRASH("This should never be called!"); -} - -nsresult -BluetoothServiceChildProcess::StopInternal() -{ - MOZ_CRASH("This should never be called!"); -} - bool BluetoothServiceChildProcess::IsConnected(uint16_t aServiceUuid) { diff --git a/dom/bluetooth2/ipc/BluetoothServiceChildProcess.h b/dom/bluetooth2/ipc/BluetoothServiceChildProcess.h index 24e4279f2cba..0b484964b714 100644 --- a/dom/bluetooth2/ipc/BluetoothServiceChildProcess.h +++ b/dom/bluetooth2/ipc/BluetoothServiceChildProcess.h @@ -46,6 +46,12 @@ public: virtual nsresult GetAdaptersInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; + virtual nsresult + StartInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; + + virtual nsresult + StopInternal(BluetoothReplyRunnable* aRunnable) MOZ_OVERRIDE; + virtual nsresult GetPairedDevicePropertiesInternal(const nsTArray& aDeviceAddresses, BluetoothReplyRunnable* aRunnable) @@ -199,14 +205,6 @@ protected: HandleShutdown() MOZ_OVERRIDE; private: - // This method should never be called. - virtual nsresult - StartInternal() MOZ_OVERRIDE; - - // This method should never be called. - virtual nsresult - StopInternal() MOZ_OVERRIDE; - bool IsSignalRegistered(const nsAString& aNodeName) { return !!mBluetoothSignalObserverTable.Get(aNodeName); diff --git a/dom/bluetooth2/ipc/PBluetooth.ipdl b/dom/bluetooth2/ipc/PBluetooth.ipdl index 1d2b0cdeca37..64fda7b6961e 100644 --- a/dom/bluetooth2/ipc/PBluetooth.ipdl +++ b/dom/bluetooth2/ipc/PBluetooth.ipdl @@ -25,6 +25,14 @@ namespace bluetooth { struct GetAdaptersRequest { }; +struct StartBluetoothRequest +{ +}; + +struct StopBluetoothRequest +{ +}; + struct SetPropertyRequest { BluetoothObjectType type; @@ -166,6 +174,8 @@ struct SendPlayStatusRequest union Request { GetAdaptersRequest; + StartBluetoothRequest; + StopBluetoothRequest; SetPropertyRequest; GetPropertyRequest; StartDiscoveryRequest; diff --git a/dom/cellbroadcast/interfaces/nsIDOMMozCellBroadcastMessage.idl b/dom/cellbroadcast/interfaces/nsIDOMMozCellBroadcastMessage.idl index 6d1e1d338bb7..cdbf57b0e3d0 100644 --- a/dom/cellbroadcast/interfaces/nsIDOMMozCellBroadcastMessage.idl +++ b/dom/cellbroadcast/interfaces/nsIDOMMozCellBroadcastMessage.idl @@ -2,6 +2,7 @@ * 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 "domstubs.idl" #include "nsISupports.idl" interface nsIDOMMozCellBroadcastEtwsInfo; @@ -10,7 +11,7 @@ interface nsIDOMMozCellBroadcastEtwsInfo; * MozCellBroadcastMessage encapsulates Cell Broadcast short message service * (CBS) messages. */ -[scriptable, uuid(6abe65de-6729-41f7-906a-3f3a2dbe30ae)] +[scriptable, uuid(701e74a9-5fc4-4e2d-a324-9b7693395159)] interface nsIDOMMozCellBroadcastMessage : nsISupports { /** @@ -53,7 +54,7 @@ interface nsIDOMMozCellBroadcastMessage : nsISupports /** * System time stamp at receival. */ - readonly attribute jsval timestamp; // jsval is for Date. + readonly attribute DOMTimeStamp timestamp; /** * Additional ETWS-specific info. diff --git a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js index 68921c283ea3..f08daec368c7 100644 --- a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js +++ b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_etws.js @@ -196,7 +196,7 @@ function testReceiving_ETWS_Timestamp() { doTestHelper(pdu, testReceiving_ETWS_WarningType, function(message) { // Cell Broadcast messages do not contain a timestamp field (however, ETWS // does). We only check the timestamp doesn't go too far (60 seconds) here. - let msMessage = message.timestamp.getTime(); + let msMessage = message.timestamp; let msNow = Date.now(); ok(Math.abs(msMessage - msNow) < (1000 * 60), "message.timestamp"); }); diff --git a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js index 69e1ee471200..9d9aec2b7c3c 100644 --- a/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js +++ b/dom/cellbroadcast/tests/marionette/test_cellbroadcast_gsm.js @@ -361,7 +361,7 @@ function testReceiving_GSM_Timestamp() { doTestHelper(pdu, testReceiving_GSM_WarningType, function(message) { // Cell Broadcast messages do not contain a timestamp field (however, ETWS // does). We only check the timestamp doesn't go too far (60 seconds) here. - let msMessage = message.timestamp.getTime(); + let msMessage = message.timestamp; let msNow = Date.now(); ok(Math.abs(msMessage - msNow) < (1000 * 60), "message.timestamp"); }); diff --git a/dom/mobilemessage/moz.build b/dom/mobilemessage/moz.build index 01b112bdeeef..4c48b5e19cad 100644 --- a/dom/mobilemessage/moz.build +++ b/dom/mobilemessage/moz.build @@ -7,5 +7,3 @@ DIRS += ['interfaces', 'src'] TEST_DIRS += ['tests'] -if CONFIG['ENABLE_TESTS']: - XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell.ini'] diff --git a/dom/mobilemessage/tests/mochitest.ini b/dom/mobilemessage/tests/mochitest/mochitest.ini similarity index 100% rename from dom/mobilemessage/tests/mochitest.ini rename to dom/mobilemessage/tests/mochitest/mochitest.ini diff --git a/dom/mobilemessage/tests/test_sms_basics.html b/dom/mobilemessage/tests/mochitest/test_sms_basics.html similarity index 100% rename from dom/mobilemessage/tests/test_sms_basics.html rename to dom/mobilemessage/tests/mochitest/test_sms_basics.html diff --git a/dom/mobilemessage/tests/test_smsfilter.html b/dom/mobilemessage/tests/mochitest/test_smsfilter.html similarity index 100% rename from dom/mobilemessage/tests/test_smsfilter.html rename to dom/mobilemessage/tests/mochitest/test_smsfilter.html diff --git a/dom/mobilemessage/tests/moz.build b/dom/mobilemessage/tests/moz.build index 3cf16eb0cfe8..af5439a45fcb 100644 --- a/dom/mobilemessage/tests/moz.build +++ b/dom/mobilemessage/tests/moz.build @@ -4,5 +4,8 @@ # 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/. -MOCHITEST_MANIFESTS += ['mochitest.ini'] +MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini'] + +if CONFIG['ENABLE_TESTS']: + XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini'] diff --git a/dom/mobilemessage/tests/header_helpers.js b/dom/mobilemessage/tests/xpcshell/header_helpers.js similarity index 100% rename from dom/mobilemessage/tests/header_helpers.js rename to dom/mobilemessage/tests/xpcshell/header_helpers.js diff --git a/dom/mobilemessage/tests/test_mms_pdu_helper.js b/dom/mobilemessage/tests/xpcshell/test_mms_pdu_helper.js similarity index 100% rename from dom/mobilemessage/tests/test_mms_pdu_helper.js rename to dom/mobilemessage/tests/xpcshell/test_mms_pdu_helper.js diff --git a/dom/mobilemessage/tests/test_mms_service.js b/dom/mobilemessage/tests/xpcshell/test_mms_service.js similarity index 100% rename from dom/mobilemessage/tests/test_mms_service.js rename to dom/mobilemessage/tests/xpcshell/test_mms_service.js diff --git a/dom/mobilemessage/tests/test_smsservice_createsmsmessage.js b/dom/mobilemessage/tests/xpcshell/test_smsservice_createsmsmessage.js similarity index 100% rename from dom/mobilemessage/tests/test_smsservice_createsmsmessage.js rename to dom/mobilemessage/tests/xpcshell/test_smsservice_createsmsmessage.js diff --git a/dom/mobilemessage/tests/test_wsp_pdu_helper.js b/dom/mobilemessage/tests/xpcshell/test_wsp_pdu_helper.js similarity index 100% rename from dom/mobilemessage/tests/test_wsp_pdu_helper.js rename to dom/mobilemessage/tests/xpcshell/test_wsp_pdu_helper.js diff --git a/dom/mobilemessage/tests/xpcshell.ini b/dom/mobilemessage/tests/xpcshell/xpcshell.ini similarity index 79% rename from dom/mobilemessage/tests/xpcshell.ini rename to dom/mobilemessage/tests/xpcshell/xpcshell.ini index 8498ad4deef0..3f410502c5a5 100644 --- a/dom/mobilemessage/tests/xpcshell.ini +++ b/dom/mobilemessage/tests/xpcshell/xpcshell.ini @@ -1,9 +1,6 @@ [DEFAULT] head = header_helpers.js tail = -support-files = - test_sms_basics.html - test_smsfilter.html [test_smsservice_createsmsmessage.js] [test_wsp_pdu_helper.js] diff --git a/dom/system/gonk/RILContentHelper.js b/dom/system/gonk/RILContentHelper.js index 19ef2cd9b791..4cc9435eec7b 100644 --- a/dom/system/gonk/RILContentHelper.js +++ b/dom/system/gonk/RILContentHelper.js @@ -312,7 +312,7 @@ function CellBroadcastMessage(pdu) { this.language = pdu.language; this.body = pdu.fullBody; this.messageClass = pdu.messageClass; - this.timestamp = new Date(pdu.timestamp); + this.timestamp = pdu.timestamp; if (pdu.etws != null) { this.etws = new CellBroadcastEtwsInfo(pdu.etws); diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js index 5d205887ac3f..3e711a78bf35 100644 --- a/dom/system/gonk/ril_worker.js +++ b/dom/system/gonk/ril_worker.js @@ -12791,9 +12791,17 @@ SimRecordHelperObject.prototype = { let ICCUtilsHelper = this.context.ICCUtilsHelper; let RIL = this.context.RIL; + // TS 31.102, clause 4.2.18 EFAD + let mncLength = 0; + if (ad && ad[3]) { + mncLength = ad[3] & 0x0f; + if (mncLength != 0x02 && mncLength != 0x03) { + mncLength = 0; + } + } // The 4th byte of the response is the length of MNC. let mccMnc = ICCUtilsHelper.parseMccMncFromImsi(RIL.iccInfoPrivate.imsi, - ad && ad[3]); + mncLength); if (mccMnc) { RIL.iccInfo.mcc = mccMnc.mcc; RIL.iccInfo.mnc = mccMnc.mnc; @@ -14130,6 +14138,7 @@ ICCUtilsHelperObject.prototype = { * The imsi of icc. * @param mncLength [optional] * The length of mnc. + * Zero indicates we haven't got a valid mnc length. * * @return An object contains the parsing result of mcc and mnc. * Or null if any error occurred. diff --git a/dom/system/gonk/tests/test_ril_worker_icc.js b/dom/system/gonk/tests/test_ril_worker_icc.js index 3384b020cbb8..124c01b45a41 100644 --- a/dom/system/gonk/tests/test_ril_worker_icc.js +++ b/dom/system/gonk/tests/test_ril_worker_icc.js @@ -2504,7 +2504,7 @@ add_test(function test_reading_ad_and_parsing_mcc_mnc() { io.loadTransparentEF = function fakeLoadTransparentEF(options) { let ad = [0x00, 0x00, 0x00]; - if (mncLengthInEf) { + if (typeof mncLengthInEf === 'number') { ad.push(mncLengthInEf); } @@ -2531,9 +2531,20 @@ add_test(function test_reading_ad_and_parsing_mcc_mnc() { } do_test(undefined, "466923202422409", "466", "92" ); + do_test(0x00, "466923202422409", "466", "92" ); + do_test(0x01, "466923202422409", "466", "92" ); + do_test(0x02, "466923202422409", "466", "92" ); do_test(0x03, "466923202422409", "466", "923"); + do_test(0x04, "466923202422409", "466", "92" ); + do_test(0xff, "466923202422409", "466", "92" ); + do_test(undefined, "310260542718417", "310", "260"); + do_test(0x00, "310260542718417", "310", "260"); + do_test(0x01, "310260542718417", "310", "260"); do_test(0x02, "310260542718417", "310", "26" ); + do_test(0x03, "310260542718417", "310", "260"); + do_test(0x04, "310260542718417", "310", "260"); + do_test(0xff, "310260542718417", "310", "260"); run_next_test(); }); diff --git a/mobile/android/base/AppConstants.java.in b/mobile/android/base/AppConstants.java.in index ff97379a717b..0419fad30987 100644 --- a/mobile/android/base/AppConstants.java.in +++ b/mobile/android/base/AppConstants.java.in @@ -121,6 +121,13 @@ public class AppConstants { false; #endif + public static final boolean MOZ_LOCALE_SWITCHER = +#ifdef MOZ_LOCALE_SWITCHER + true; +#else + false; +#endif + public static final boolean MOZ_UPDATER = #ifdef MOZ_UPDATER true; diff --git a/mobile/android/base/BrowserLocaleManager.java b/mobile/android/base/BrowserLocaleManager.java index 572de5669410..eff725fde47d 100644 --- a/mobile/android/base/BrowserLocaleManager.java +++ b/mobile/android/base/BrowserLocaleManager.java @@ -75,6 +75,10 @@ public class BrowserLocaleManager implements LocaleManager { } } + public boolean isEnabled() { + return AppConstants.MOZ_LOCALE_SWITCHER; + } + /** * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} * stringifies as "es_ES". diff --git a/mobile/android/base/LocaleManager.java b/mobile/android/base/LocaleManager.java index bcc250c39de2..75ef750b70d8 100644 --- a/mobile/android/base/LocaleManager.java +++ b/mobile/android/base/LocaleManager.java @@ -18,6 +18,11 @@ import android.content.res.Resources; */ public interface LocaleManager { void initialize(Context context); + + /** + * @return true if locale switching is enabled. + */ + boolean isEnabled(); Locale getCurrentLocale(Context context); String getAndApplyPersistedLocale(Context context); void correctLocale(Context context, Resources resources, Configuration newConfig); diff --git a/mobile/android/base/Tabs.java b/mobile/android/base/Tabs.java index 16448eb3e20f..206ce27e594d 100644 --- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -188,13 +188,18 @@ public class Tabs implements GeckoEventListener { } } - private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate) { + private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) { final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) : new Tab(mAppContext, id, url, external, parentId, title); synchronized (this) { lazyRegisterBookmarkObserver(); mTabs.put(id, tab); - mOrder.add(tab); + + if (tabIndex > -1) { + mOrder.add(tabIndex, tab); + } else { + mOrder.add(tab); + } } // Suppress the ADDED event to prevent animation of tabs created via session restore. @@ -427,7 +432,8 @@ public class Tabs implements GeckoEventListener { tab = addTab(id, url, message.getBoolean("external"), message.getInt("parentId"), message.getString("title"), - message.getBoolean("isPrivate")); + message.getBoolean("isPrivate"), + message.getInt("tabIndex")); // If we added the tab as a stub, we should have already // selected it, so ignore this flag for stubbed tabs. @@ -799,7 +805,10 @@ public class Tabs implements GeckoEventListener { // long as it's a valid URI. String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null; - added = addTab(tabId, tabUrl, external, parentId, url, isPrivate); + // Add the new tab to the end of the tab order. + final int tabIndex = -1; + + added = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex); added.setDesktopMode(desktopMode); } } catch (Exception e) { diff --git a/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java b/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java index 7d6931b1e255..a0cad143710d 100644 --- a/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java @@ -6,13 +6,11 @@ package org.mozilla.gecko.fxa.activities; import java.util.Locale; -import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.setup.activities.ActivityUtils; import org.mozilla.gecko.sync.setup.activities.LocaleAware; diff --git a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java index 83e310719c16..4ed4dae2ff13 100644 --- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java @@ -18,6 +18,7 @@ import org.mozilla.gecko.fxa.login.Married; import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender; +import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; import org.mozilla.gecko.sync.SyncConfiguration; import android.accounts.Account; @@ -28,10 +29,13 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.text.TextUtils; /** * A fragment that displays the status of an AndroidFxAccount. @@ -39,7 +43,9 @@ import android.preference.PreferenceScreen; * The owning activity is responsible for providing an AndroidFxAccount at * appropriate times. */ -public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener { +public class FxAccountStatusFragment + extends PreferenceFragment + implements OnPreferenceClickListener, OnPreferenceChangeListener { private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); // When a checkbox is toggled, wait 5 seconds (for other checkbox actions) @@ -65,7 +71,12 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre protected CheckBoxPreference tabsPreference; protected CheckBoxPreference passwordsPreference; + protected EditTextPreference deviceNamePreference; + protected volatile AndroidFxAccount fxAccount; + // The contract is: when fxAccount is non-null, then clientsDataDelegate is + // non-null. If violated then an IllegalStateException is thrown. + protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate; // Used to post delayed sync requests. protected Handler handler; @@ -88,6 +99,10 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + addPreferences(); + } + + protected void addPreferences() { addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); emailPreference = ensureFindPreference("email"); @@ -119,6 +134,9 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre historyPreference.setOnPreferenceClickListener(this); tabsPreference.setOnPreferenceClickListener(this); passwordsPreference.setOnPreferenceClickListener(this); + + deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name"); + deviceNamePreference.setOnPreferenceChangeListener(this); } /** @@ -177,6 +195,8 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre historyPreference.setEnabled(enabled); tabsPreference.setEnabled(enabled); passwordsPreference.setEnabled(enabled); + // Since we can't sync, we can't update our remote client record. + deviceNamePreference.setEnabled(enabled); } /** @@ -294,6 +314,14 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre throw new IllegalArgumentException("fxAccount must not be null"); } this.fxAccount = fxAccount; + try { + this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs()); + } catch (Exception e) { + Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e); + // Something is terribly wrong; best to get a stack trace rather than + // continue with a null clients delegate. + throw new IllegalStateException(e); + } handler = new Handler(); // Attached to current (assumed to be UI) thread. @@ -319,6 +347,17 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); } + protected void hardRefresh() { + // This is the only way to guarantee that the EditText dialogs created by + // EditTextPreferences are re-created. This works around the issue described + // at http://androiddev.orkitra.com/?p=112079. + final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); + statusScreen.removeAll(); + addPreferences(); + + refresh(); + } + protected void refresh() { // refresh is called from our onResume, which can happen before the owning // Activity tells us about an account (via our public @@ -372,6 +411,10 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre // No matter our state, we should update the checkboxes. updateSelectedEngines(); } + + final String clientName = clientsDataDelegate.getClientName(); + deviceNamePreference.setSummary(clientName); + deviceNamePreference.setText(clientName); } /** @@ -571,4 +614,22 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre button.setOnPreferenceClickListener(listener); } } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == deviceNamePreference) { + String newClientName = (String) newValue; + if (TextUtils.isEmpty(newClientName)) { + newClientName = clientsDataDelegate.getDefaultClientName(); + } + final long now = System.currentTimeMillis(); + clientsDataDelegate.setClientName(newClientName, now); + requestDelayedSync(); // Try to update our remote client record. + hardRefresh(); // Updates the value displayed to the user, among other things. + return true; + } + + // For everything else, accept the change. + return true; + } } diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 16a3bb36c1d8..d2ac7ea9de37 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -357,7 +357,11 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { FxAccountGlobalSession globalSession = null; try { - ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); + final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'."); + FxAccountConstants.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp()); + } // We compute skew over time using SkewHandler. This yields an unchanging // skew adjustment that the HawkAuthHeaderProvider uses to adjust its diff --git a/mobile/android/base/locales/en-US/sync_strings.dtd b/mobile/android/base/locales/en-US/sync_strings.dtd index 15ec2b36f748..d6969be28b5d 100644 --- a/mobile/android/base/locales/en-US/sync_strings.dtd +++ b/mobile/android/base/locales/en-US/sync_strings.dtd @@ -175,6 +175,7 @@ + diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index 9ed07c80af30..f290d68fdbbc 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -131,6 +131,7 @@ OnSharedPreferenceChangeListener * Track the last locale so we know whether to redisplay. */ private Locale lastLocale = Locale.getDefault(); + private boolean localeSwitchingIsEnabled; private void updateActionBarTitle(int title) { if (Build.VERSION.SDK_INT >= 14) { @@ -268,6 +269,10 @@ OnSharedPreferenceChangeListener // Apply the current user-selected locale, if necessary. checkLocale(); + // Track this so we can decide whether to show locale options. + // See also the workaround below for Bug 1015209. + localeSwitchingIsEnabled = BrowserLocaleManager.getInstance().isEnabled(); + // For Android v11+ where we use Fragments (v11+ only due to bug 866352), // check that PreferenceActivity.EXTRA_SHOW_FRAGMENT has been set // (or set it) before super.onCreate() is called so Android can display @@ -283,10 +288,17 @@ OnSharedPreferenceChangeListener updateTitle(getString(R.string.pref_header_customize)); } - // So that Android doesn't put the fragment title (or nothing at - // all) in the action bar. if (onIsMultiPane()) { + // So that Android doesn't put the fragment title (or nothing at + // all) in the action bar. updateActionBarTitle(R.string.settings_title); + + if (Build.VERSION.SDK_INT < 13) { + // Affected by Bug 1015209 -- no detach/attach. + // If we try rejigging fragments, we'll crash, so don't + // enable locale switching at all. + localeSwitchingIsEnabled = false; + } } } @@ -397,6 +409,18 @@ OnSharedPreferenceChangeListener public void onBuildHeaders(List
target) { if (onIsMultiPane()) { loadHeadersFromResource(R.xml.preference_headers, target); + + // If locale switching is disabled, remove the section + // entirely. This logic will need to be extended when + // content language selection (Bug 881510) is implemented. + if (!localeSwitchingIsEnabled) { + for (Header header : target) { + if (header.id == R.id.pref_header_language) { + target.remove(header); + break; + } + } + } } } @@ -564,9 +588,20 @@ OnSharedPreferenceChangeListener private void setupPreferences(PreferenceGroup preferences, ArrayList prefs) { for (int i = 0; i < preferences.getPreferenceCount(); i++) { Preference pref = preferences.getPreference(i); + + // Eliminate locale switching if necessary. + // This logic will need to be extended when + // content language selection (Bug 881510) is implemented. + if (!localeSwitchingIsEnabled && + "preferences_locale".equals(pref.getExtras().getString("resource", null))) { + preferences.removePreference(pref); + i--; + continue; + } + String key = pref.getKey(); if (pref instanceof PreferenceGroup) { - // If no datareporting is enabled, remove UI. + // If datareporting is disabled, remove UI. if (PREFS_DATA_REPORTING_PREFERENCES.equals(key)) { if (!AppConstants.MOZ_DATA_REPORTING) { preferences.removePreference(pref); diff --git a/mobile/android/base/resources/xml-v11/preference_headers.xml b/mobile/android/base/resources/xml-v11/preference_headers.xml index 678d83dafebd..d45f023b856a 100644 --- a/mobile/android/base/resources/xml-v11/preference_headers.xml +++ b/mobile/android/base/resources/xml-v11/preference_headers.xml @@ -28,7 +28,8 @@
+ android:title="@string/pref_header_language" + android:id="@+id/pref_header_language">
diff --git a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml index 16e578cf18fd..b256c1409daf 100644 --- a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml +++ b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml @@ -66,6 +66,12 @@ android:key="passwords" android:persistent="false" android:title="@string/fxaccount_status_passwords" /> + + + * Changing the client's name should be reflected remotely, while changing the + * clients count should not (since that data is only used to inform local + * policy.) + * + * @return timestamp in milliseconds. + */ + public long getLastModifiedTimestamp(); } diff --git a/mobile/android/base/sync/repositories/android/FennecTabsRepository.java b/mobile/android/base/sync/repositories/android/FennecTabsRepository.java index d4cc48e963bd..3213dfbab960 100644 --- a/mobile/android/base/sync/repositories/android/FennecTabsRepository.java +++ b/mobile/android/base/sync/repositories/android/FennecTabsRepository.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.db.Tab; import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; import org.mozilla.gecko.sync.repositories.InactiveSessionException; import org.mozilla.gecko.sync.repositories.NoContentProviderException; import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; @@ -30,12 +31,10 @@ import android.net.Uri; import android.os.RemoteException; public class FennecTabsRepository extends Repository { - protected final String localClientName; - protected final String localClientGuid; + protected final ClientsDataDelegate clientsDataDelegate; - public FennecTabsRepository(final String localClientName, final String localClientGuid) { - this.localClientName = localClientName; - this.localClientGuid = localClientGuid; + public FennecTabsRepository(ClientsDataDelegate clientsDataDelegate) { + this.clientsDataDelegate = clientsDataDelegate; } /** @@ -144,14 +143,17 @@ public class FennecTabsRepository extends Repository { public void run() { // We fetch all local tabs (since the record must contain them all) // but only process the record if the timestamp is sufficiently - // recent. + // recent, or if the client data has been modified. try { final Cursor cursor = tabsHelper.safeQuery(tabsProvider, ".fetchSince()", null, localClientSelection, localClientSelectionArgs, positionAscending); try { + final String localClientGuid = clientsDataDelegate.getAccountGUID(); + final String localClientName = clientsDataDelegate.getClientName(); final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, localClientGuid, localClientName); - if (tabsRecord.lastModified >= timestamp) { + if (tabsRecord.lastModified >= timestamp || + clientsDataDelegate.getLastModifiedTimestamp() >= timestamp) { delegate.onFetchedRecord(tabsRecord); } } finally { diff --git a/mobile/android/base/sync/setup/activities/SendTabActivity.java b/mobile/android/base/sync/setup/activities/SendTabActivity.java index e35cae344362..880019a8a728 100644 --- a/mobile/android/base/sync/setup/activities/SendTabActivity.java +++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java @@ -25,12 +25,9 @@ import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConstants; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; -import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; -import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository.FennecTabsRepositorySession; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.setup.SyncAccounts; import org.mozilla.gecko.sync.setup.activities.LocaleAware.LocaleAwareActivity; -import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; import org.mozilla.gecko.sync.syncadapter.SyncAdapter; import android.accounts.Account; diff --git a/mobile/android/base/sync/stage/FennecTabsServerSyncStage.java b/mobile/android/base/sync/stage/FennecTabsServerSyncStage.java index c3731163a682..40a474ef47f6 100644 --- a/mobile/android/base/sync/stage/FennecTabsServerSyncStage.java +++ b/mobile/android/base/sync/stage/FennecTabsServerSyncStage.java @@ -4,7 +4,6 @@ package org.mozilla.gecko.sync.stage; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; import org.mozilla.gecko.sync.repositories.RecordFactory; import org.mozilla.gecko.sync.repositories.Repository; import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; @@ -31,8 +30,7 @@ public class FennecTabsServerSyncStage extends ServerSyncStage { @Override protected Repository getLocalRepository() { - final ClientsDataDelegate clientsDelegate = session.getClientsDelegate(); - return new FennecTabsRepository(clientsDelegate.getClientName(), clientsDelegate.getAccountGUID()); + return new FennecTabsRepository(session.getClientsDelegate()); } @Override diff --git a/mobile/android/base/sync/stage/SyncClientsEngineStage.java b/mobile/android/base/sync/stage/SyncClientsEngineStage.java index 3fd9a4d8d89b..ffad3272f6b5 100644 --- a/mobile/android/base/sync/stage/SyncClientsEngineStage.java +++ b/mobile/android/base/sync/stage/SyncClientsEngineStage.java @@ -397,6 +397,11 @@ public class SyncClientsEngineStage extends AbstractSessionManagingSyncStage { return true; } + if (session.getClientsDelegate().getLastModifiedTimestamp() > lastUpload) { + // Something's changed locally since we last uploaded. + return true; + } + // Note the opportunity for clock drift problems here. // TODO: if we track download times, we can use the timestamp of most // recent download response instead of the current time. diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 9f59e4edb9ab..f9ec5d116010 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -921,7 +921,12 @@ var BrowserApp = { aParams = aParams || {}; let newTab = new Tab(aURI, aParams); - this._tabs.push(newTab); + + if (typeof aParams.tabIndex == "number") { + this._tabs.splice(aParams.tabIndex, 0, newTab); + } else { + this._tabs.push(newTab); + } let selected = "selected" in aParams ? aParams.selected : true; if (selected) @@ -972,17 +977,23 @@ var BrowserApp = { if (aTab == this.selectedTab) this.selectedTab = null; + let tabIndex = this._tabs.indexOf(aTab); + let evt = document.createEvent("UIEvents"); - evt.initUIEvent("TabClose", true, false, window, null); + evt.initUIEvent("TabClose", true, false, window, tabIndex); aTab.browser.dispatchEvent(evt); - // Get a title for the undo close toast. Fall back to the URL if there is no title. - let title = aTab.browser.contentDocument.title || aTab.browser.contentDocument.URL; - aTab.destroy(); - this._tabs.splice(this._tabs.indexOf(aTab), 1); + this._tabs.splice(tabIndex, 1); if (aShowUndoToast) { + // Get a title for the undo close toast. Fall back to the URL if there is no title. + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + let closedTabData = ss.getClosedTabs(window)[0]; + + let historyEntry = closedTabData.entries[closedTabData.index - 1]; + let title = historyEntry.title || historyEntry.url; + let message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1); NativeWindow.toast.show(message, "short", { button: { @@ -990,7 +1001,6 @@ var BrowserApp = { label: Strings.browser.GetStringFromName("undoCloseToast.action2"), callback: function() { UITelemetry.addEvent("undo.1", "toast", null, "closetab"); - let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); ss.undoCloseTab(window, 0); } } @@ -3050,6 +3060,7 @@ Tab.prototype = { tabID: this.id, uri: uri, parentId: ("parentId" in aParams) ? aParams.parentId : -1, + tabIndex: ("tabIndex" in aParams) ? aParams.tabIndex : -1, external: ("external" in aParams) ? aParams.external : false, selected: ("selected" in aParams) ? aParams.selected : true, title: title, diff --git a/mobile/android/components/SessionStore.idl b/mobile/android/components/SessionStore.idl index efc51e424b31..a9c3de05f4b5 100644 --- a/mobile/android/components/SessionStore.idl +++ b/mobile/android/components/SessionStore.idl @@ -14,7 +14,7 @@ interface nsIDOMNode; * tabs contained in them. */ -[scriptable, uuid(fe116b56-0226-4562-b52a-a623dad07ead)] +[scriptable, uuid(91eca9cf-6741-4c8f-a3a0-2e957240894d)] interface nsISessionStore : nsISupports { /** @@ -32,9 +32,9 @@ interface nsISessionStore : nsISupports * Get closed tab data * * @param aWindow is the browser window for which to get closed tab data - * @returns a JSON string representing the list of closed tabs. + * @returns a JS array of closed tabs. */ - AString getClosedTabData(in nsIDOMWindow aWindow); + jsval getClosedTabs(in nsIDOMWindow aWindow); /** * @param aWindow is the browser window to reopen a closed tab in. diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js index 5e5b1eb03da1..0a3d8097bd9a 100644 --- a/mobile/android/components/SessionStore.js +++ b/mobile/android/components/SessionStore.js @@ -46,6 +46,10 @@ SessionStore.prototype = { _maxTabsUndo: 1, _pendingWrite: 0, + // The index where the most recently closed tab was in the tabs array + // when it was closed. + _lastClosedTabIndex: -1, + init: function ss_init() { // Get file references this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); @@ -98,6 +102,8 @@ SessionStore.prototype = { for (let [ssid, win] in Iterator(this._windows)) win.closedTabs = []; + this._lastClosedTabIndex = -1; + if (this._loadState == STATE_RUNNING) { // Save the purged state immediately this.saveState(); @@ -164,7 +170,7 @@ SessionStore.prototype = { } case "TabClose": { let browser = aEvent.target; - this.onTabClose(window, browser); + this.onTabClose(window, browser, aEvent.detail); this.onTabRemove(window, browser); break; } @@ -269,7 +275,7 @@ SessionStore.prototype = { this.saveStateDelayed(); }, - onTabClose: function ss_onTabClose(aWindow, aBrowser) { + onTabClose: function ss_onTabClose(aWindow, aBrowser, aTabIndex) { if (this._maxTabsUndo == 0) return; @@ -283,6 +289,8 @@ SessionStore.prototype = { let length = this._windows[aWindow.__SSID].closedTabs.length; if (length > this._maxTabsUndo) this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); + + this._lastClosedTabIndex = aTabIndex; } }, @@ -818,11 +826,11 @@ SessionStore.prototype = { return this._windows[aWindow.__SSID].closedTabs.length; }, - getClosedTabData: function ss_getClosedTabData(aWindow) { + getClosedTabs: function ss_getClosedTabs(aWindow) { if (!aWindow.__SSID) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); - return JSON.stringify(this._windows[aWindow.__SSID].closedTabs); + return this._windows[aWindow.__SSID].closedTabs; }, undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { @@ -845,11 +853,14 @@ SessionStore.prototype = { let params = { selected: true, isPrivate: closedTab.isPrivate, - desktopMode: closedTab.desktopMode + desktopMode: closedTab.desktopMode, + tabIndex: this._lastClosedTabIndex }; let tab = aWindow.BrowserApp.addTab(closedTab.entries[closedTab.index - 1].url, params); this._restoreHistory(closedTab, tab.browser.sessionHistory); + this._lastClosedTabIndex = -1; + // Put back the extra data tab.browser.__SS_extdata = closedTab.extData; @@ -869,6 +880,11 @@ SessionStore.prototype = { // remove closed tab from the array closedTabs.splice(aIndex, 1); + + // Forget the last closed tab index if we're forgetting the last closed tab. + if (aIndex == 0) { + this._lastClosedTabIndex = -1; + } }, getTabValue: function ss_getTabValue(aTab, aKey) { diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh index 0c712354bc65..b6ce94ebdd79 100644 --- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -64,6 +64,9 @@ MOZ_SERVICES_FXACCOUNTS=1 # Enable Wifi-AP/cell tower data reporting MOZ_DATA_REPORTING=1 +# Enable runtime locale switching. +MOZ_LOCALE_SWITCHER=1 + # Enable the "synthetic APKs" implementation of Open Web Apps. MOZ_ANDROID_SYNTHAPKS=1 diff --git a/mobile/android/services/strings.xml.in b/mobile/android/services/strings.xml.in index e787bc3ec8ac..50f4f0c9ba31 100644 --- a/mobile/android/services/strings.xml.in +++ b/mobile/android/services/strings.xml.in @@ -169,6 +169,7 @@ &syncBrand.shortName.label; &fxaccount_status_header2; &fxaccount_status_signed_in_as; +&fxaccount_status_device_name; &fxaccount_status_sync; &fxaccount_status_sync_enabled; &fxaccount_status_needs_verification2; diff --git a/mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java b/mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java index a123dfb69c02..334515b9f566 100644 --- a/mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java +++ b/mobile/android/tests/background/junit3/src/db/TestFennecTabsRepositorySession.java @@ -7,6 +7,7 @@ import org.json.simple.JSONArray; import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; +import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.repositories.NoContentProviderException; import org.mozilla.gecko.sync.repositories.RepositorySession; @@ -24,8 +25,9 @@ import android.database.Cursor; import android.os.RemoteException; public class TestFennecTabsRepositorySession extends AndroidSyncTestCase { - public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces. - public static final String TEST_CLIENT_NAME = "test client name"; + public static final MockClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate(); + public static final String TEST_CLIENT_GUID = clientsDataDelegate.getAccountGUID(); + public static final String TEST_CLIENT_NAME = clientsDataDelegate.getClientName(); // Override these to test against data that is not live. public static final String TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS ?"; @@ -72,7 +74,7 @@ public class TestFennecTabsRepositorySession extends AndroidSyncTestCase { * Override this chain in order to avoid our test code having to create two * sessions all the time. */ - return new FennecTabsRepository(TEST_CLIENT_NAME, TEST_CLIENT_GUID) { + return new FennecTabsRepository(clientsDataDelegate) { @Override public void createSession(RepositorySessionCreationDelegate delegate, Context context) { @@ -197,8 +199,19 @@ public class TestFennecTabsRepositorySession extends AndroidSyncTestCase { // Not all tabs are modified after this, but the record should contain them all. performWait(fetchSinceRunnable(session, 1000, new Record[] { tabsRecord })); - // No tabs are modified after this, so we shouldn't get a record at all. - performWait(fetchSinceRunnable(session, 4000, new Record[] { })); + // No tabs are modified after this, but our client name has changed in the interim. + performWait(fetchSinceRunnable(session, 4000, new Record[] { tabsRecord })); + + // No tabs are modified after this, and our client name hasn't changed, so + // we shouldn't get a record at all. Note: this runs after our static + // initializer that sets the client data timestamp. + final long now = System.currentTimeMillis(); + performWait(fetchSinceRunnable(session, now, new Record[] { })); + + // No tabs are modified after this, but our client name has changed, so + // again we get a record. + clientsDataDelegate.setClientName("new client name", System.currentTimeMillis()); + performWait(fetchSinceRunnable(session, now, new Record[] { tabsRecord })); session.abort(); } diff --git a/mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java b/mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java index 5bf5071d9b02..8a74e79cd1cf 100644 --- a/mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java +++ b/mobile/android/tests/background/junit3/src/testhelpers/MockClientsDataDelegate.java @@ -10,6 +10,7 @@ public class MockClientsDataDelegate implements ClientsDataDelegate { private String accountGUID; private String clientName; private int clientsCount; + private long clientDataTimestamp = 0; @Override public synchronized String getAccountGUID() { @@ -19,10 +20,21 @@ public class MockClientsDataDelegate implements ClientsDataDelegate { return accountGUID; } + @Override + public synchronized String getDefaultClientName() { + return "Default client"; + } + + @Override + public synchronized void setClientName(String clientName, long now) { + this.clientName = clientName; + this.clientDataTimestamp = now; + } + @Override public synchronized String getClientName() { if (clientName == null) { - clientName = "Default Name"; + setClientName(getDefaultClientName(), System.currentTimeMillis()); } return clientName; } @@ -38,7 +50,12 @@ public class MockClientsDataDelegate implements ClientsDataDelegate { } @Override - public boolean isLocalGUID(String guid) { + public synchronized boolean isLocalGUID(String guid) { return getAccountGUID().equals(guid); } -} \ No newline at end of file + + @Override + public synchronized long getLastModifiedTimestamp() { + return clientDataTimestamp; + } +} diff --git a/testing/xpcshell/xpcshell_android.ini b/testing/xpcshell/xpcshell_android.ini index 6233d2853ccf..b13f7f8e4472 100644 --- a/testing/xpcshell/xpcshell_android.ini +++ b/testing/xpcshell/xpcshell_android.ini @@ -12,7 +12,7 @@ [include:dom/activities/tests/unit/xpcshell.ini] [include:dom/apps/tests/unit/xpcshell.ini] [include:dom/encoding/test/unit/xpcshell.ini] -[include:dom/mobilemessage/tests/xpcshell.ini] +[include:dom/mobilemessage/tests/xpcshell/xpcshell.ini] [include:dom/network/tests/unit/xpcshell.ini] [include:dom/payment/tests/unit/xpcshell.ini] [include:dom/permission/tests/unit/xpcshell.ini] diff --git a/testing/xpcshell/xpcshell_b2g.ini b/testing/xpcshell/xpcshell_b2g.ini index 5f797c8c3153..2850d08182db 100644 --- a/testing/xpcshell/xpcshell_b2g.ini +++ b/testing/xpcshell/xpcshell_b2g.ini @@ -3,7 +3,7 @@ ; file, You can obtain one at http://mozilla.org/MPL/2.0/. [include:dom/apps/tests/unit/xpcshell.ini] -[include:dom/mobilemessage/tests/xpcshell.ini] +[include:dom/mobilemessage/tests/xpcshell/xpcshell.ini] [include:dom/network/tests/unit_stats/xpcshell.ini] [include:dom/system/gonk/tests/xpcshell.ini] [include:dom/wappush/tests/xpcshell.ini] diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 8f7416a3cff3..f3284105c8e9 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -4143,6 +4143,60 @@ "n_values": 10, "description": "Track click count on about:newtab tiles per index (0-8). For non-default row or column configurations all clicks into the '9' bucket." }, + "NEWTAB_PAGE_DIRECTORY_LINK0_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #0 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK1_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #1 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK2_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #2 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK3_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #3 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK4_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #4 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK5_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #5 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK6_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #6 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK7_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #7 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, + "NEWTAB_PAGE_DIRECTORY_LINK8_SHOWN": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track impression count of directory link #8 per index (0-8). Grid configurations showing 9+ tiles fall into the '9' bucket." + }, "NEWTAB_PAGE_DIRECTORY_AFFILIATE_SHOWN": { "expires_in_version": "35", "kind": "enumerated", diff --git a/toolkit/devtools/LayoutHelpers.jsm b/toolkit/devtools/LayoutHelpers.jsm index 020f753c1827..67f77f700bd3 100644 --- a/toolkit/devtools/LayoutHelpers.jsm +++ b/toolkit/devtools/LayoutHelpers.jsm @@ -412,98 +412,4 @@ LayoutHelpers.prototype = { return [xOffset * scale, yOffset * scale]; }, - - - - /******************************************************************** - * GetBoxQuads POLYFILL START TODO: Remove this when bug 917755 is fixed. - ********************************************************************/ - _getBoxQuadsFromRect: function(rect, node) { - let scale = this.calculateScale(node); - let [xOffset, yOffset] = this._getNodeOffsets(node); - - let out = { - p1: { - x: rect.left * scale + xOffset, - y: rect.top * scale + yOffset - }, - p2: { - x: (rect.left + rect.width) * scale + xOffset, - y: rect.top * scale + yOffset - }, - p3: { - x: (rect.left + rect.width) * scale + xOffset, - y: (rect.top + rect.height) * scale + yOffset - }, - p4: { - x: rect.left * scale + xOffset, - y: (rect.top + rect.height) * scale + yOffset - } - }; - - out.bounds = { - bottom: out.p4.y, - height: out.p4.y - out.p1.y, - left: out.p1.x, - right: out.p2.x, - top: out.p1.y, - width: out.p2.x - out.p1.x, - x: out.p1.x, - y: out.p1.y - }; - - return out; - }, - - _parseNb: function(distance) { - let nb = parseFloat(distance, 10); - return isNaN(nb) ? 0 : nb; - }, - - getAdjustedQuadsPolyfill: function(node, region) { - // Get the border-box rect - // Note that this is relative to the node's viewport, so before we can use - // it, will need to go back up the frames like getRect - let borderRect = node.getBoundingClientRect(); - - // If the boxType is border, no need to go any further, we're done - if (region === "border") { - return this._getBoxQuadsFromRect(borderRect, node); - } - - // Else, need to get margin/padding/border distances - let style = node.ownerDocument.defaultView.getComputedStyle(node); - let camel = s => s.substring(0, 1).toUpperCase() + s.substring(1); - let distances = {border:{}, padding:{}, margin: {}}; - - for (let side of ["top", "right", "bottom", "left"]) { - distances.border[side] = this._parseNb(style["border" + camel(side) + "Width"]); - distances.padding[side] = this._parseNb(style["padding" + camel(side)]); - distances.margin[side] = this._parseNb(style["margin" + camel(side)]); - } - - // From the border-box rect, calculate the content-box, padding-box and - // margin-box rects - function offsetRect(rect, offsetType, dir=1) { - return { - top: rect.top + (dir * distances[offsetType].top), - left: rect.left + (dir * distances[offsetType].left), - width: rect.width - (dir * (distances[offsetType].left + distances[offsetType].right)), - height: rect.height - (dir * (distances[offsetType].top + distances[offsetType].bottom)) - }; - } - - if (region === "margin") { - return this._getBoxQuadsFromRect(offsetRect(borderRect, "margin", -1), node); - } else if (region === "padding") { - return this._getBoxQuadsFromRect(offsetRect(borderRect, "border"), node); - } else if (region === "content") { - let paddingRect = offsetRect(borderRect, "border"); - return this._getBoxQuadsFromRect(offsetRect(paddingRect, "padding"), node); - } - }, - - /******************************************************************** - * GetBoxQuads POLYFILL END - ********************************************************************/ }; diff --git a/toolkit/devtools/server/actors/highlighter.js b/toolkit/devtools/server/actors/highlighter.js index 864fc02a2079..298fc2cec3f5 100644 --- a/toolkit/devtools/server/actors/highlighter.js +++ b/toolkit/devtools/server/actors/highlighter.js @@ -398,6 +398,9 @@ BoxModelHighlighter.prototype = { let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes"; + let dimensionBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); + dimensionBox.className = "highlighter-nodeinfobar-dimensions"; + // Add some content to force a better boundingClientRect pseudoClassesBox.textContent = " "; @@ -411,6 +414,7 @@ BoxModelHighlighter.prototype = { texthbox.appendChild(idLabel); texthbox.appendChild(classesBox); texthbox.appendChild(pseudoClassesBox); + texthbox.appendChild(dimensionBox); nodeInfobar.appendChild(texthbox); @@ -427,6 +431,7 @@ BoxModelHighlighter.prototype = { idLabel: idLabel, classesBox: classesBox, pseudoClassesBox: pseudoClassesBox, + dimensionBox: dimensionBox, positioner: infobarPositioner, barHeight: barHeight, }; @@ -582,9 +587,7 @@ BoxModelHighlighter.prototype = { options.region = options.region || "content"; - // TODO: Remove this polyfill - this.rect = - this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin"); + this.rect = this.layoutHelpers.getAdjustedQuads(this.currentNode, "margin"); if (!this.rect) { return null; @@ -592,9 +595,8 @@ BoxModelHighlighter.prototype = { if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) { for (let boxType in this._boxModelNodes) { - // TODO: Remove this polyfill let {p1, p2, p3, p4} = boxType === "margin" ? this.rect : - this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType); + this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType); let boxNode = this._boxModelNodes[boxType]; boxNode.setAttribute("points", @@ -705,29 +707,36 @@ BoxModelHighlighter.prototype = { * Update node information (tagName#id.class) */ _updateInfobar: function() { - if (this.currentNode) { - // Tag name - this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName; - - // ID - this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : ""; - - // Classes - let classes = this.nodeInfo.classesBox; - - classes.textContent = this.currentNode.classList.length ? - "." + Array.join(this.currentNode.classList, ".") : ""; - - // Pseudo-classes - let pseudos = PSEUDO_CLASSES.filter(pseudo => { - return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo); - }, this); - - let pseudoBox = this.nodeInfo.pseudoClassesBox; - pseudoBox.textContent = pseudos.join(""); - - this._moveInfobar(); + if (!this.currentNode) { + return; } + + // Tag name + this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName; + + // ID + this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : ""; + + // Classes + let classes = this.nodeInfo.classesBox; + + classes.textContent = this.currentNode.classList.length ? + "." + Array.join(this.currentNode.classList, ".") : ""; + + // Pseudo-classes + let pseudos = PSEUDO_CLASSES.filter(pseudo => { + return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo); + }, this); + + let pseudoBox = this.nodeInfo.pseudoClassesBox; + pseudoBox.textContent = pseudos.join(""); + + // Dimensions + let dimensionBox = this.nodeInfo.dimensionBox; + let rect = this.currentNode.getBoundingClientRect(); + dimensionBox.textContent = Math.ceil(rect.width) + " x " + + Math.ceil(rect.height); + this._moveInfobar(); }, /** diff --git a/toolkit/modules/DirectoryLinksProvider.jsm b/toolkit/modules/DirectoryLinksProvider.jsm index 672fde51c7de..805fe6357c48 100644 --- a/toolkit/modules/DirectoryLinksProvider.jsm +++ b/toolkit/modules/DirectoryLinksProvider.jsm @@ -268,6 +268,7 @@ let DirectoryLinksProvider = { this._readDirectoryLinksFile().then(rawLinks => { // all directory links have a frecency of DIRECTORY_FRECENCY aCallback(rawLinks.map((link, position) => { + link.directoryIndex = position; link.frecency = DIRECTORY_FRECENCY; link.lastVisitDate = rawLinks.length - position; return link; diff --git a/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js b/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js index 003b2d6af59c..92a74cc29b16 100644 --- a/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js +++ b/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js @@ -259,7 +259,7 @@ add_task(function test_linksURL_locale() { links = yield fetchData(); do_check_eq(links.length, 1); - expected_data = [{url: "http://example.com", title: "US", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + expected_data = [{url: "http://example.com", title: "US", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1, directoryIndex: 0}]; isIdentical(links, expected_data); yield promiseDirectoryDownloadOnPrefChange("general.useragent.locale", "zh-CN"); @@ -267,8 +267,8 @@ add_task(function test_linksURL_locale() { links = yield fetchData(); do_check_eq(links.length, 2) expected_data = [ - {url: "http://example.net", title: "CN", frecency: DIRECTORY_FRECENCY, lastVisitDate: 2}, - {url: "http://example.net/2", title: "CN2", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1} + {url: "http://example.net", title: "CN", frecency: DIRECTORY_FRECENCY, lastVisitDate: 2, directoryIndex: 0}, + {url: "http://example.net/2", title: "CN2", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1, directoryIndex: 1} ]; isIdentical(links, expected_data); @@ -280,7 +280,7 @@ add_task(function test_DirectoryLinksProvider__prefObserver_url() { let links = yield fetchData(); do_check_eq(links.length, 1); - let expectedData = [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + let expectedData = [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1, directoryIndex: 0}]; isIdentical(links, expectedData); // tests these 2 things: