author | Ryan VanderMeulen <ryanvm@gmail.com> |
Tue, 25 Feb 2014 15:20:54 -0500 | |
changeset 170482 | 22650589a724b5a4b875996865ce71c288d875b9 |
parent 170404 | f0bfbe2caf468e1fcd4b3ed20cfe80b290ba9549 (current diff) |
parent 170481 | 4f3ea4de80fccd03966b3798ddc804c8e0dbf34e (diff) |
child 170491 | c349f7cdd5c1c850c30ee768797350f3200ff42d |
child 170495 | b6fbeba807c725bc0179c75bae2e136cde0b2e99 |
child 170552 | d11a993d78468f867ee21093d81dadae8404d750 |
push id | 26288 |
push user | ryanvm@gmail.com |
push date | Tue, 25 Feb 2014 20:20:43 +0000 |
treeherder | mozilla-central@22650589a724 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 30.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/b2g/components/FxAccountsMgmtService.jsm +++ b/b2g/components/FxAccountsMgmtService.jsm @@ -34,17 +34,17 @@ this.FxAccountsMgmtService = { _sendChromeEvent: function(aEventName, aMsg) { if (!this._shell) { return; } log.debug("Chrome event " + JSON.stringify(aMsg)); this._shell.sendCustomEvent(aEventName, aMsg); }, - _onFullfill: function(aMsgId, aData) { + _onFulfill: function(aMsgId, aData) { this._sendChromeEvent("mozFxAccountsChromeEvent", { id: aMsgId, data: aData ? aData : null }); }, _onReject: function(aMsgId, aReason) { this._sendChromeEvent("mozFxAccountsChromeEvent", { @@ -95,49 +95,49 @@ this.FxAccountsMgmtService = { return; } switch(data.method) { case "getAccounts": FxAccountsManager.getAccount().then( account => { // We only expose the email and verification status so far. - self._onFullfill(msg.id, account); + self._onFulfill(msg.id, account); }, reason => { self._onReject(msg.id, reason); } ).then(null, Components.utils.reportError); break; case "logout": FxAccountsManager.signOut().then( () => { - self._onFullfill(msg.id); + self._onFulfill(msg.id); }, reason => { self._onReject(msg.id, reason); } ).then(null, Components.utils.reportError); break; case "queryAccount": FxAccountsManager.queryAccount(data.accountId).then( result => { - self._onFullfill(msg.id, result); + self._onFulfill(msg.id, result); }, reason => { self._onReject(msg.id, reason); } ).then(null, Components.utils.reportError); break; case "signIn": case "signUp": case "refreshAuthentication": FxAccountsManager[data.method](data.accountId, data.password).then( user => { - self._onFullfill(msg.id, user); + self._onFulfill(msg.id, user); }, reason => { self._onReject(msg.id, reason); } ).then(null, Components.utils.reportError); break; } }
new file mode 100644 --- /dev/null +++ b/b2g/components/test/unit/test_fxaccounts.js @@ -0,0 +1,184 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsMgmtService", + "resource://gre/modules/FxAccountsMgmtService.jsm", + "FxAccountsMgmtService"); + +// At end of test, restore original state +const ORIGINAL_AUTH_URI = Services.prefs.getCharPref("identity.fxaccounts.auth.uri"); +const ORIGINAL_SHELL = FxAccountsMgmtService._shell; +do_register_cleanup(function() { + Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI); + FxAccountsMgmtService._shell = ORIGINAL_SHELL; +}); + +// Make profile available so that fxaccounts can store user data +do_get_profile(); + +// Mock the b2g shell; make message passing possible +let mockShell = { + sendCustomEvent: function(aEventName, aMsg) { + Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null); + }, +}; + +function run_test() { + run_next_test(); +} + +add_task(function test_overall() { + do_check_neq(FxAccountsMgmtService, null); +}); + +// Check that invalid email capitalization is corrected on signIn. +// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin +add_test(function test_invalidEmailCase_signIn() { + do_test_pending(); + let clientEmail = "greta.garbo@gmail.com"; + let canonicalEmail = "Greta.Garbo@gmail.COM"; + let attempts = 0; + + function writeResp(response, msg) { + if (typeof msg === "object") { + msg = JSON.stringify(msg); + } + response.bodyOutputStream.write(msg, msg.length); + } + + // Mock of the fxa accounts auth server, reproducing the behavior of + // /account/login when email capitalization is incorrect on signIn. + let server = httpd_setup({ + "/account/login": function(request, response) { + response.setHeader("Content-Type", "application/json"); + attempts += 1; + + // Ensure we don't get in an endless loop + if (attempts > 2) { + response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance"); + writeResp(response, {}); + return; + } + + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + let jsonBody = JSON.parse(body); + let email = jsonBody.email; + + // The second time through, the accounts client will call the api with + // the correct email capitalization. + if (email == canonicalEmail) { + response.setStatusLine(request.httpVersion, 200, "Yay"); + writeResp(response, { + uid: "your-uid", + sessionToken: "your-sessionToken", + keyFetchToken: "your-keyFetchToken", + verified: true, + authAt: 1392144866, + }); + return; + } + + // If the client has the wrong case on the email, we return a 400, with + // the capitalization of the email as saved in the accounts database. + response.setStatusLine(request.httpVersion, 400, "Incorrect email case"); + writeResp(response, { + code: 400, + errno: 120, + error: "Incorrect email case", + email: canonicalEmail, + }); + return; + }, + }); + + // Point the FxAccountsClient's hawk rest request client to the mock server + Services.prefs.setCharPref("identity.fxaccounts.auth.uri", server.baseURI); + + // Receive a mozFxAccountsChromeEvent message + function onMessage(subject, topic, data) { + let message = subject.wrappedJSObject; + + switch (message.id) { + // When we signed in as "Greta.Garbo", the server should have told us + // that the proper capitalization is really "greta.garbo". Call + // getAccounts to get the signed-in user and ensure that the + // capitalization is correct. + case "signIn": + FxAccountsMgmtService.handleEvent({ + detail: { + id: "getAccounts", + data: { + method: "getAccounts", + } + } + }); + break; + + // Having initially signed in as "Greta.Garbo", getAccounts should show + // us that the signed-in user has the properly-capitalized email, + // "greta.garbo". + case "getAccounts": + Services.obs.removeObserver(onMessage, "mozFxAccountsChromeEvent"); + + do_check_eq(message.data.accountId, canonicalEmail); + + do_test_finished(); + server.stop(run_next_test); + break; + + // We should not receive any other mozFxAccountsChromeEvent messages + default: + do_throw("wat!"); + break; + } + } + + Services.obs.addObserver(onMessage, "mozFxAccountsChromeEvent", false); + + FxAccountsMgmtService._shell = mockShell; + + // Trigger signIn using an email with incorrect capitalization + FxAccountsMgmtService.handleEvent({ + detail: { + id: "signIn", + data: { + method: "signIn", + accountId: clientEmail, + password: "123456", + }, + }, + }); +}); + +// End of tests +// Utility functions follow + +function httpd_setup (handlers, port=-1) { + let server = new HttpServer(); + for (let path in handlers) { + server.registerPathHandler(path, handlers[path]); + } + try { + server.start(port); + } catch (ex) { + dump("ERROR starting server on port " + port + ". Already a process listening?"); + do_throw(ex); + } + + // Set the base URI for convenience. + let i = server.identity; + server.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; + + return server; +} + +
--- a/b2g/components/test/unit/xpcshell.ini +++ b/b2g/components/test/unit/xpcshell.ini @@ -1,13 +1,14 @@ [DEFAULT] head = tail = [test_bug793310.js] [test_bug832946.js] +[test_fxaccounts.js] [test_signintowebsite.js] head = head_identity.js tail =
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -949,16 +949,19 @@ pref("toolkit.crashreporter.pluginHangSu // URL for "Learn More" for Crash Reporter pref("toolkit.crashreporter.infoURL", "https://www.mozilla.org/legal/privacy/firefox.html#crash-reporter"); // base URL for web-based support pages pref("app.support.baseURL", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/"); +// base url for web-based feedback pages +pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/%APP%/%VERSION%/"); + // Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror) pref("security.alternate_certificate_error_page", "certerror"); // Whether to start the private browsing mode at application startup pref("browser.privatebrowsing.autostart", false); // Don't try to alter this pref, it'll be reset the next time you use the // bookmarking dialog
--- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -1056,16 +1056,26 @@ let BookmarkingUI = { this._popupNeedsUpdate = true; }, onPopupShowing: function BUI_onPopupShowing(event) { // Don't handle events for submenus. if (event.target != event.currentTarget) return; + // Ideally this code would never be reached, but if you click the outer + // button's border, some cpp code for the menu button's so-called XBL binding + // decides to open the popup even though the dropmarker is invisible. + if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) { + this._showSubview(); + event.preventDefault(); + event.stopPropagation(); + return; + } + let widget = CustomizableUI.getWidget("bookmarks-menu-button") .forWindow(window); if (widget.overflowed) { // Don't open a popup in the overflow popup, rather just open the Library. event.preventDefault(); widget.node.removeAttribute("closemenu"); PlacesCommandHook.showPlacesOrganizer("BookmarksMenu"); return; @@ -1341,35 +1351,40 @@ let BookmarkingUI = { this._notificationTimeout = setTimeout( () => { this.notifier.removeAttribute("notification"); this.notifier.removeAttribute("in-bookmarks-toolbar"); this.button.removeAttribute("notification"); this.notifier.style.transform = ''; }, 1000); }, + _showSubview: function() { + let view = document.getElementById("PanelUI-bookmarks"); + view.addEventListener("ViewShowing", this); + view.addEventListener("ViewHiding", this); + let anchor = document.getElementById("bookmarks-menu-button"); + anchor.setAttribute("closemenu", "none"); + PanelUI.showSubView("PanelUI-bookmarks", anchor, + CustomizableUI.AREA_PANEL); + }, + onCommand: function BUI_onCommand(aEvent) { if (aEvent.target != aEvent.currentTarget) { return; } // Handle special case when the button is in the panel. - let widget = CustomizableUI.getWidget("bookmarks-menu-button") - .forWindow(window); let isBookmarked = this._itemIds.length > 0; if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) { - let view = document.getElementById("PanelUI-bookmarks"); - view.addEventListener("ViewShowing", this); - view.addEventListener("ViewHiding", this); - widget.node.setAttribute("closemenu", "none"); - PanelUI.showSubView("PanelUI-bookmarks", widget.node, - CustomizableUI.AREA_PANEL); + this._showSubview(); return; } + let widget = CustomizableUI.getWidget("bookmarks-menu-button") + .forWindow(window); if (widget.overflowed) { // Allow to close the panel if the page is already bookmarked, cause // we are going to open the edit bookmark panel. if (isBookmarked) widget.node.removeAttribute("closemenu"); else widget.node.setAttribute("closemenu", "none"); }
--- a/browser/base/content/browser-tabview.js +++ b/browser/base/content/browser-tabview.js @@ -416,28 +416,26 @@ let TabView = { }, // ---------- // Function: _addToolbarButton // Adds the TabView button to the TabsToolbar. _addToolbarButton: function TabView__addToolbarButton() { let buttonId = "tabview-button"; - if (document.getElementById(buttonId)) + if (CustomizableUI.getPlacementOfWidget(buttonId)) return; - let toolbar = document.getElementById("TabsToolbar"); - let currentSet = toolbar.currentSet.split(","); - let alltabsPos = currentSet.indexOf("alltabs-button"); - if (-1 == alltabsPos) - return; - - let allTabsBtn = document.getElementById("alltabs-button"); - let nextItem = allTabsBtn.nextSibling; - toolbar.insertItem(buttonId, nextItem); + let allTabsBtnPlacement = CustomizableUI.getPlacementOfWidget("alltabs-button"); + // allTabsBtnPlacement can never be null because the button isn't removable + let desiredPosition = allTabsBtnPlacement.position + 1; + CustomizableUI.addWidgetToArea(buttonId, "TabsToolbar", desiredPosition); + // NB: this is for backwards compatibility, and should be removed by + // https://bugzilla.mozilla.org/show_bug.cgi?id=976041 + document.persist("TabsToolbar", "currentset"); }, // ---------- // Function: updateGroupNumberBroadcaster // Updates the group number broadcaster. updateGroupNumberBroadcaster: function TabView_updateGroupNumberBroadcaster(number) { let groupsNumber = document.getElementById("tabviewGroupsNumber"); groupsNumber.setAttribute("groups", number);
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -4238,26 +4238,28 @@ function onViewToolbarsPopupShowing(aEve return; } // triggerNode can be a nested child element of a toolbaritem. let toolbarItem = popup.triggerNode; if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") { toolbarItem = toolbarItem.firstChild; - } else { + } else if (toolbarItem && toolbarItem.localName != "toolbar") { while (toolbarItem && toolbarItem.parentNode) { let parent = toolbarItem.parentNode; if ((parent.classList && parent.classList.contains("customization-target")) || parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well. parent.localName == "toolbarpaletteitem" || parent.localName == "toolbar") break; toolbarItem = parent; } + } else { + toolbarItem = null; } // Right-clicking on an empty part of the tabstrip will exit // the above loop with toolbarItem being the xul:document. // That has no parentNode, and we should disable the items in // this case. let movable = toolbarItem && toolbarItem.parentNode && CustomizableUI.isWidgetRemovable(toolbarItem);
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -707,19 +707,16 @@ <label id="urlbar-display" value="&urlbar.switchToTab.label;"/> </box> <hbox id="urlbar-icons"> <image id="page-report-button" class="urlbar-icon" hidden="true" tooltiptext="&pageReportIcon.tooltip;" onclick="gPopupBlockerObserver.onReportButtonClick(event);"/> - <image id="star-button" - class="urlbar-icon" - onclick="if (event.button === 0) BookmarkingUI.onCommand(event);"/> </hbox> <toolbarbutton id="urlbar-go-button" class="chromeclass-toolbar-additional" onclick="gURLBar.handleCommand(event);" tooltiptext="&goEndCap.tooltip;"/> <toolbarbutton id="urlbar-reload-button" class="chromeclass-toolbar-additional" command="Browser:ReloadOrDuplicate"
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -52,16 +52,17 @@ support-files = file_bug906190.js file_bug906190.sjs file_bug970276_popup1.html file_bug970276_popup2.html file_bug970276_favicon1.ico file_bug970276_favicon2.ico file_dom_notifications.html file_fullscreen-window-open.html + get_user_media.html head.js healthreport_testRemoteCommands.html moz.png offlineQuotaNotification.cacheManifest offlineQuotaNotification.html page_style_sample.html plugin_add_dynamically.html plugin_alternate_content.html @@ -259,16 +260,17 @@ skip-if = os == "mac" # bug 967013, bug run-if = datareporting [browser_discovery.js] [browser_duplicateIDs.js] [browser_drag.js] skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638. [browser_findbarClose.js] [browser_fullscreen-window-open.js] [browser_gestureSupport.js] +[browser_get_user_media.js] [browser_getshortcutoruri.js] [browser_hide_removing.js] [browser_homeDrop.js] [browser_identity_UI.js] [browser_keywordBookmarklets.js] [browser_keywordSearch.js] [browser_keywordSearch_postData.js] [browser_lastAccessedTab.js]
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/general/browser_get_user_media.js @@ -0,0 +1,742 @@ +const kObservedTopics = [ + "getUserMedia:response:allow", + "getUserMedia:revoke", + "getUserMedia:response:deny", + "getUserMedia:request", + "recording-device-events", + "recording-window-ended" +]; + +const PREF_PERMISSION_FAKE = "media.navigator.permission.fake"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", + "@mozilla.org/mediaManagerService;1", + "nsIMediaManagerService"); + +var gObservedTopics = {}; +function observer(aSubject, aTopic, aData) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = 1; + else + ++gObservedTopics[aTopic]; +} + +function promiseNotification(aTopic, aAction) { + let deferred = Promise.defer(); + + Services.obs.addObserver(function observer() { + ok(true, "got " + aTopic + " notification"); + Services.obs.removeObserver(observer, aTopic); + + if (kObservedTopics.indexOf(aTopic) != -1) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = -1; + else + --gObservedTopics[aTopic]; + } + + deferred.resolve(); + }, aTopic, false); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function expectNotification(aTopic) { + is(gObservedTopics[aTopic], 1, "expected notification " + aTopic); + if (aTopic in gObservedTopics) + --gObservedTopics[aTopic]; +} + +function expectNoNotifications() { + for (let topic in gObservedTopics) { + if (gObservedTopics[topic]) + is(gObservedTopics[topic], 0, topic + " notification unexpected"); + } + gObservedTopics = {} +} + +function promiseMessage(aMessage, aAction) { + let deferred = Promise.defer(); + + content.addEventListener("message", function messageListener(event) { + content.removeEventListener("message", messageListener); + is(event.data, aMessage, "received " + aMessage); + if (event.data == aMessage) + deferred.resolve(); + else + deferred.reject(); + }); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function promisePopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => PopupNotifications.getNotification(aName), + () => { + ok(!!PopupNotifications.getNotification(aName), + aName + " notification appeared"); + deferred.resolve(); + }, "timeout waiting for popup notification " + aName); + + return deferred.promise; +} + +function promiseNoPopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => !PopupNotifications.getNotification(aName), + () => { + ok(!PopupNotifications.getNotification(aName), + aName + " notification removed"); + deferred.resolve(); + }, "timeout waiting for popup notification " + aName + " to disappear"); + + return deferred.promise; +} + +const kActionAlways = 1; +const kActionDeny = 2; +const kActionNever = 3; + +function activateSecondaryAction(aAction) { + let notification = PopupNotifications.panel.firstChild; + notification.button.focus(); + let popup = notification.menupopup; + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press 'down' as many time as needed to select the requested action. + while (aAction--) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_RETURN", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", + { altKey: !navigator.platform.contains("Mac") }); +} + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); + kObservedTopics.forEach(topic => { + Services.obs.removeObserver(observer, topic); + }); + Services.prefs.clearUserPref(PREF_PERMISSION_FAKE); +}); + +function getMediaCaptureState() { + let hasVideo = {}; + let hasAudio = {}; + MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio); + if (hasVideo.value && hasAudio.value) + return "CameraAndMicrophone"; + if (hasVideo.value) + return "Camera"; + if (hasAudio.value) + return "Microphone"; + return "none"; +} + +function closeStream(aAlreadyClosed) { + expectNoNotifications(); + + info("closing the stream"); + content.wrappedJSObject.closeStream(); + + if (!aAlreadyClosed) + yield promiseNotification("recording-device-events"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + if (!aAlreadyClosed) + expectNotification("recording-window-ended"); + + let statusButton = document.getElementById("webrtc-status-button"); + ok(statusButton.hidden, "WebRTC status button hidden"); +} + +function checkDeviceSelectors(aAudio, aVideo) { + let micSelector = document.getElementById("webRTC-selectMicrophone"); + if (aAudio) + ok(!micSelector.hidden, "microphone selector visible"); + else + ok(micSelector.hidden, "microphone selector hidden"); + + let cameraSelector = document.getElementById("webRTC-selectCamera"); + if (aVideo) + ok(!cameraSelector.hidden, "camera selector visible"); + else + ok(cameraSelector.hidden, "camera selector hidden"); +} + +function checkSharingUI() { + yield promisePopupNotification("webRTC-sharingDevices"); + let statusButton = document.getElementById("webrtc-status-button"); + ok(!statusButton.hidden, "WebRTC status button visible"); +} + +function checkNotSharing() { + is(getMediaCaptureState(), "none", "expected nothing to be shared"); + + ok(!PopupNotifications.getNotification("webRTC-sharingDevices"), + "no webRTC-sharingDevices popup notification"); + + let statusButton = document.getElementById("webrtc-status-button"); + ok(statusButton.hidden, "WebRTC status button hidden"); +} + +let gTests = [ + +{ + desc: "getUserMedia audio+video", + run: function checkAudioVideo() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI(); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio only", + run: function checkAudioOnly() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "Microphone", "expected microphone to be shared"); + + yield checkSharingUI(); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia video only", + run: function checkVideoOnly() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(false, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(false, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "Camera", "expected camera to be shared"); + + yield checkSharingUI(); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio+video, user disables video", + run: function checkDisableVideo() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + // disable the camera + document.getElementById("webRTC-selectCamera-menulist").value = -1; + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + + // reset the menuitem to have no impact on the following tests. + document.getElementById("webRTC-selectCamera-menulist").value = 0; + + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "Microphone", + "expected microphone to be shared"); + + yield checkSharingUI(); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio+video, user disables audio", + run: function checkDisableAudio() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + // disable the microphone + document.getElementById("webRTC-selectMicrophone-menulist").value = -1; + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + + // reset the menuitem to have no impact on the following tests. + document.getElementById("webRTC-selectMicrophone-menulist").value = 0; + + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "Camera", + "expected microphone to be shared"); + + yield checkSharingUI(); + yield closeStream(); + } +}, + +{ + desc: "getUserMedia audio+video, user disables both audio and video", + run: function checkDisableAudioVideo() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + // disable the camera and microphone + document.getElementById("webRTC-selectCamera-menulist").value = -1; + document.getElementById("webRTC-selectMicrophone-menulist").value = -1; + + yield promiseMessage("error: PERMISSION_DENIED", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + + // reset the menuitems to have no impact on the following tests. + document.getElementById("webRTC-selectCamera-menulist").value = 0; + document.getElementById("webRTC-selectMicrophone-menulist").value = 0; + + expectNotification("getUserMedia:response:deny"); + expectNotification("recording-window-ended"); + checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video, user clicks \"Don't Share\"", + run: function checkDontShare() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + yield promiseMessage("error: PERMISSION_DENIED", () => { + activateSecondaryAction(kActionDeny); + }); + + expectNotification("getUserMedia:response:deny"); + expectNotification("recording-window-ended"); + checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: stop sharing", + run: function checkStopSharing() { + yield promiseNotification("getUserMedia:request", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI(); + + PopupNotifications.getNotification("webRTC-sharingDevices").reshow(); + activateSecondaryAction(kActionDeny); + + yield promiseNotification("recording-device-events"); + expectNotification("getUserMedia:revoke"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + + if (gObservedTopics["recording-device-events"] == 1) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + gObservedTopics["recording-device-events"] = 0; + } + + expectNoNotifications(); + checkNotSharing(); + + // the stream is already closed, but this will do some cleanup anyway + yield closeStream(true); + } +}, + +{ + desc: "getUserMedia prompt: Always/Never Share", + run: function checkRememberCheckbox() { + function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo, + aExpectedAudioPerm, aExpectedVideoPerm, aNever) { + yield promiseNotification("getUserMedia:request", () => { + content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo); + }); + + yield promisePopupNotification("webRTC-shareDevices"); + + let elt = id => document.getElementById(id); + + let noAudio = aAllowAudio === undefined; + is(elt("webRTC-selectMicrophone").hidden, noAudio, + "microphone selector expected to be " + (noAudio ? "hidden" : "visible")); + if (!noAudio) + elt("webRTC-selectMicrophone-menulist").value = (aAllowAudio || aNever) ? 0 : -1; + + let noVideo = aAllowVideo === undefined; + is(elt("webRTC-selectCamera").hidden, noVideo, + "camera selector expected to be " + (noVideo ? "hidden" : "visible")); + if (!noVideo) + elt("webRTC-selectCamera-menulist").value = (aAllowVideo || aNever) ? 0 : -1; + + let expectedMessage = + (aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED"; + yield promiseMessage(expectedMessage, () => { + activateSecondaryAction(aNever ? kActionNever : kActionAlways); + }); + let expected = []; + if (expectedMessage == "ok") { + expectNotification("getUserMedia:response:allow"); + expectNotification("recording-device-events"); + if (aAllowVideo) + expected.push("Camera"); + if (aAllowAudio) + expected.push("Microphone"); + expected = expected.join("And"); + } + else { + expectNotification("getUserMedia:response:deny"); + expectNotification("recording-window-ended"); + expected = "none"; + } + is(getMediaCaptureState(), expected, + "expected " + expected + " to be shared"); + + function checkDevicePermissions(aDevice, aExpected) { + let Perms = Services.perms; + let uri = content.document.documentURIObject; + let devicePerms = Perms.testExactPermission(uri, aDevice); + if (aExpected === undefined) + is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions"); + else { + is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION, + aDevice + " persistently " + (aExpected ? "allowed" : "denied")); + } + Perms.remove(uri.host, aDevice); + } + checkDevicePermissions("microphone", aExpectedAudioPerm); + checkDevicePermissions("camera", aExpectedVideoPerm); + + if (expectedMessage == "ok") + yield closeStream(); + } + + // 3 cases where the user accepts the device prompt. + info("audio+video, user grants, expect both perms set to allow"); + yield checkPerm(true, true, true, true, true, true); + info("audio only, user grants, check audio perm set to allow, video perm not set"); + yield checkPerm(true, false, true, undefined, true, undefined); + info("video only, user grants, check video perm set to allow, audio perm not set"); + yield checkPerm(false, true, undefined, true, undefined, true); + + // 3 cases where the user rejects the device request. + // First test these cases by setting the device to 'No Audio'/'No Video' + info("audio+video, user denies, expect both perms set to deny"); + yield checkPerm(true, true, false, false, false, false); + info("audio only, user denies, expect audio perm set to deny, video not set"); + yield checkPerm(true, false, false, undefined, false, undefined); + info("video only, user denies, expect video perm set to deny, audio perm not set"); + yield checkPerm(false, true, undefined, false, undefined, false); + // Now test these 3 cases again by using the 'Never Share' action. + info("audio+video, user denies, expect both perms set to deny"); + yield checkPerm(true, true, false, false, false, false, true); + info("audio only, user denies, expect audio perm set to deny, video not set"); + yield checkPerm(true, false, false, undefined, false, undefined, true); + info("video only, user denies, expect video perm set to deny, audio perm not set"); + yield checkPerm(false, true, undefined, false, undefined, false, true); + + // 2 cases where the user allows half of what's requested. + info("audio+video, user denies video, grants audio, " + + "expect video perm set to deny, audio perm set to allow."); + yield checkPerm(true, true, true, false, true, false); + info("audio+video, user denies audio, grants video, " + + "expect video perm set to allow, audio perm set to deny."); + yield checkPerm(true, true, false, true, false, true); + } +}, + +{ + desc: "getUserMedia without prompt: use persistent permissions", + run: function checkUsePersistentPermissions() { + function usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo, + aExpectStream) { + let Perms = Services.perms; + let uri = content.document.documentURIObject; + if (aAllowAudio !== undefined) { + Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION + : Perms.DENY_ACTION); + } + if (aAllowVideo !== undefined) { + Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION + : Perms.DENY_ACTION); + } + + let gum = function() { + content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo); + }; + + if (aExpectStream === undefined) { + // Check that we get a prompt. + yield promiseNotification("getUserMedia:request", gum); + yield promisePopupNotification("webRTC-shareDevices"); + + // Deny the request to cleanup... + yield promiseMessage("error: PERMISSION_DENIED", () => { + activateSecondaryAction(kActionDeny); + }); + expectNotification("getUserMedia:response:deny"); + expectNotification("recording-window-ended"); + } + else { + let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio); + let expectedMessage = allow ? "ok" : "error: PERMISSION_DENIED"; + yield promiseMessage(expectedMessage, gum); + + if (expectedMessage == "ok") { + expectNotification("recording-device-events"); + + // Check what's actually shared. + let expected = []; + if (aAllowVideo && aRequestVideo) + expected.push("Camera"); + if (aAllowAudio && aRequestAudio) + expected.push("Microphone"); + expected = expected.join("And"); + is(getMediaCaptureState(), expected, + "expected " + expected + " to be shared"); + + yield closeStream(); + } + else { + expectNotification("recording-window-ended"); + } + } + + Perms.remove(uri.host, "camera"); + Perms.remove(uri.host, "microphone"); + } + + // Set both permissions identically + info("allow audio+video, request audio+video, expect ok (audio+video)"); + yield usePerm(true, true, true, true, true); + info("deny audio+video, request audio+video, expect denied"); + yield usePerm(false, false, true, true, false); + + // Allow audio, deny video. + info("allow audio, deny video, request audio+video, expect ok (audio)"); + yield usePerm(true, false, true, true, true); + info("allow audio, deny video, request audio, expect ok (audio)"); + yield usePerm(true, false, true, false, true); + info("allow audio, deny video, request video, expect denied"); + yield usePerm(true, false, false, true, false); + + // Deny audio, allow video. + info("deny audio, allow video, request audio+video, expect ok (video)"); + yield usePerm(false, true, true, true, true); + info("deny audio, allow video, request audio, expect denied"); + yield usePerm(false, true, true, false, true); + info("deny audio, allow video, request video, expect ok (video)"); + yield usePerm(false, true, false, true, false); + + // Allow audio, video not set. + info("allow audio, request audio+video, expect prompt"); + yield usePerm(true, undefined, true, true, undefined); + info("allow audio, request audio, expect ok (audio)"); + yield usePerm(true, undefined, true, false, true); + info("allow audio, request video, expect prompt"); + yield usePerm(true, undefined, false, true, undefined); + + // Deny audio, video not set. + info("deny audio, request audio+video, expect prompt"); + yield usePerm(false, undefined, true, true, undefined); + info("deny audio, request audio, expect denied"); + yield usePerm(false, undefined, true, false, false); + info("deny audio, request video, expect prompt"); + yield usePerm(false, undefined, false, true, undefined); + + // Allow video, video not set. + info("allow video, request audio+video, expect prompt"); + yield usePerm(undefined, true, true, true, undefined); + info("allow video, request audio, expect prompt"); + yield usePerm(undefined, true, true, false, undefined); + info("allow video, request video, expect ok (video)"); + yield usePerm(undefined, true, false, true, true); + + // Deny video, video not set. + info("deny video, request audio+video, expect prompt"); + yield usePerm(undefined, false, true, true, undefined); + info("deny video, request audio, expect prompt"); + yield usePerm(undefined, false, true, false, undefined); + info("deny video, request video, expect denied"); + yield usePerm(undefined, false, false, true, false); + } +}, + +{ + desc: "Stop Sharing removes persistent permissions", + run: function checkStopSharingRemovesPersistentPermissions() { + function stopAndCheckPerm(aRequestAudio, aRequestVideo) { + let Perms = Services.perms; + let uri = content.document.documentURIObject; + + // Initially set both permissions to 'allow'. + Perms.add(uri, "microphone", Perms.ALLOW_ACTION); + Perms.add(uri, "camera", Perms.ALLOW_ACTION); + + // Start sharing what's been requested. + yield promiseMessage("ok", () => { + content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo); + }); + expectNotification("recording-device-events"); + yield checkSharingUI(); + + // Stop sharing. + PopupNotifications.getNotification("webRTC-sharingDevices").reshow(); + activateSecondaryAction(kActionDeny); + + yield promiseNotification("recording-device-events"); + expectNotification("getUserMedia:revoke"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + + if (gObservedTopics["recording-device-events"] == 1) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + gObservedTopics["recording-device-events"] = 0; + } + + // Check that permissions have been removed as expected. + let audioPerm = Perms.testExactPermission(uri, "microphone"); + if (aRequestAudio) + is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed"); + else + is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched"); + + let videoPerm = Perms.testExactPermission(uri, "camera"); + if (aRequestVideo) + is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed"); + else + is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched"); + + // Cleanup. + yield closeStream(true); + + Perms.remove(uri.host, "camera"); + Perms.remove(uri.host, "microphone"); + } + + info("request audio+video, stop sharing resets both"); + yield stopAndCheckPerm(true, true); + info("request audio, stop sharing resets audio only"); + yield stopAndCheckPerm(true, false); + info("request video, stop sharing resets video only"); + yield stopAndCheckPerm(false, true); + } +} + +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function onload() { + tab.linkedBrowser.removeEventListener("load", onload, true); + + kObservedTopics.forEach(topic => { + Services.obs.addObserver(observer, topic, false); + }); + Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true); + + Task.spawn(function () { + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + expectNoNotifications(); + } + }).then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath) + rootDir = rootDir.replace("chrome://mochitests/content/", + "http://127.0.0.1:8888/"); + content.location = rootDir + "get_user_media.html"; +} + + +function wait(time) { + let deferred = Promise.defer(); + setTimeout(deferred.resolve, time); + return deferred.promise; +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/general/get_user_media.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head><meta charset="UTF-8"></head> +<body> +<div id="message"></div> +<script> +function message(m) { + document.getElementById("message").innerHTML = m; + window.parent.postMessage(m, "*"); +} + +var gStream; + +function requestDevice(aAudio, aVideo) { + window.navigator.mozGetUserMedia({video: aVideo, audio: aAudio, fake: true}, + function(stream) { + gStream = stream; + message("ok"); + }, function(err) { message("error: " + err); }); +} +message("pending"); + +function closeStream() { + if (!gStream) + return; + gStream.stop(); + gStream = null; + message("closed"); +} +</script> +</body> +</html>
--- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -554,17 +554,20 @@ function openHealthReport() } #endif /** * Opens the feedback page for this version of the application. */ function openFeedbackPage() { - openUILinkIn("https://input.mozilla.org/feedback", "tab"); + var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.feedback.baseURL"); + openUILinkIn(url, "tab"); } function buildHelpMenu() { // Enable/disable the "Report Web Forgery" menu item. if (typeof gSafeBrowsing != "undefined") gSafeBrowsing.setReportPhishingMenu(); }
--- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -220,17 +220,17 @@ let CustomizableUIInternal = { legacy: true, type: CustomizableUI.TYPE_TOOLBAR, defaultPlacements: [ "tabbrowser-tabs", "new-tab-button", "alltabs-button", "tabs-closebutton", ], - defaultCollapsed: false, + defaultCollapsed: null, }, true); this.registerArea(CustomizableUI.AREA_BOOKMARKS, { legacy: true, type: CustomizableUI.TYPE_TOOLBAR, defaultPlacements: [ "personal-bookmarks", ], defaultCollapsed: true, @@ -2122,17 +2122,19 @@ let CustomizableUIInternal = { let placements = gPlacements.get(areaId); for (let areaNode of areaNodes) { this.buildArea(areaId, placements, areaNode); let area = gAreas.get(areaId); if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { let defaultCollapsed = area.get("defaultCollapsed"); let win = areaNode.ownerDocument.defaultView; - win.setToolbarVisibility(areaNode, !defaultCollapsed); + if (defaultCollapsed !== null) { + win.setToolbarVisibility(areaNode, !defaultCollapsed); + } } } } }, /** * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. */ @@ -2146,21 +2148,25 @@ let CustomizableUIInternal = { // Need to clear the previous state before setting the prefs // because pref observers may check if there is a previous UI state. this._clearPreviousUIState(); Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); this.loadSavedState(); - for (let areaId of Object.keys(gSavedState.placements)) { - let placements = gSavedState.placements[areaId]; - gPlacements.set(areaId, placements); + // If the user just customizes toolbar/titlebar visibility, gSavedState will be null + // and we don't need to do anything else here: + if (gSavedState) { + for (let areaId of Object.keys(gSavedState.placements)) { + let placements = gSavedState.placements[areaId]; + gPlacements.set(areaId, placements); + } + this._rebuildRegisteredAreas(); } - this._rebuildRegisteredAreas(); }, _clearPreviousUIState: function() { Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => { gUIStateBeforeReset[prop] = null; }); }, @@ -2281,17 +2287,17 @@ let CustomizableUIInternal = { return itemNode && removableOrDefault(itemNode || item); }); } if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; let collapsed = container.getAttribute(attribute) == "true"; let defaultCollapsed = props.get("defaultCollapsed"); - if (collapsed != defaultCollapsed) { + if (defaultCollapsed !== null && collapsed != defaultCollapsed) { LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")"); return false; } } } LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") + "\nvs.\n" + defaultPlacements.join(",")); @@ -2489,17 +2495,19 @@ this.CustomizableUI = { * - legacy: set to true if you want customizableui to * automatically migrate the currentset attribute * - overflowable: set to true if your toolbar is overflowable. * This requires an anchor, and only has an * effect for toolbars. * - defaultPlacements: an array of widget IDs making up the * default contents of the area * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies - * if toolbar is collapsed by default (default to true) + * if toolbar is collapsed by default (default to true). + * Specify null to ensure that reset/inDefaultArea don't care + * about a toolbar's collapsed state */ registerArea: function(aName, aProperties) { CustomizableUIInternal.registerArea(aName, aProperties); }, /** * Register a concrete node for a registered area. This method is automatically * called from any toolbar in the main browser window that has its * "customizable" attribute set to true. There should normally be no need to @@ -2872,17 +2880,18 @@ this.CustomizableUI = { getAreaType: function(aArea) { let area = gAreas.get(aArea); return area ? area.get("type") : null; }, /** * Check if a toolbar is collapsed by default. * * @param aArea the ID of the area whose default-collapsed state you want to know. - * @return `true` or `false` depending on the area, null if the area is unknown. + * @return `true` or `false` depending on the area, null if the area is unknown, + * or its collapsed state cannot normally be controlled by the user */ isToolbarDefaultCollapsed: function(aArea) { let area = gAreas.get(aArea); return area ? area.get("defaultCollapsed") : null; }, /** * Obtain the DOM node that is the customize target for an area in a * specific window.
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm +++ b/browser/components/customizableui/src/CustomizableWidgets.jsm @@ -378,16 +378,17 @@ const CustomizableWidgets = [{ class: cls, label: true, tooltiptext: "tooltiptext2", shortcutId: "key_fullZoomEnlarge", }]; let node = aDocument.createElementNS(kNSXUL, "toolbaritem"); node.setAttribute("id", "zoom-controls"); + node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); // Set this as an attribute in addition to the property to make sure we can style correctly. node.setAttribute("removable", "true"); node.classList.add("chromeclass-toolbar-additional"); node.classList.add("toolbaritem-combined-buttons"); node.classList.add(kWidePanelItemClass); buttons.forEach(function(aButton, aIndex) { @@ -536,16 +537,17 @@ const CustomizableWidgets = [{ class: cls, label: true, tooltiptext: "tooltiptext2", shortcutId: "key_paste", }]; let node = aDocument.createElementNS(kNSXUL, "toolbaritem"); node.setAttribute("id", "edit-controls"); + node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); // Set this as an attribute in addition to the property to make sure we can style correctly. node.setAttribute("removable", "true"); node.classList.add("chromeclass-toolbar-additional"); node.classList.add("toolbaritem-combined-buttons"); node.classList.add(kWidePanelItemClass); buttons.forEach(function(aButton, aIndex) {
--- a/browser/components/customizableui/src/CustomizeMode.jsm +++ b/browser/components/customizableui/src/CustomizeMode.jsm @@ -686,20 +686,20 @@ CustomizeMode.prototype = { wrapper.setAttribute("itemchecked", "true"); aNode.removeAttribute("checked"); } if (aNode.hasAttribute("id")) { wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id")); } - if (aNode.hasAttribute("title")) { + if (aNode.hasAttribute("label")) { + wrapper.setAttribute("title", aNode.getAttribute("label")); + } else if (aNode.hasAttribute("title")) { wrapper.setAttribute("title", aNode.getAttribute("title")); - } else if (aNode.hasAttribute("label")) { - wrapper.setAttribute("title", aNode.getAttribute("label")); } if (aNode.hasAttribute("flex")) { wrapper.setAttribute("flex", aNode.getAttribute("flex")); } let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode); wrapper.setAttribute("removable", removable);
--- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js +++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js @@ -30,16 +30,51 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); let hiddenPromise = contextMenuHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; }); +// Right-click on an empty bit of extra toolbar should +// show a context menu with moving options disabled, +// and a toggle option for the extra toolbar +add_task(function() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = contextMenuShown(contextMenu); + let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []); + toolbar.setAttribute("context", "toolbar-context-menu"); + toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu"); + EventUtils.synthesizeMouseAtCenter(toolbar, {type: "contextmenu", button: 2 }); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["#toggle_880164_empty_toolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = contextMenuHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; + removeCustomToolbars(); +}); + + // Right-click on the urlbar-container should // show a context menu with disabled options to move it. add_task(function() { let contextMenu = document.getElementById("toolbar-context-menu"); let shownPromise = contextMenuShown(contextMenu); let urlBarContainer = document.getElementById("urlbar-container"); // Need to make sure not to click within an edit field. let urlbarRect = urlBarContainer.getBoundingClientRect();
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -78,17 +78,17 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", - "resource:///modules/AsyncShutdown.jsm"); + "resource://gre/modules/AsyncShutdown.jsm"); const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; const PREF_PLUGINS_UPDATEURL = "plugins.update.url"; // Seconds of idle before trying to create a bookmarks backup. const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60; // Minimum interval between backups. We try to not create more than one backup // per interval. @@ -633,16 +633,19 @@ BrowserGlue.prototype = { /** * Application shutdown handler. */ _onQuitApplicationGranted: function () { // This pref must be set here because SessionStore will use its value // on quit-application. this._setPrefToSaveSession(); + + // Call trackStartupCrashEnd here in case the delayed call on startup hasn't + // yet occurred (see trackStartupCrashEnd caller in browser.js). try { let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] .getService(Ci.nsIAppStartup); appStartup.trackStartupCrashEnd(); } catch (e) { Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e); } @@ -1289,17 +1292,17 @@ BrowserGlue.prototype = { var notifyBox = win.gBrowser.getNotificationBox(); var notification = notifyBox.appendNotification(text, title, null, notifyBox.PRIORITY_CRITICAL_MEDIUM, buttons); notification.persistence = -1; // Until user closes it }, _migrateUI: function BG__migrateUI() { - const UI_VERSION = 19; + const UI_VERSION = 20; const BROWSER_DOCURL = "chrome://browser/content/browser.xul#"; let currentUIVersion = 0; try { currentUIVersion = Services.prefs.getIntPref("browser.migration.version"); } catch(ex) {} if (currentUIVersion >= UI_VERSION) return; @@ -1552,16 +1555,25 @@ BrowserGlue.prototype = { detector == "ruprob" || detector == "ukprob")) { // If the encoding detector pref value is not reachable from the UI, // reset to default (varies by localization). Services.prefs.clearUserPref("intl.charset.detector"); } } + if (currentUIVersion < 20) { + // Remove persisted collapsed state from TabsToolbar. + let resource = this._rdf.GetResource("collapsed"); + let toolbar = this._rdf.GetResource(BROWSER_DOCURL + "TabsToolbar"); + if (this._getPersist(toolbar, resource)) { + this._setPersist(toolbar, resource); + } + } + if (this._dirty) this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); delete this._rdf; delete this._dataSource; // Update the migration version. Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
--- a/browser/components/places/content/menu.xml +++ b/browser/components/places/content/menu.xml @@ -573,15 +573,31 @@ </implementation> <handlers> <handler event="popupshowing" phase="target"><![CDATA[ this.adjustArrowPosition(); ]]></handler> <handler event="popupshown" phase="target"><![CDATA[ this.setAttribute("panelopen", "true"); + //XXXgijs: this is sadfaces, reading styles right after we dirty layout, but + //I don't know of a way around it. + let container = document.getAnonymousElementByAttribute(this, "anonid", "container"); + let cs = getComputedStyle(container); + let transitionProp = cs.transitionProperty; + let transitionTime = parseFloat(cs.transitionDuration); + if ((transitionProp.indexOf("transform") > -1 || transitionProp == "all") && + transitionTime > 0) { + this.style.pointerEvents = 'none'; + } + ]]></handler> + <handler event="transitionend"><![CDATA[ + if (event.originalTarget.getAttribute("anonid") == "container" && + event.propertyName == "transform") { + this.style.removeProperty("pointer-events"); + } ]]></handler> <handler event="popuphidden" phase="target"><![CDATA[ this.removeAttribute("panelopen"); ]]></handler> </handlers> </binding> </bindings>
--- a/browser/components/preferences/aboutPermissions.js +++ b/browser/components/preferences/aboutPermissions.js @@ -33,17 +33,17 @@ let gVisitStmt = gPlacesDatabase.createA "SELECT SUM(visit_count) AS count " + "FROM moz_places " + "WHERE rev_host = :rev_host"); /** * Permission types that should be tested with testExactPermission, as opposed * to testPermission. This is based on what consumers use to test these permissions. */ -let TEST_EXACT_PERM_TYPES = ["geo"]; +let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"]; /** * Site object represents a single site, uniquely identified by a host. */ function Site(host) { this.host = host; this.listitem = null; @@ -325,26 +325,29 @@ let PermissionDefaults = { if (!Services.prefs.getBoolPref("full-screen-api.enabled")) { return this.DENY; } return this.UNKNOWN; }, set fullscreen(aValue) { let value = (aValue != this.DENY); Services.prefs.setBoolPref("full-screen-api.enabled", value); - } -} + }, + + get camera() this.UNKNOWN, + get microphone() this.UNKNOWN +}; /** * AboutPermissions manages the about:permissions page. */ let AboutPermissions = { /** * Number of sites to return from the places database. - */ + */ PLACES_SITES_LIMIT: 50, /** * When adding sites to the dom sites-list, divide workload into intervals. */ LIST_BUILD_CHUNK: 5, // interval size LIST_BUILD_DELAY: 100, // delay between intervals @@ -364,27 +367,28 @@ let AboutPermissions = { /** * This reflects the permissions that we expose in the UI. These correspond * to permission type strings in the permission manager, PermissionDefaults, * and element ids in aboutPermissions.xul. * * Potential future additions: "sts/use", "sts/subd" */ - _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", "fullscreen"], + _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", + "fullscreen", "camera", "microphone"], /** * Permissions that don't have a global "Allow" option. */ - _noGlobalAllow: ["geo", "indexedDB", "fullscreen"], + _noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"], /** * Permissions that don't have a global "Deny" option. */ - _noGlobalDeny: [], + _noGlobalDeny: ["camera", "microphone"], _stringBundle: Services.strings. createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"), /** * Called on page load. */ init: function() { @@ -402,17 +406,17 @@ let AboutPermissions = { Services.prefs.addObserver("dom.indexedDB.enabled", this, false); Services.prefs.addObserver("dom.disable_open_during_load", this, false); Services.prefs.addObserver("full-screen-api.enabled", this, false); Services.obs.addObserver(this, "perm-changed", false); Services.obs.addObserver(this, "passwordmgr-storage-changed", false); Services.obs.addObserver(this, "cookie-changed", false); Services.obs.addObserver(this, "browser:purge-domain-data", false); - + this._observersInitialized = true; Services.obs.notifyObservers(null, "browser-permissions-preinit", null); }, /** * Called on page unload. */ cleanUp: function() { @@ -537,32 +541,32 @@ let AboutPermissions = { if (itemCnt % this.LIST_BUILD_CHUNK == 0) { yield true; } try { // aLogin.hostname is a string in origin URL format (e.g. "http://foo.com") let uri = NetUtil.newURI(aLogin.hostname); this.addHost(uri.host); } catch (e) { - // newURI will throw for add-ons logins stored in chrome:// URIs + // newURI will throw for add-ons logins stored in chrome:// URIs } itemCnt++; }, this); let disabledHosts = Services.logins.getAllDisabledHosts(); disabledHosts.forEach(function(aHostname) { if (itemCnt % this.LIST_BUILD_CHUNK == 0) { yield true; } try { // aHostname is a string in origin URL format (e.g. "http://foo.com") let uri = NetUtil.newURI(aHostname); this.addHost(uri.host); } catch (e) { - // newURI will throw for add-ons logins stored in chrome:// URIs + // newURI will throw for add-ons logins stored in chrome:// URIs } itemCnt++; }, this); let (enumerator = Services.perms.enumerator) { while (enumerator.hasMoreElements()) { if (itemCnt % this.LIST_BUILD_CHUNK == 0) { yield true; @@ -773,17 +777,17 @@ let AboutPermissions = { }, updateVisitCount: function() { this._selectedSite.getVisitCount(function(aCount) { let visitForm = AboutPermissions._stringBundle.GetStringFromName("visitCount"); let visitLabel = PluralForm.get(aCount, visitForm) .replace("#1", aCount); document.getElementById("site-visit-count").value = visitLabel; - }); + }); }, updatePasswordsCount: function() { if (!this._selectedSite) { document.getElementById("passwords-count").hidden = true; document.getElementById("passwords-manage-all-button").hidden = false; return; }
--- a/browser/components/preferences/aboutPermissions.xul +++ b/browser/components/preferences/aboutPermissions.xul @@ -108,16 +108,58 @@ <menuitem id="geo-1" value="1" label="&permission.allow;"/> <menuitem id="geo-2" value="2" label="&permission.block;"/> </menupopup> </menulist> </hbox> </vbox> </hbox> + <!-- Camera --> + <hbox id="camera-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="camera"/> + <vbox> + <label class="pref-title" value="&camera.label;"/> + <hbox align="center"> + <menulist id="camera-menulist" + class="pref-menulist" + type="camera" + oncommand="AboutPermissions.onPermissionCommand(event);"> + <menupopup> + <menuitem id="camera-0" value="0" label="&permission.alwaysAsk;"/> + <menuitem id="camera-1" value="1" label="&permission.allow;"/> + <menuitem id="camera-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + </hbox> + </vbox> + </hbox> + + <!-- Microphone --> + <hbox id="microphone-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="microphone"/> + <vbox> + <label class="pref-title" value="µphone.label;"/> + <hbox align="center"> + <menulist id="microphone-menulist" + class="pref-menulist" + type="microphone" + oncommand="AboutPermissions.onPermissionCommand(event);"> + <menupopup> + <menuitem id="microphone-0" value="0" label="&permission.alwaysAsk;"/> + <menuitem id="microphone-1" value="1" label="&permission.allow;"/> + <menuitem id="microphone-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + </hbox> + </vbox> + </hbox> + <!-- Cookies --> <hbox id="cookie-pref-item" class="pref-item" align="top"> <image class="pref-icon" type="cookie"/> <vbox> <label class="pref-title" value="&cookie.label;"/> <hbox align="center"> <menulist id="cookie-menulist"
--- a/browser/components/preferences/languages.xul +++ b/browser/components/preferences/languages.xul @@ -42,18 +42,17 @@ <stringbundleset id="languageSet"> <stringbundle id="bundleRegions" src="chrome://global/locale/regionNames.properties"/> <stringbundle id="bundleLanguages" src="chrome://global/locale/languageNames.properties"/> <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> <stringbundle id="bundleAccepted" src="resource://gre/res/language.properties"/> </stringbundleset> - <description>&languages.customize.prefLangDescript;</description> - <label>&languages.customize.active.label;</label> + <description>&languages.customize.description;</description> <grid flex="1"> <columns> <column flex="1"/> <column/> </columns> <rows> <row flex="1"> <listbox id="activeLanguages" flex="1" rows="6"
--- a/browser/components/preferences/sync.xul +++ b/browser/components/preferences/sync.xul @@ -292,23 +292,24 @@ <richlistitem> <checkbox label="&engine.tabs.label;" accesskey="&engine.tabs.accesskey;" preference="engine.tabs"/> </richlistitem> </richlistbox> </vbox> </groupbox> - <vbox> + <hbox align="center"> <label value="&syncDeviceName.label;" accesskey="&syncDeviceName.accesskey;" control="syncComputerName"/> <textbox id="fxaSyncComputerName" + flex="1" onchange="gSyncUtils.changeName(this)"/> - </vbox> + </hbox> <hbox id="tosPP" pack="center"> <label class="text-link" onclick="event.stopPropagation();gSyncUtils.openToS();" value="&prefs.tosLink.label;"/> <label class="text-link" onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();" value="&fxaPrivacyNotice.link.label;"/> </hbox>
--- a/browser/components/preferences/tests/browser_permissions.js +++ b/browser/components/preferences/tests/browser_permissions.js @@ -22,26 +22,28 @@ const PERM_FIRST_PARTY_ONLY = 9; // used to set permissions on test sites const TEST_PERMS = { "password": PERM_ALLOW, "cookie": PERM_ALLOW, "geo": PERM_UNKNOWN, "indexedDB": PERM_UNKNOWN, "popup": PERM_DENY, "fullscreen" : PERM_UNKNOWN, + "camera": PERM_UNKNOWN, + "microphone": PERM_UNKNOWN }; const NO_GLOBAL_ALLOW = [ "geo", "indexedDB", "fullscreen" ]; // number of managed permissions in the interface -const TEST_PERMS_COUNT = 6; +const TEST_PERMS_COUNT = 8; function test() { waitForExplicitFinish(); registerCleanupFunction(cleanUp); // add test history visit addVisits(TEST_URI_1, function() { // set permissions ourselves to avoid problems with different defaults @@ -159,17 +161,17 @@ var tests = [ }); runNextTest(); }, function test_all_sites_permission() { // apply the old default of allowing all cookies Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); - + // there should be no user-set pref for cookie behavior is(Services.prefs.getIntPref("network.cookie.cookieBehavior"), PERM_UNKNOWN, "network.cookie.cookieBehavior is expected default"); // the default behavior is to allow cookies let cookieMenulist = getPermissionMenulist("cookie"); is(cookieMenulist.value, PERM_ALLOW, "menulist correctly shows that cookies are allowed"); @@ -184,22 +186,22 @@ var tests = [ runNextTest(); }, function test_manage_all_passwords() { // make sure "Manage All Passwords..." button opens the correct dialog addWindowListener("chrome://passwordmgr/content/passwordManager.xul", runNextTest); gBrowser.contentDocument.getElementById("passwords-manage-all-button").doCommand(); - + }, function test_manage_all_cookies() { // make sure "Manage All Cookies..." button opens the correct dialog - addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest); + addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest); gBrowser.contentDocument.getElementById("cookies-manage-all-button").doCommand(); }, function test_select_site() { // select the site that has the permissions we set at the beginning of the test let testSiteItem = getSiteItem(TEST_URI_2.host); gSitesList.selectedItem = testSiteItem;
--- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -1707,16 +1707,20 @@ let SessionStoreInternal = { let data = DyingWindowCache.get(aWindow).extData || {}; return data[aKey] || ""; } throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); }, setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) { + if (typeof aStringValue != "string") { + throw new TypeError("setWindowValue only accepts string values"); + } + if (!("__SSi" in aWindow)) { throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); } if (!this._windows[aWindow.__SSi].extData) { this._windows[aWindow.__SSi].extData = {}; } this._windows[aWindow.__SSi].extData[aKey] = aStringValue; this.saveStateDelayed(aWindow); @@ -1737,16 +1741,20 @@ let SessionStoreInternal = { else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { // If the tab hasn't been fully restored, get the data from the to-be-restored data data = aTab.linkedBrowser.__SS_data.extData; } return data[aKey] || ""; }, setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) { + if (typeof aStringValue != "string") { + throw new TypeError("setTabValue only accepts string values"); + } + // If the tab hasn't been restored, then set the data there, otherwise we // could lose newly added data. let saveTo; if (aTab.__SS_extdata) { saveTo = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { saveTo = aTab.linkedBrowser.__SS_data.extData; @@ -1778,16 +1786,20 @@ let SessionStoreInternal = { } }, getGlobalValue: function ssi_getGlobalValue(aKey) { return this._globalState.get(aKey); }, setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) { + if (typeof aStringValue != "string") { + throw new TypeError("setGlobalValue only accepts string values"); + } + this._globalState.set(aKey, aStringValue); this.saveStateDelayed(); }, deleteGlobalValue: function ssi_deleteGlobalValue(aKey) { this._globalState.delete(aKey); this.saveStateDelayed(); },
--- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -257,35 +257,23 @@ InspectorPanel.prototype = { markDirty: function InspectorPanel_markDirty() { this.isDirty = true; }, /** * Hooks the searchbar to show result and auto completion suggestions. */ setupSearchBox: function InspectorPanel_setupSearchBox() { - let searchDoc; - if (this.target.isLocalTab) { - searchDoc = this.browser.contentDocument; - } else if (this.target.window) { - searchDoc = this.target.window.document; - } else { - searchDoc = null; - } // Initiate the selectors search object. - let setNodeFunction = function(eventName, node) { - this.selection.setNodeFront(node, "selectorsearch"); - }.bind(this); if (this.searchSuggestions) { this.searchSuggestions.destroy(); this.searchSuggestions = null; } this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); - this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox); - this.searchSuggestions.on("node-selected", setNodeFunction); + this.searchSuggestions = new SelectorSearch(this, this.searchBox); }, /** * Build the sidebar. */ setupSidebar: function InspectorPanel_setupSidebar() { let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); this.sidebar = new ToolSidebar(tabbox, this, "inspector");
--- a/browser/devtools/inspector/selector-search.js +++ b/browser/devtools/inspector/selector-search.js @@ -1,39 +1,34 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const EventEmitter = require("devtools/shared/event-emitter"); const promise = require("sdk/core/promise"); loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup); // Maximum number of selector suggestions shown in the panel. const MAX_SUGGESTIONS = 15; /** * Converts any input box on a page to a CSS selector search and suggestion box. * * @constructor * @param InspectorPanel aInspector * The InspectorPanel whose `walker` attribute should be used for * document traversal. - * @param nsIDOMDocument aContentDocument - * The content document which inspector is attached to, or null if - * a remote document. * @param nsiInputElement aInputNode * The input element to which the panel will be attached and from where * search input will be taken. */ -function SelectorSearch(aInspector, aContentDocument, aInputNode) { +function SelectorSearch(aInspector, aInputNode) { this.inspector = aInspector; - this.doc = aContentDocument; this.searchBox = aInputNode; this.panelDoc = this.searchBox.ownerDocument; // initialize variables. this._lastSearched = null; this._lastValidSearch = ""; this._lastToLastValidSearch = null; this._searchResults = null; @@ -50,29 +45,27 @@ function SelectorSearch(aInspector, aCon let options = { panelId: "inspector-searchbox-panel", listBoxId: "searchbox-panel-listbox", autoSelect: true, position: "before_start", direction: "ltr", theme: "auto", onClick: this._onListBoxKeypress, - onKeypress: this._onListBoxKeypress, + onKeypress: this._onListBoxKeypress }; this.searchPopup = new AutocompletePopup(this.panelDoc, options); // event listeners. this.searchBox.addEventListener("command", this._onHTMLSearch, true); this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); // For testing, we need to be able to wait for the most recent node request // to finish. Tests can watch this promise for that. this._lastQuery = promise.resolve(null); - - EventEmitter.decorate(this); } exports.SelectorSearch = SelectorSearch; SelectorSearch.prototype = { get walker() this.inspector.walker, @@ -160,41 +153,39 @@ SelectorSearch.prototype = { } } return this._state; }, /** * Removes event listeners and cleans up references. */ - destroy: function SelectorSearch_destroy() { + destroy: function() { // event listeners. this.searchBox.removeEventListener("command", this._onHTMLSearch, true); this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); this.searchPopup.destroy(); this.searchPopup = null; this.searchBox = null; - this.doc = null; this.panelDoc = null; this._searchResults = null; this._searchSuggestions = null; - EventEmitter.decorate(this); }, _selectResult: function(index) { return this._searchResults.item(index).then(node => { - this.emit("node-selected", node); + this.inspector.selection.setNodeFront(node, "selectorsearch"); }); }, /** * The command callback for the input box. This function is automatically * invoked as the user is typing if the input box type is search. */ - _onHTMLSearch: function SelectorSearch__onHTMLSearch() { + _onHTMLSearch: function() { let query = this.searchBox.value; if (query == this._lastSearched) { return; } this._lastSearched = query; this._searchResults = []; this._searchIndex = 0; @@ -251,34 +242,34 @@ SelectorSearch.prototype = { this.searchPopup.hidePopup(); } this.searchBox.classList.remove("devtools-no-search-result"); return this._selectResult(0); } return this._selectResult(0).then(() => { this.searchBox.classList.remove("devtools-no-search-result"); - }).then( () => this.showSuggestions()); + }).then(() => this.showSuggestions()); } if (query.match(/[\s>+]$/)) { this._lastValidSearch = query + "*"; } else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0]; this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; } this.searchBox.classList.add("devtools-no-search-result"); return this.showSuggestions(); }); }, /** * Handles keypresses inside the input box. */ - _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) { + _onSearchKeypress: function(aEvent) { let query = this.searchBox.value; switch(aEvent.keyCode) { case aEvent.DOM_VK_RETURN: if (query == this._lastSearched && this._searchResults) { this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; } else { this._onHTMLSearch(); @@ -343,17 +334,17 @@ SelectorSearch.prototype = { if (this._searchResults && this._searchResults.length > 0) { this._lastQuery = this._selectResult(this._searchIndex); } }, /** * Handles keypress and mouse click on the suggestions richlistbox. */ - _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) { + _onListBoxKeypress: function(aEvent) { switch(aEvent.keyCode || aEvent.button) { case aEvent.DOM_VK_RETURN: case aEvent.DOM_VK_TAB: case 0: // left mouse button aEvent.stopPropagation(); aEvent.preventDefault(); this.searchBox.value = this.searchPopup.selectedItem.label; this.searchBox.focus(); @@ -399,21 +390,20 @@ SelectorSearch.prototype = { this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || ["",""])[1]; this._onHTMLSearch(); break; } }, - /** * Populates the suggestions list and show the suggestion popup. */ - _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) { + _showPopup: function(aList, aFirstPart) { let total = 0; let query = this.searchBox.value; let toLowerCase = false; let items = []; // In case of tagNames, change the case to small. if (query.match(/.*[\.#][^\.#]{0,}$/) == null) { toLowerCase = true; } @@ -453,17 +443,17 @@ SelectorSearch.prototype = { this.searchPopup.hidePopup(); } }, /** * Suggests classes,ids and tags based on the user input as user types in the * searchbox. */ - showSuggestions: function SelectorSearch_showSuggestions() { + showSuggestions: function() { let query = this.searchBox.value; let firstPart = ""; if (this.state == this.States.TAG) { // gets the tag that is being completed. For ex. 'div.foo > s' returns 's', // 'di' returns 'di' and likewise. firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1]; query = query.slice(0, query.length - firstPart.length); } @@ -493,10 +483,10 @@ SelectorSearch.prototype = { if (this.state == this.States.CLASS) { firstPart = "." + firstPart; } else if (this.state == this.States.ID) { firstPart = "#" + firstPart; } this._showPopup(result.suggestions, firstPart); }); - }, + } };
--- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -161,16 +161,19 @@ MarkupView.prototype = { this._containers.get(nodeFront).hovered = true; this._hoveredNode = nodeFront; } }, _onMouseLeave: function() { this._hideBoxModel(); + if (this._hoveredNode) { + this._containers.get(this._hoveredNode).hovered = false; + } this._hoveredNode = null; }, _showBoxModel: function(nodeFront, options={}) { this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); }, _hideBoxModel: function() {
--- a/browser/devtools/markupview/test/browser.ini +++ b/browser/devtools/markupview/test/browser.ini @@ -2,16 +2,17 @@ support-files = browser_inspector_markup_edit.html browser_inspector_markup_mutation.html browser_inspector_markup_mutation_flashing.html browser_inspector_markup_navigation.html browser_inspector_markup_subset.html browser_inspector_markup_765105_tooltip.png browser_inspector_markup_950732.html + browser_inspector_markup_962647_search.html head.js [browser_bug896181_css_mixed_completion_new_attribute.js] # Bug 916763 - too many intermittent failures skip-if = true [browser_inspector_markup_edit.js] [browser_inspector_markup_edit_2.js] [browser_inspector_markup_edit_3.js] @@ -23,8 +24,9 @@ skip-if = true [browser_inspector_markup_mutation_flashing.js] [browser_inspector_markup_navigation.js] [browser_inspector_markup_subset.js] [browser_inspector_markup_765105_tooltip.js] [browser_inspector_markup_950732.js] [browser_inspector_markup_964014_copy_image_data.js] [browser_inspector_markup_968316_highlit_node_on_hover_then_select.js] [browser_inspector_markup_968316_highlight_node_after_mouseleave_mousemove.js] +[browser_inspector_markup_962647_search.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head></head> +<body> + <ul> + <li> + <span>this is an <em>important</em> node</span> + </li> + </ul> +</body> +</html> \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.js @@ -0,0 +1,50 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that searching for nodes using the selector-search input expands and +// selects the right nodes in the markup-view, even when those nodes are deeply +// nested (and therefore not attached yet when the markup-view is initialized). + +const TEST_URL = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html"; + +function test() { + waitForExplicitFinish(); + + let p = content.document.querySelector("p"); + Task.spawn(function() { + info("loading the test page"); + yield addTab(TEST_URL); + + info("opening the inspector"); + let {inspector, toolbox} = yield openInspector(); + + ok(!getContainerForRawNode(inspector.markup, getNode("em")), + "The <em> tag isn't present yet in the markup-view"); + + // Searching for the innermost element first makes sure that the inspector + // back-end is able to attach the resulting node to the tree it knows at the + // moment. When the inspector is started, the <body> is the default selected + // node, and only the parents up to the ROOT are known, and its direct children + info("searching for the innermost child: <em>"); + let updated = inspector.once("inspector-updated"); + searchUsingSelectorSearch("em", inspector); + yield updated; + + ok(getContainerForRawNode(inspector.markup, getNode("em")), + "The <em> tag is now imported in the markup-view"); + is(inspector.selection.node, getNode("em"), + "The <em> tag is the currently selected node"); + + info("searching for other nodes too"); + for (let node of ["span", "li", "ul"]) { + let updated = inspector.once("inspector-updated"); + searchUsingSelectorSearch(node, inspector); + yield updated; + is(inspector.selection.node, getNode(node), + "The <" + node + "> tag is the currently selected node"); + } + + gBrowser.removeCurrentTab(); + }).then(null, ok.bind(null, false)).then(finish); +}
--- a/browser/devtools/markupview/test/head.js +++ b/browser/devtools/markupview/test/head.js @@ -63,17 +63,16 @@ function openInspector() { * HTML node * @param {MarkupView} markupView The instance of MarkupView currently loaded into the inspector panel * @param {DOMNode} rawNode The DOM node for which the container is required * @return {MarkupContainer} */ function getContainerForRawNode(markupView, rawNode) { let front = markupView.walker.frontForRawNode(rawNode); let container = markupView.getContainer(front); - ok(container, "A markup-container object was found"); return container; } /** * Simple DOM node accesor function that takes either a node or a string css * selector as argument and returns the corresponding node * @param {String|DOMNode} nodeOrSelector * @return {DOMNode} @@ -235,8 +234,31 @@ function redoChange(inspector) { if (!canRedo) { return promise.reject(); } let mutated = inspector.once("markupmutation"); inspector.markup.undo.redo(); return mutated; } + +/** + * Get the selector-search input box from the inspector panel + * @return {DOMNode} + */ +function getSelectorSearchBox(inspector) { + return inspector.panelWin.document.getElementById("inspector-searchbox"); +} + +/** + * Using the inspector panel's selector search box, search for a given selector. + * The selector input string will be entered in the input field and the <ENTER> + * keypress will be simulated. + * This function won't wait for any events and is not async. It's up to callers + * to subscribe to events and react accordingly. + */ +function searchUsingSelectorSearch(selector, inspector) { + info("Entering \"" + selector + "\" into the selector-search input field"); + let field = getSelectorSearchBox(inspector); + field.focus(); + field.value = selector; + EventUtils.sendKey("return", inspector.panelWin); +}
--- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -221,24 +221,26 @@ run-if = os == "win" [browser_webconsole_bug_770099_violation.js] [browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js] [browser_webconsole_bug_804845_ctrl_key_nav.js] run-if = os == "mac" [browser_webconsole_bug_817834_add_edited_input_to_history.js] [browser_webconsole_bug_821877_csp_errors.js] [browser_webconsole_bug_837351_securityerrors.js] [browser_webconsole_bug_846918_hsts_invalid-headers.js] +[browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js] [browser_webconsole_cached_autocomplete.js] [browser_webconsole_change_font_size.js] [browser_webconsole_chrome.js] [browser_webconsole_closure_inspection.js] [browser_webconsole_completion.js] [browser_webconsole_console_extras.js] [browser_webconsole_console_logging_api.js] [browser_webconsole_count.js] +[browser_webconsole_dont_navigate_on_doubleclick.js] [browser_webconsole_execution_scope.js] [browser_webconsole_for_of.js] [browser_webconsole_history.js] [browser_webconsole_input_field_focus_on_panel_select.js] [browser_webconsole_js_input_expansion.js] [browser_webconsole_jsterm.js] [browser_webconsole_live_filtering_of_message_types.js] [browser_webconsole_live_filtering_on_search_strings.js]
--- a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js +++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js @@ -177,38 +177,47 @@ function testCompletion(hud) { jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); yield undefined; newItems = popup.getItems(); ok(!newItems.every(function(item) { return item.label != "prop1"; }), "autocomplete results do contain prop1"); - // Test if 'foo1Obj.prop1.' gives 'prop11' + // Test if 'foo2Obj.prop1.' gives 'prop11' input.value = "foo2Obj.prop1."; input.setSelectionRange(14, 14); jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); yield undefined; newItems = popup.getItems(); ok(!newItems.every(function(item) { return item.label != "prop11"; }), "autocomplete results do contain prop11"); - // Test if 'foo1Obj.prop1.prop11.' gives suggestions for a string i.e. 'length' + // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string i.e. 'length' input.value = "foo2Obj.prop1.prop11."; input.setSelectionRange(21, 21); jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); yield undefined; newItems = popup.getItems(); ok(!newItems.every(function(item) { return item.label != "length"; }), "autocomplete results do contain length"); + // Test if 'foo1Obj[0].' throws no errors. + input.value = "foo2Obj[0]."; + input.setSelectionRange(11, 11); + jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); + yield undefined; + + newItems = popup.getItems(); + is(newItems.length, 0, "no items for foo2Obj[0]"); + testDriver = null; executeSoon(finishTest); yield undefined; } function debuggerOpened(aResult) { let debuggerWin = aResult.panelWin;
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js @@ -0,0 +1,112 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the 'Log Request and Response Bodies' buttons can be toggled with keyboard. +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 915141: Toggle log response bodies with keyboard"; +let hud; + +function test() { + let saveBodiesMenuItem; + let saveBodiesContextMenuItem; + + loadTab(TEST_URI).then(({tab: tab}) => { + return openConsole(tab); + }) + .then((aHud) => { + hud = aHud; + saveBodiesMenuItem = hud.ui.rootElement.querySelector("#saveBodies"); + saveBodiesContextMenuItem = hud.ui.rootElement.querySelector("#saveBodiesContextMenu"); + + // Test the context menu action. + info("Testing 'Log Request and Response Bodies' menuitem of right click context menu."); + + return openPopup(saveBodiesContextMenuItem); + }) + .then(() => { + is(saveBodiesContextMenuItem.getAttribute("checked"), "false", + "Context menu: 'log responses' is not checked before action."); + is(hud.ui._saveRequestAndResponseBodies, false, + "Context menu: Responses are not logged before action."); + + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + + return waitForUpdate(saveBodiesContextMenuItem); + }) + .then(() => { + is(saveBodiesContextMenuItem.getAttribute("checked"), "true", + "Context menu: 'log responses' is checked after menuitem was selected with keyboard."); + is(hud.ui._saveRequestAndResponseBodies, true, + "Context menu: Responses are saved after menuitem was selected with keyboard."); + + return openPopup(saveBodiesMenuItem); + }) + .then(() => { + // Test the 'Net' menu item. + info("Testing 'Log Request and Response Bodies' menuitem of 'Net' menu in the console."); + // 'Log Request and Response Bodies' should be selected due to previous test. + + is(saveBodiesMenuItem.getAttribute("checked"), "true", + "Console net menu: 'log responses' is checked before action."); + is(hud.ui._saveRequestAndResponseBodies, true, + "Console net menu: Responses are logged before action."); + + // The correct item is the last one in the menu. + EventUtils.synthesizeKey("VK_UP", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + + return waitForUpdate(saveBodiesMenuItem); + }) + .then(() => { + is(saveBodiesMenuItem.getAttribute("checked"), "false", + "Console net menu: 'log responses' is NOT checked after menuitem was selected with keyboard."); + is(hud.ui._saveRequestAndResponseBodies, false, + "Responses are NOT saved after menuitem was selected with keyboard."); + }) + .then(finishTest); +} + +/** + * Opens and waits for the menu containing aMenuItem to open. + * @param aMenuItem MenuItem + * A MenuItem in a menu that should be opened. + * @return A promise that's resolved once menu is open. + */ +function openPopup(aMenuItem) { + let menu = aMenuItem.parentNode; + + let menuOpened = promise.defer(); + let uiUpdated = promise.defer(); + // The checkbox menuitem is updated asynchronously on 'popupshowing' event so + // it's better to wait for both the update to happen and the menu to open + // before continuing or the test might fail due to a race between menu being + // shown and the item updated to have the correct state. + hud.ui.once("save-bodies-ui-toggled", uiUpdated.resolve); + menu.addEventListener("popupshown", function onPopup () { + menu.removeEventListener("popupshown", onPopup); + menuOpened.resolve(); + }); + + menu.openPopup(); + return Promise.all([menuOpened.promise, uiUpdated.promise]); +} + +/** + * Waits for the settings and menu containing aMenuItem to update. + * @param aMenuItem MenuItem + * The menuitem that should be updated. + * @return A promise that's resolved once the settings and menus are updated. + */ +function waitForUpdate(aMenuItem) { + info("Waiting for settings update to complete."); + let deferred = promise.defer(); + hud.ui.once("save-bodies-pref-reversed", function () { + hud.ui.once("save-bodies-ui-toggled", deferred.resolve); + // The checked state is only updated once the popup is shown. + aMenuItem.parentNode.openPopup(); + }); + return deferred.promise; +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js @@ -0,0 +1,43 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that if a link in console is double clicked, the console frame doesn't +// navigate to that destination (bug 975707). + +function test() { + Task.spawn(runner).then(finishTest); + + function* runner() { + const TEST_PAGE_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html" + "?_uniq=" + Date.now(); + + const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>"); + const hud = yield openConsole(tab); + + content.location = TEST_PAGE_URI; + + let messages = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "Network request message", + url: TEST_PAGE_URI, + category: CATEGORY_NETWORK + }] + }); + + let networkEventMessage = messages[0].matched.values().next().value; + let urlNode = networkEventMessage.querySelector(".url"); + + let deferred = promise.defer(); + urlNode.addEventListener("click", function onClick(aEvent) { + urlNode.removeEventListener("click", onClick); + ok(aEvent.defaultPrevented, "The default action was prevented."); + + deferred.resolve(); + }); + + EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2}, hud.iframeWindow); + + yield deferred.promise; + } +}
--- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -532,22 +532,22 @@ WebConsoleFrame.prototype = { this.getSaveRequestAndResponseBodies().then(aValue => { this.setSaveRequestAndResponseBodies(!aValue); aElement.setAttribute("checked", aValue); this.emit("save-bodies-pref-reversed"); }); } let saveBodies = doc.getElementById("saveBodies"); - saveBodies.addEventListener("click", reverseSaveBodiesPref); + saveBodies.addEventListener("command", reverseSaveBodiesPref); saveBodies.disabled = !this.getFilterState("networkinfo") && !this.getFilterState("network"); let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu"); - saveBodiesContextMenu.addEventListener("click", reverseSaveBodiesPref); + saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref); saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && !this.getFilterState("network"); saveBodies.parentNode.addEventListener("popupshowing", () => { updateSaveBodiesPrefUI(saveBodies); saveBodies.disabled = !this.getFilterState("networkinfo") && !this.getFilterState("network"); }); @@ -2671,23 +2671,23 @@ WebConsoleFrame.prototype = { this._startX = aEvent.clientX; this._startY = aEvent.clientY; }, false); aNode.addEventListener("click", (aEvent) => { let mousedown = this._mousedown; this._mousedown = false; + aEvent.preventDefault(); + // Do not allow middle/right-click or 2+ clicks. if (aEvent.detail != 1 || aEvent.button != 0) { return; } - aEvent.preventDefault(); - // If this event started with a mousedown event and it ends at a different // location, we consider this text selection. if (mousedown && (this._startX != aEvent.clientX) && (this._startY != aEvent.clientY)) { this._startX = this._startY = undefined; return;
--- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -492,18 +492,22 @@ identity.loggedIn.signOut.accessKey = O # The number of devices can be either one or two. getUserMedia.shareCamera.message = Would you like to share your camera with %S? getUserMedia.shareMicrophone.message = Would you like to share your microphone with %S? getUserMedia.shareCameraAndMicrophone.message = Would you like to share your camera and microphone with %S? getUserMedia.noVideo.label = No Video getUserMedia.noAudio.label = No Audio getUserMedia.shareSelectedDevices.label = Share Selected Device;Share Selected Devices getUserMedia.shareSelectedDevices.accesskey = S +getUserMedia.always.label = Always Share +getUserMedia.always.accesskey = A getUserMedia.denyRequest.label = Don't Share getUserMedia.denyRequest.accesskey = D +getUserMedia.never.label = Never Share +getUserMedia.never.accesskey = N getUserMedia.sharingCamera.message2 = You are currently sharing your camera with this page. getUserMedia.sharingMicrophone.message2 = You are currently sharing your microphone with this page. getUserMedia.sharingCameraAndMicrophone.message2 = You are currently sharing your camera and microphone with this page. getUserMedia.continueSharing.label = Continue Sharing getUserMedia.continueSharing.accesskey = C getUserMedia.stopSharing.label = Stop Sharing getUserMedia.stopSharing.accesskey = S
--- a/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd +++ b/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd @@ -37,8 +37,10 @@ <!-- LOCALIZATION NOTE (indexedDB.label): This is describing indexedDB storage using the same language used for the permIndexedDB string in browser/pageInfo.dtd --> <!ENTITY indexedDB.label "Maintain Offline Storage"> <!ENTITY popup.label "Open Pop-up Windows"> <!ENTITY fullscreen.label "Fullscreen"> +<!ENTITY camera.label "Use the Camera"> +<!ENTITY microphone.label "Use the Microphone">
--- a/browser/locales/en-US/chrome/browser/preferences/languages.dtd +++ b/browser/locales/en-US/chrome/browser/preferences/languages.dtd @@ -1,17 +1,16 @@ <!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <!ENTITY window.width "30em"> <!ENTITY languages.customize.Header "Languages"> -<!ENTITY languages.customize.prefLangDescript "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference."> -<!ENTITY languages.customize.active.label "Languages in order of preference:"> +<!ENTITY languages.customize.description "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference:"> <!ENTITY languages.customize.moveUp.label "Move Up"> <!ENTITY languages.customize.moveUp.accesskey "U"> <!ENTITY languages.customize.moveDown.label "Move Down"> <!ENTITY languages.customize.moveDown.accesskey "D"> <!ENTITY languages.customize.deleteButton.label "Remove"> <!ENTITY languages.customize.deleteButton.accesskey "R"> <!ENTITY languages.customize.selectLanguage.label "Select a language to add…"> <!ENTITY languages.customize.addButton.label "Add">
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties +++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties @@ -5,15 +5,17 @@ allow = Allow allowForSession = Allow for Session block = Block alwaysAsk = Always Ask permission.cookie.label = Set Cookies permission.desktop-notification.label = Show Notifications permission.image.label = Load Images +permission.camera.label = Use the Camera +permission.microphone.label = Use the Microphone permission.install.label = Install Add-ons permission.popup.label = Open Pop-up Windows permission.geo.label = Access Your Location permission.indexedDB.label = Maintain Offline Storage permission.fullscreen.label = Enter Fullscreen permission.pointerLock.label = Hide the Mouse Pointer
--- a/browser/locales/en-US/profile/bookmarks.inc +++ b/browser/locales/en-US/profile/bookmarks.inc @@ -1,15 +1,15 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. #filter emptyLines # LOCALIZATION NOTE: The 'en-US' strings in the URLs will be replaced with -# your locale code, and link to your translated pages as soon as they're +# your locale code, and link to your translated pages as soon as they're # live. #define bookmarks_title Bookmarks #define bookmarks_heading Bookmarks #define bookmarks_toolbarfolder Bookmarks Toolbar Folder #define bookmarks_toolbarfolder_description Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar @@ -32,9 +32,14 @@ # LOCALIZATION NOTE (firefox_community): # link title for https://www.mozilla.org/en-US/contribute/ #define firefox_community Get Involved # LOCALIZATION NOTE (firefox_about): # link title for https://www.mozilla.org/en-US/about/ #define firefox_about About Us +# LOCALIZATION NOTE (firefox_feedback): +# link title for browser feedback page +# currently used by Metro only: https://input.mozilla.org/feedback/metrofirefox +#define firefox_feedback Give Feedback + #unfilter emptyLines
--- a/browser/metro/base/content/browser.js +++ b/browser/metro/base/content/browser.js @@ -158,17 +158,16 @@ var Browser = { } messageManager.addMessageListener("DOMLinkAdded", this); messageManager.addMessageListener("Browser:FormSubmit", this); messageManager.addMessageListener("Browser:CanUnload:Return", this); messageManager.addMessageListener("scroll", this); messageManager.addMessageListener("Browser:CertException", this); messageManager.addMessageListener("Browser:BlockedSite", this); - messageManager.addMessageListener("Browser:TapOnSelection", this); Task.spawn(function() { // Activation URIs come from protocol activations, secondary tiles, and file activations let activationURI = yield this.getShortcutOrURI(Services.metro.activationURI); let self = this; function loadStartupURI() { if (activationURI) { @@ -231,17 +230,16 @@ var Browser = { ClickEventHandler.uninit(); ContentAreaObserver.shutdown(); Appbar.shutdown(); messageManager.removeMessageListener("Browser:FormSubmit", this); messageManager.removeMessageListener("scroll", this); messageManager.removeMessageListener("Browser:CertException", this); messageManager.removeMessageListener("Browser:BlockedSite", this); - messageManager.removeMessageListener("Browser:TapOnSelection", this); Services.obs.removeObserver(SessionHistoryObserver, "browser:purge-session-history"); window.controllers.removeController(this); window.controllers.removeController(BrowserUI); }, getHomePage: function getHomePage(aOptions) { @@ -861,26 +859,16 @@ var Browser = { break; } case "Browser:CertException": this._handleCertException(aMessage); break; case "Browser:BlockedSite": this._handleBlockedSite(aMessage); break; - case "Browser:TapOnSelection": - if (!InputSourceHelper.isPrecise) { - if (SelectionHelperUI.isActive) { - SelectionHelperUI.shutdown(); - } - if (SelectionHelperUI.canHandle(aMessage)) { - SelectionHelperUI.openEditSession(aMessage); - } - } - break; } }, }; Browser.MainDragger = function MainDragger() { this._horizontalScrollbar = document.getElementById("horizontal-scroller"); this._verticalScrollbar = document.getElementById("vertical-scroller"); this._scrollScales = { x: 0, y: 0 };
--- a/browser/metro/base/content/startui/Start.xul +++ b/browser/metro/base/content/startui/Start.xul @@ -62,17 +62,17 @@ </richgrid> </vbox> <vbox id="start-bookmarks" class="meta-section"> <label class="meta-section-title wide-title" value="&bookmarksHeader.label;"/> <html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')"> &narrowBookmarksHeader.label; </html:div> - <richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="2"> + <richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="3"> <richgriditem/> <richgriditem/> </richgrid> </vbox> <vbox id="start-history" class="meta-section"> <label class="meta-section-title wide-title" value="&recentHistoryHeader.label;"/> <html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-history')">
--- a/browser/metro/locales/generic/profile/bookmarks.json.in +++ b/browser/metro/locales/generic/profile/bookmarks.json.in @@ -1,14 +1,17 @@ #filter substitution {"type":"text/x-moz-place-container","root":"placesRoot","children": [{"type":"text/x-moz-place-container","title":"@bookmarks_title@","annos":[{"name":"metro/bookmarksRoot","expires":4,"type":1,"value":1}], "children": [ {"index":1,"title":"@firefox_about@", "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/about/", "iconUri":"data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AVVBO%2F1NOTv9UT07%2FVVBOAFVQTwBUT04AU05N%2F1ROTv9VT0%2F%2FVVBPAFROTgBUT04AVE5O%2F1VQTv9VUE7%2F%2F%2F%2F%2FAFVQTv9UTk7%2FVE5O%2F1NOTgBUTk4AVE9OAFRPTvdTTk7%2FVE5O%2F1NOTgBVUE4AU05OAFVQTv9UTk7%2FU05N%2F%2F%2F%2F%2FwBTTk7%2FVE5O%2F1VQTv9VUE4AVVBOAFROTgBUT073VE5O%2F1ROTv9TTk4AVE5OAFROTgBVUE7%2FVE5O%2F1VQT%2F%2F%2F%2F%2F8AVE5O%2F1VQTv9UTk7%2FVE5OAFRPTgBUTk4AVE9O91VQT%2F9UTk7%2FU05OAFRPTgBUT04AVE5O%2F1NOTv9UTk7%2F%2F%2F%2F%2FAFROTv9UTk7%2FVVBP%2F1ROTgBUTk4AVE5OAFVQTvdUTk7%2FU05N%2F1NOTQBTTk4AU05OAFVPT%2F9TTk7%2FVE5O%2F%2F%2F%2F%2FwBUTk7%2FVE9O%2F1ROTv9UTk4AVU9PAFVQTwBVT0%2F3VE9O%2F1ROTv9UTk4AVE9OAFNOTgBVUE7%2FU05O%2F1ROTv%2F%2F%2F%2F8AVE9O%2F1VQT%2F9VUE%2F%2FVE5OAFVQTgBUTk4AVE9O91ROTv9UTk7%2FVE5OAFNOTgBUTk4AVE5O%2F1NOTf9UTk7%2F%2F%2F%2F%2FAFVQTv9UT07%2FU05N%2F1ROTgBVUE4AVE5OAFVQTutVUE7%2FVE9O%2F1NOTQBUT04AVVBOAFROTv9UTk7%2FVE9O%2F1ROTiFUTk7%2FVE5O%2F1NOTf9UTk4%2B%2F%2F%2F%2FAP%2F%2F%2FwBUTk7%2FVVBO%2F1ROTv9UTk4%2BVE9OAFRPTgBTTk3%2FVE5O%2F1ROTv9UT05lU05O%2F1ROTv9VUE7%2FU05N%2F1VQToBUT06eU05O%2F1RPTv9UTk7%2FVE9O%2F1VPT51VT0%2BaVE5O%2F1ROTv9VT0%2F%2FVE5Or1VQTv9UTk7%2FU05N%2F1ROTv9TTk7%2FVVBO%2F1NOTf9TTk7%2FU05N%2BVNOTv9UTk7%2FVVBP%2F1RPTv9VUE%2F%2FU05OzP%2F%2F%2FwBUT07%2FU05N%2F1ROTo%2F%2F%2F%2F8AVE9O%2F1RPTv9UTk7%2FVE5OqFROTgBVUE5hU05O%2F1VQTv9TTk7%2FVE9O31ROTgD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2FAAD%2F%2FwAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAgAAAAAAAAACIYQAA%2F%2F8AAP%2F%2FAAAoAAAAIAAAAEAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk3BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NAFNOTQBTTk0AU05NAFNOTQBTTk3LU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk1WU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NkVNOTQBTTk0AU05NHFNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk29U05NHFNOTQBTTk0cU05N%2B1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2FlNOTfBTTk3%2BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N9lNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfb%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk1aU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3P%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTahTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3bU05N21NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk12%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NwVNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05NHFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3FNOTQBTTk0AU05NqVNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NqVNOTQD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05NAFNOTTlTTk29U05N51NOTdxTTk0AU05NAFNOTXZTTk3cU05N9lNOTf9TTk37U05N51NOTXVTTk0AU05NAFNOTQBTTk0AU05NOFNOTc9TTk32U05N%2F1NOTf9TTk3wU05Nz1NOTThTTk0AU05NAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8A4BwPAAAADwAAAA4AAAAeBAGAH8cH4H%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F" }, - {"index":2,"title":"@firefox_community@", "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/contribute/", + {"index":2,"title":"@firefox_feedback@", "type":"text/x-moz-place", "uri":"https://input.mozilla.org/feedback/metrofirefox", + "iconUri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA0xJREFUeNpUU11sFFUYPffO3d2Z7i677NIuNM3uUJRWBEMUTLARwYAlEEwISVMDPKHiz4OgiYEmYuID+OArPvDog0Ejhgcx4UdDaCk2soiW7lpafnZp6lK3u9vd7czszNwZ72wpwZN8+e795p5zJ9/5LnFdF1/K0g8ATh41eDratWtDt1Z4J8gbm9PhVBsWMCriioifRNzwCpXx880P5LGA621+jq89UfIF+/cXRjq9/T6Dg4jct+8rHD1Qx+R3152rf2iXtxdvflErGte8M1SQ93uLtwei2Dl7e8Aje2sPuQaQr3McP/URPjmtoPddlR7e477Bg6HL6suxvqYAIdgSi0tNwt7+GFIpHxYxFJHwcdSPsWoDfYd24vXPVXTu7sa2T5+XazV6es2u1uVUouSllc/6TC1vw6no2LAp/ESsKUiBKhjKyWfgBvz4djgK9ZUE4kklUn9kHaO+AGlv75D8lu4gqC7crs8lsGzHQZw9U4J7qYI/b87i9tUHcCAhM2libs5G1/Y45ovGViZ6uCQYpWCUQKtRhF5d1RRhvkwzT08UUM5kUZsuwHYY0lkKhXIkUjLmy7ybNQwnABuQV1AMXqzB+uUGpgtNU3Ci92tY33wvrOKC5EA3CBKqhuiSEsqaBqNqMxrwkXptThCEX7GEhNawgxVCzMPAhQ/w46ZfcXxjDtWZIsxqGa915IFyCfW/H0EJEY2J/v/1T97qCS+VsHqNjIejNtalGF7YKCMo2xjJ/QucuwK74z0klTwOrc+iUTAxNVSGRJxRZnP3zMSo+WIkzpS2pIyOdTHYJkf6t3nk7priP8ZwLPgWelrG8f6OWcSlBioTFWR+l+CPxi6RkwEaUiQMMkLWb+0PQw4wcM5AZQn3y34UqwZayDySbRZUNQBdWJodruNeTqkuT7Z2SdsYMXUXt8RVb06NmUG5haBF4aCWjbhPx6qQgYQM+B3qDT5yd2yMZyyjOKlvmblXusO8Zn1m8BEx0r0mcc+nh7X2eIiis8uPSIxAV8QBMR4zUxZygwSubmvFGadHVG89eUyLOBViKx2CI47r7uaUqNxecEcOECyNUINb7rl6xR34sG7fX+T8T+BpIZGeE9H+uDQtIvs0cRH/CTAA8jZdCaXrR1kAAAAASUVORK5CYII=" + }, + {"index":3,"title":"@firefox_community@", "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/contribute/", "iconUri":"data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AVVBO%2F1NOTv9UT07%2FVVBOAFVQTwBUT04AU05N%2F1ROTv9VT0%2F%2FVVBPAFROTgBUT04AVE5O%2F1VQTv9VUE7%2F%2F%2F%2F%2FAFVQTv9UTk7%2FVE5O%2F1NOTgBUTk4AVE9OAFRPTvdTTk7%2FVE5O%2F1NOTgBVUE4AU05OAFVQTv9UTk7%2FU05N%2F%2F%2F%2F%2FwBTTk7%2FVE5O%2F1VQTv9VUE4AVVBOAFROTgBUT073VE5O%2F1ROTv9TTk4AVE5OAFROTgBVUE7%2FVE5O%2F1VQT%2F%2F%2F%2F%2F8AVE5O%2F1VQTv9UTk7%2FVE5OAFRPTgBUTk4AVE9O91VQT%2F9UTk7%2FU05OAFRPTgBUT04AVE5O%2F1NOTv9UTk7%2F%2F%2F%2F%2FAFROTv9UTk7%2FVVBP%2F1ROTgBUTk4AVE5OAFVQTvdUTk7%2FU05N%2F1NOTQBTTk4AU05OAFVPT%2F9TTk7%2FVE5O%2F%2F%2F%2F%2FwBUTk7%2FVE9O%2F1ROTv9UTk4AVU9PAFVQTwBVT0%2F3VE9O%2F1ROTv9UTk4AVE9OAFNOTgBVUE7%2FU05O%2F1ROTv%2F%2F%2F%2F8AVE9O%2F1VQT%2F9VUE%2F%2FVE5OAFVQTgBUTk4AVE9O91ROTv9UTk7%2FVE5OAFNOTgBUTk4AVE5O%2F1NOTf9UTk7%2F%2F%2F%2F%2FAFVQTv9UT07%2FU05N%2F1ROTgBVUE4AVE5OAFVQTutVUE7%2FVE9O%2F1NOTQBUT04AVVBOAFROTv9UTk7%2FVE9O%2F1ROTiFUTk7%2FVE5O%2F1NOTf9UTk4%2B%2F%2F%2F%2FAP%2F%2F%2FwBUTk7%2FVVBO%2F1ROTv9UTk4%2BVE9OAFRPTgBTTk3%2FVE5O%2F1ROTv9UT05lU05O%2F1ROTv9VUE7%2FU05N%2F1VQToBUT06eU05O%2F1RPTv9UTk7%2FVE9O%2F1VPT51VT0%2BaVE5O%2F1ROTv9VT0%2F%2FVE5Or1VQTv9UTk7%2FU05N%2F1ROTv9TTk7%2FVVBO%2F1NOTf9TTk7%2FU05N%2BVNOTv9UTk7%2FVVBP%2F1RPTv9VUE%2F%2FU05OzP%2F%2F%2FwBUT07%2FU05N%2F1ROTo%2F%2F%2F%2F8AVE9O%2F1RPTv9UTk7%2FVE5OqFROTgBVUE5hU05O%2F1VQTv9TTk7%2FVE9O31ROTgD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2FAAD%2F%2FwAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAgAAAAAAAAACIYQAA%2F%2F8AAP%2F%2FAAAoAAAAIAAAAEAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk3BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NAFNOTQBTTk0AU05NAFNOTQBTTk3LU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk1WU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NkVNOTQBTTk0AU05NHFNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk29U05NHFNOTQBTTk0cU05N%2B1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2FlNOTfBTTk3%2BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N9lNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfb%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk1aU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3P%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTahTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3bU05N21NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk12%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NwVNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05NHFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3FNOTQBTTk0AU05NqVNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NqVNOTQD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05NAFNOTTlTTk29U05N51NOTdxTTk0AU05NAFNOTXZTTk3cU05N9lNOTf9TTk37U05N51NOTXVTTk0AU05NAFNOTQBTTk0AU05NOFNOTc9TTk32U05N%2F1NOTf9TTk3wU05Nz1NOTThTTk0AU05NAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8A4BwPAAAADwAAAA4AAAAeBAGAH8cH4H%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F" } ] }] }
--- a/browser/modules/SitePermissions.jsm +++ b/browser/modules/SitePermissions.jsm @@ -181,16 +181,19 @@ let gPermissionObject = { return SitePermissions.SESSION; return SitePermissions.ALLOW; } }, "desktop-notification": {}, + "camera": {}, + "microphone": {}, + "popup": { getDefault: function () { return Services.prefs.getBoolPref("dom.disable_open_during_load") ? SitePermissions.BLOCK : SitePermissions.ALLOW; } }, "install": {
--- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -115,41 +115,62 @@ function prompt(aContentWindow, aCallID, requestType = "Microphone"; else if (videoDevices.length) requestType = "Camera"; else { denyRequest(aCallID, "NO_DEVICES_FOUND"); return; } - let host = aContentWindow.document.documentURIObject.host; + let uri = aContentWindow.document.documentURIObject; let browser = getBrowserForWindow(aContentWindow); let chromeDoc = browser.ownerDocument; let chromeWin = chromeDoc.defaultView; let stringBundle = chromeWin.gNavigatorBundle; let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", - [ host ]); + [ uri.host ]); let mainAction = { label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, stringBundle.getString("getUserMedia.shareSelectedDevices.label")), accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"), // The real callback will be set during the "showing" event. The // empty function here is so that PopupNotifications.show doesn't // reject the action. callback: function() {} }; - let secondaryActions = [{ - label: stringBundle.getString("getUserMedia.denyRequest.label"), - accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), - callback: function () { - denyRequest(aCallID); + let secondaryActions = [ + { + label: stringBundle.getString("getUserMedia.always.label"), + accessKey: stringBundle.getString("getUserMedia.always.accesskey"), + callback: function () { + mainAction.callback(true); + } + }, + { + label: stringBundle.getString("getUserMedia.denyRequest.label"), + accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), + callback: function () { + denyRequest(aCallID); + } + }, + { + label: stringBundle.getString("getUserMedia.never.label"), + accessKey: stringBundle.getString("getUserMedia.never.accesskey"), + callback: function () { + denyRequest(aCallID); + let perms = Services.perms; + if (audioDevices.length) + perms.add(uri, "microphone", perms.DENY_ACTION); + if (videoDevices.length) + perms.add(uri, "camera", perms.DENY_ACTION); + } } - }]; + ]; let options = { eventCallback: function(aTopic, aNewBrowser) { if (aTopic == "swapping") return true; if (aTopic != "showing") return false; @@ -183,28 +204,39 @@ function prompt(aContentWindow, aCallID, listDevices(camMenupopup, videoDevices); listDevices(micMenupopup, audioDevices); if (requestType == "CameraAndMicrophone") { let stringBundle = chromeDoc.defaultView.gNavigatorBundle; addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); } - this.mainAction.callback = function() { + this.mainAction.callback = function(aRemember) { let allowedDevices = Cc["@mozilla.org/supports-array;1"] .createInstance(Ci.nsISupportsArray); + let perms = Services.perms; if (videoDevices.length) { let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; - if (videoDeviceIndex != "-1") + let allowCamera = videoDeviceIndex != "-1"; + if (allowCamera) allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); + if (aRemember) { + perms.add(uri, "camera", + allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION); + } } if (audioDevices.length) { let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; - if (audioDeviceIndex != "-1") + let allowMic = audioDeviceIndex != "-1"; + if (allowMic) allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); + if (aRemember) { + perms.add(uri, "microphone", + allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION); + } } if (allowedDevices.Count() == 0) { denyRequest(aCallID); return; } Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); @@ -247,30 +279,39 @@ function showBrowserSpecificIndicator(aB return; } let chromeWin = aBrowser.ownerDocument.defaultView; let stringBundle = chromeWin.gNavigatorBundle; let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); + let uri = aBrowser.contentWindow.document.documentURIObject; let windowId = aBrowser.contentWindow .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .currentInnerWindowID; let mainAction = { label: stringBundle.getString("getUserMedia.continueSharing.label"), accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"), callback: function () {}, dismiss: true }; let secondaryActions = [{ label: stringBundle.getString("getUserMedia.stopSharing.label"), accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"), callback: function () { + let perms = Services.perms; + if (hasVideo.value && + perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION) + perms.remove(uri.host, "camera"); + if (hasAudio.value && + perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION) + perms.remove(uri.host, "microphone"); + Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); } }]; let options = { hideNotNow: true, dismissed: true, eventCallback: function(aTopic) aTopic == "swapping" };
--- a/browser/themes/linux/preferences/aboutPermissions.css +++ b/browser/themes/linux/preferences/aboutPermissions.css @@ -92,16 +92,22 @@ list-style-image: url(chrome://global/skin/icons/question-64.png); } .pref-icon[type="plugins"] { list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png); } .pref-icon[type="fullscreen"] { list-style-image: url(chrome://global/skin/icons/question-64.png); } +.pref-icon[type="camera"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} +.pref-icon[type="microphone"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} .pref-title { font-size: 125%; margin-bottom: 0; font-weight: bold; } .pref-menulist {
--- a/browser/themes/osx/preferences/aboutPermissions.css +++ b/browser/themes/osx/preferences/aboutPermissions.css @@ -102,16 +102,22 @@ list-style-image: url(chrome://global/skin/icons/question-64.png); } .pref-icon[type="plugins"] { list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png); } .pref-icon[type="fullscreen"] { list-style-image: url(chrome://global/skin/icons/question-64.png); } +.pref-icon[type="camera"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} +.pref-icon[type="microphone"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} @media (min-resolution: 2dppx) { .pref-icon[type="geo"] { list-style-image: url(chrome://browser/skin/Geolocation-64@2x.png); } } .pref-title {
--- a/browser/themes/shared/devtools/common.css +++ b/browser/themes/shared/devtools/common.css @@ -127,44 +127,16 @@ .devtools-autocomplete-listbox:focus.light-theme > richlistitem[selected] > .initial-value { color: #222; } .devtools-autocomplete-listbox.light-theme > richlistitem > label { color: #666; } -/* Responsive container */ - -.devtools-responsive-container { - -moz-box-orient: horizontal; -} - -@media (max-width: 700px) { - .devtools-responsive-container { - -moz-box-orient: vertical; - } - - .devtools-responsive-container > .devtools-side-splitter { - border: 0; - margin: 0; - border-top: 1px solid black; - min-height: 3px; - height: 3px; - margin-bottom: -3px; - /* In some edge case the cursor is not changed to n-resize */ - cursor: n-resize; - } - - .devtools-responsive-container > .devtools-sidebar-tabs { - min-height: 35vh; - max-height: 75vh; - } -} - /* Tooltip widget (see browser/devtools/shared/widgets/Tooltip.js) */ .devtools-tooltip .panel-arrowcontent { padding: 4px; } .devtools-tooltip .panel-arrowcontainer { /* Reseting the transition used when panels are shown */
--- a/browser/themes/shared/devtools/dark-theme.css +++ b/browser/themes/shared/devtools/dark-theme.css @@ -310,15 +310,19 @@ div.CodeMirror span.eval-text { .devtools-horizontal-splitter { border-bottom: 1px solid black; } .devtools-side-splitter { -moz-border-end: 1px solid black; } +.devtools-responsive-container > .devtools-side-splitter { + border-top: 1px solid black; +} + .devtools-textinput, .devtools-searchinput { background-color: rgba(24, 29, 32, 1); color: rgba(184, 200, 217, 1); } %include toolbars.inc.css
--- a/browser/themes/shared/devtools/light-theme.css +++ b/browser/themes/shared/devtools/light-theme.css @@ -309,9 +309,13 @@ div.CodeMirror span.eval-text { .devtools-horizontal-splitter { border-bottom: 1px solid #aaa; } .devtools-side-splitter { -moz-border-end: 1px solid #aaa; } +.devtools-responsive-container > .devtools-side-splitter { + border-top: 1px solid #aaa; +} + %include toolbars.inc.css
--- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -11,16 +11,43 @@ so both the left and right margins are set via js, while the start margin is always overridden here. */ } .generic-toggled-side-pane[animated] { transition: margin 0.25s ease-in-out; } +/* Responsive container */ + +.devtools-responsive-container { + -moz-box-orient: horizontal; +} + +@media (max-width: 700px) { + .devtools-responsive-container { + -moz-box-orient: vertical; + } + + .devtools-responsive-container > .devtools-side-splitter { + border: 0; + margin: 0; + min-height: 3px; + height: 3px; + margin-bottom: -3px; + /* In some edge case the cursor is not changed to n-resize */ + cursor: n-resize; + } + + .devtools-responsive-container > .devtools-sidebar-tabs { + min-height: 35vh; + max-height: 75vh; + } +} + /* BreacrumbsWidget */ .breadcrumbs-widget-container { -moz-margin-end: 3px; max-height: 25px; /* Set max-height for proper sizing on linux */ height: 25px; /* Set height to prevent starting small waiting for content */ /* A fake 1px-shadow is included in the border-images of the breadcrumbs-widget-items, to match toolbar-buttons style.
--- a/browser/themes/windows/browser-aero.css +++ b/browser/themes/windows/browser-aero.css @@ -218,29 +218,23 @@ } @media (-moz-windows-glass) { #main-window[sizemode=fullscreen]:not(:-moz-lwtheme) { -moz-appearance: none; background-color: #556; } - /* Use inverted icons for glassed toolbars */ - #TabsToolbar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme), - #TabsToolbar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme), + /* Use inverted icons for non-fogged glassed toolbars */ #toolbar-menubar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme), #toolbar-menubar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme), #toolbar-menubar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme), #toolbar-menubar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme), - #TabsToolbar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme), - #TabsToolbar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme), #toolbar-menubar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme), - #toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme), - #TabsToolbar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme), - #TabsToolbar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) { + #toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) { list-style-image: url("chrome://browser/skin/Toolbar-inverted.png"); } /* Glass Fog */ #TabsToolbar:not(:-moz-lwtheme) { background-image: none; position: relative;
--- a/browser/themes/windows/downloads/indicator-aero.css +++ b/browser/themes/windows/downloads/indicator-aero.css @@ -1,30 +1,28 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@media (-moz-windows-compositor) { +@media (-moz-windows-glass) { /* The following rules are for the downloads indicator when in its normal, non-downloading, non-paused state (ie, it's just showing the downloads button icon). */ - :-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme), + #toolbar-menubar #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme), /* The following rules are for the downloads indicator when in its paused or undetermined progress state. We use :not([counter]) as a shortcut for :-moz-any([progress], [paused]). */ - /* This is the case where the downloads indicator has been moved next to the menubar as well as - the case where the downloads indicator is in the tabstrip toolbar. */ - #toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter, - #TabsToolbar #downloads-button:not(:-moz-lwtheme):not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter { + /* This is the case where the downloads indicator has been moved next to the menubar. */ + #toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter { background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 0, 198, 18, 180); } - :-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-indicator-counter:not(:-moz-lwtheme) { + #toolbar-menubar #downloads-indicator-counter:not(:-moz-lwtheme) { color: white; text-shadow: 0 0 1px rgba(0,0,0,.7), 0 1px 1.5px rgba(0,0,0,.5); } } #downloads-indicator-counter { margin-bottom: -1px;
--- a/browser/themes/windows/preferences/aboutPermissions.css +++ b/browser/themes/windows/preferences/aboutPermissions.css @@ -95,16 +95,22 @@ list-style-image: url(chrome://global/skin/icons/question-64.png); } .pref-icon[type="plugins"] { list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png); } .pref-icon[type="fullscreen"] { list-style-image: url(chrome://global/skin/icons/question-64.png); } +.pref-icon[type="camera"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} +.pref-icon[type="microphone"] { + list-style-image: url(chrome://global/skin/icons/question-64.png); +} .pref-title { font-size: 125%; margin-bottom: 0; font-weight: bold; } .pref-menulist {
--- a/build/mobile/robocop/Makefile.in +++ b/build/mobile/robocop/Makefile.in @@ -40,16 +40,17 @@ java-tests := \ PP_TARGETS += testconstants testconstants-dep := $(dir-tests)/TestConstants.java testconstants := $(TESTPATH)/TestConstants.java.in testconstants_PATH := $(dir-tests) PP_TARGETS += manifest manifest := $(srcdir)/AndroidManifest.xml.in manifest_TARGET := AndroidManifest.xml +ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml # Install robocop configs and helper INSTALL_TARGETS += robocop robocop_TARGET := libs robocop_DEST := $(CURDIR) robocop_FILES := \ $(TESTPATH)/robocop.ini \ $(TESTPATH)/robocop_autophone.ini \
--- a/build/mobile/robocop/moz.build +++ b/build/mobile/robocop/moz.build @@ -6,17 +6,17 @@ DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] main = add_android_eclipse_project('Robocop', OBJDIR + '/AndroidManifest.xml') main.package_name = 'org.mozilla.roboexample.test' main.res = SRCDIR + '/res' main.recursive_make_targets += [ OBJDIR + '/AndroidManifest.xml', - TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java'] + '../../../mobile/android/base/tests/TestConstants.java'] main.extra_jars += [SRCDIR + '/robotium-solo-4.3.1.jar'] main.assets = TOPSRCDIR + '/mobile/android/base/tests/assets' main.referenced_projects += ['Fennec'] main.add_classpathentry('harness', SRCDIR, dstdir='harness/org/mozilla/gecko') main.add_classpathentry('src', TOPSRCDIR + '/mobile/android/base/tests', dstdir='src/org/mozilla/gecko/tests')
--- a/config/makefiles/java-build.mk +++ b/config/makefiles/java-build.mk @@ -14,37 +14,40 @@ export:: classes classes: $(call mkdir_deps,classes) endif #} JAVAFILES ifdef ANDROID_APK_NAME #{ android_res_dirs := $(addprefix $(srcdir)/,$(or $(ANDROID_RES_DIRS),res)) _ANDROID_RES_FLAG := $(addprefix -S ,$(android_res_dirs)) _ANDROID_ASSETS_FLAG := $(addprefix -A ,$(ANDROID_ASSETS_DIR)) +android_manifest := $(or $(ANDROID_MANIFEST_FILE),AndroidManifest.xml) GENERATED_DIRS += classes classes.dex: $(call mkdir_deps,classes) classes.dex: R.java classes.dex: $(ANDROID_APK_NAME).ap_ +classes.dex: $(ANDROID_EXTRA_JARS) classes.dex: $(JAVAFILES) - $(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^) + $(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^) \ + $(if $(strip $(ANDROID_EXTRA_JARS)),-classpath $(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS)))) $(DX) --dex --output=$@ classes $(ANDROID_EXTRA_JARS) # R.java and $(ANDROID_APK_NAME).ap_ are both produced by aapt. To # save an aapt invocation, we produce them both at the same time. R.java: .aapt.deps $(ANDROID_APK_NAME).ap_: .aapt.deps # This uses the fact that Android resource directories list all # resource files one subdirectory below the parent resource directory. android_res_files := $(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(android_res_dirs))))) -.aapt.deps: AndroidManifest.xml $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR)) +.aapt.deps: $(android_manifest) $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR)) $(AAPT) package -f -M $< -I $(ANDROID_SDK)/android.jar $(_ANDROID_RES_FLAG) $(_ANDROID_ASSETS_FLAG) \ -J ${@D} \ -F $(ANDROID_APK_NAME).ap_ @$(TOUCH) $@ $(ANDROID_APK_NAME)-unsigned-unaligned.apk: $(ANDROID_APK_NAME).ap_ classes.dex cp $< $@ $(ZIP) -0 $@ classes.dex @@ -61,19 +64,16 @@ GARBAGE += \ classes.dex \ $(ANDROID_APK_NAME).ap_ \ $(ANDROID_APK_NAME)-unsigned-unaligned.apk \ $(ANDROID_APK_NAME)-unaligned.apk \ $(ANDROID_APK_NAME).apk \ $(NULL) JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar -ifdef ANDROID_EXTRA_JARS #{ -JAVA_CLASSPATH := $(JAVA_CLASSPATH):$(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS))) -endif #} ANDROID_EXTRA_JARS # Include Android specific java flags, instead of what's in rules.mk. include $(topsrcdir)/config/android-common.mk endif #} ANDROID_APK_NAME ifdef JAVA_JAR_TARGETS #{ # Arg 1: Output target name with .jar suffix, like jars/jarfile.jar.
--- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -9,16 +9,17 @@ #include "nsHashPropertyBag.h" #ifdef MOZ_WIDGET_GONK #include "nsIAudioManager.h" #endif #include "nsIDOMFile.h" #include "nsIEventTarget.h" #include "nsIUUIDGenerator.h" #include "nsIScriptGlobalObject.h" +#include "nsIPermissionManager.h" #include "nsIPopupWindowManager.h" #include "nsISupportsArray.h" #include "nsIDocShell.h" #include "nsIDocument.h" #include "nsISupportsPrimitives.h" #include "nsIInterfaceRequestorUtils.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/MediaStreamTrackBinding.h" @@ -28,18 +29,16 @@ // For PR_snprintf #include "prprf.h" #include "nsJSUtils.h" #include "nsDOMFile.h" #include "nsGlobalWindow.h" -#include "mozilla/Preferences.h" - /* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */ #include "MediaEngineDefault.h" #if defined(MOZ_WEBRTC) #include "MediaEngineWebRTC.h" #endif #ifdef MOZ_B2G #include "MediaPermissionGonk.h" @@ -903,16 +902,23 @@ public: // MUST happen after ErrorCallbackRunnable Run()s, as it checks the active window list NS_DispatchToMainThread(new GetUserMediaListenerRemove(mWindowID, mListener)); } return NS_OK; } nsresult + SetContraints(const MediaStreamConstraintsInternal& aConstraints) + { + mConstraints = aConstraints; + return NS_OK; + } + + nsresult SetAudioDevice(MediaDevice* aAudioDevice) { mAudioDevice = aAudioDevice; mDeviceChosen = true; return NS_OK; } nsresult @@ -1070,17 +1076,21 @@ public: , mLoopbackAudioDevice(aAudioLoopbackDev) , mLoopbackVideoDevice(aVideoLoopbackDev) {} NS_IMETHOD Run() { NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread"); - MediaEngine *backend = mManager->GetBackend(mWindowId); + nsRefPtr<MediaEngine> backend; + if (mConstraints.mFake) + backend = new MediaEngineDefault(); + else + backend = mManager->GetBackend(mWindowId); ScopedDeletePtr<SourceSet> final (GetSources(backend, mConstraints.mVideom, &MediaEngine::EnumerateVideoDevices, mLoopbackVideoDevice)); { ScopedDeletePtr<SourceSet> s (GetSources(backend, mConstraints.mAudiom, &MediaEngine::EnumerateAudioDevices, mLoopbackAudioDevice)); @@ -1410,24 +1420,69 @@ MediaManager::GetUserMedia(JSContext* aC if (c.mPicture) { // ShowFilePickerForMimeType() must run on the Main Thread! (on Android) runnable->Arm(); NS_DispatchToMainThread(runnable); return NS_OK; } #endif // XXX No full support for picture in Desktop yet (needs proper UI) - if (aPrivileged || c.mFake) { + if (aPrivileged || + (c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) { runnable->Arm(); mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } else { + // Check if this site has persistent permissions. + nsresult rv; + nsCOMPtr<nsIPermissionManager> permManager = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION; + if (c.mAudio) { + rv = permManager->TestExactPermissionFromPrincipal( + aWindow->GetExtantDoc()->NodePrincipal(), "microphone", &audioPerm); + NS_ENSURE_SUCCESS(rv, rv); + if (audioPerm == nsIPermissionManager::PROMPT_ACTION) { + audioPerm = nsIPermissionManager::UNKNOWN_ACTION; + } + } + + uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION; + if (c.mVideo) { + rv = permManager->TestExactPermissionFromPrincipal( + aWindow->GetExtantDoc()->NodePrincipal(), "camera", &videoPerm); + NS_ENSURE_SUCCESS(rv, rv); + if (videoPerm == nsIPermissionManager::PROMPT_ACTION) { + videoPerm = nsIPermissionManager::UNKNOWN_ACTION; + } + } + + if ((!c.mAudio || audioPerm) && (!c.mVideo || videoPerm)) { + // All permissions we were about to request already have a saved value. + if (c.mAudio && audioPerm == nsIPermissionManager::DENY_ACTION) { + c.mAudio = false; + runnable->SetContraints(c); + } + if (c.mVideo && videoPerm == nsIPermissionManager::DENY_ACTION) { + c.mVideo = false; + runnable->SetContraints(c); + } + + runnable->Arm(); + if (!c.mAudio && !c.mVideo) { + return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); + } + + return mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + // Ask for user permission, and dispatch runnable (or not) when a response // is received via an observer notification. Each call is paired with its // runnable by a GUID. - nsresult rv; nsCOMPtr<nsIUUIDGenerator> uuidgen = do_GetService("@mozilla.org/uuid-generator;1", &rv); NS_ENSURE_SUCCESS(rv, rv); // Generate a call ID. nsID id; rv = uuidgen->GenerateUUIDInPlace(&id); NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/media/MediaManager.h +++ b/dom/media/MediaManager.h @@ -14,16 +14,17 @@ #include "nsObserverService.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "nsPIDOMWindow.h" #include "nsIDOMNavigatorUserMedia.h" #include "nsXULAppAPI.h" #include "mozilla/Attributes.h" +#include "mozilla/Preferences.h" #include "mozilla/StaticPtr.h" #include "mozilla/dom/MediaStreamTrackBinding.h" #include "prlog.h" #include "DOMMediaStream.h" #ifdef MOZ_WEBRTC #include "mtransport/runnable_utils.h" #endif @@ -97,22 +98,26 @@ public: return mStream->AsSourceStream(); } // mVideo/AudioSource are set by Activate(), so we assume they're capturing // if set and represent a real capture device. bool CapturingVideo() { NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); - return mVideoSource && !mVideoSource->IsFake() && !mStopped; + return mVideoSource && !mStopped && + (!mVideoSource->IsFake() || + Preferences::GetBool("media.navigator.permission.fake")); } bool CapturingAudio() { NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); - return mAudioSource && !mAudioSource->IsFake() && !mStopped; + return mAudioSource && !mStopped && + (!mAudioSource->IsFake() || + Preferences::GetBool("media.navigator.permission.fake")); } void SetStopped() { mStopped = true; } // implement in .cpp to avoid circular dependency with MediaOperationRunnable
--- a/dom/system/OSFileConstants.cpp +++ b/dom/system/OSFileConstants.cpp @@ -696,16 +696,17 @@ static const dom::ConstantSpec gWinPrope INT_CONSTANT(INVALID_FILE_ATTRIBUTES), // GetNamedSecurityInfo and SetNamedSecurityInfo constants INT_CONSTANT(UNPROTECTED_DACL_SECURITY_INFORMATION), INT_CONSTANT(SE_FILE_OBJECT), INT_CONSTANT(DACL_SECURITY_INFORMATION), // Errors + INT_CONSTANT(ERROR_INVALID_HANDLE), INT_CONSTANT(ERROR_ACCESS_DENIED), INT_CONSTANT(ERROR_DIR_NOT_EMPTY), INT_CONSTANT(ERROR_FILE_EXISTS), INT_CONSTANT(ERROR_ALREADY_EXISTS), INT_CONSTANT(ERROR_FILE_NOT_FOUND), INT_CONSTANT(ERROR_NO_MORE_FILES), INT_CONSTANT(ERROR_PATH_NOT_FOUND),
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -36,16 +36,17 @@ import org.mozilla.gecko.home.BrowserSea import org.mozilla.gecko.home.HomeBanner; import org.mozilla.gecko.home.HomeConfigInvalidator; import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.home.SearchEngine; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.prompts.Prompt; +import org.mozilla.gecko.prompts.PromptListItem; import org.mozilla.gecko.sync.setup.SyncAccounts; import org.mozilla.gecko.toolbar.AutocompleteHandler; import org.mozilla.gecko.toolbar.BrowserToolbar; import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.MenuUtils; import org.mozilla.gecko.util.StringUtils; @@ -86,16 +87,17 @@ import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.Window; import android.view.animation.Interpolator; import android.widget.RelativeLayout; +import android.widget.ListView; import android.widget.Toast; import android.widget.ViewFlipper; abstract public class BrowserApp extends GeckoApp implements TabsPanel.TabsLayoutChangeListener, PropertyAnimator.PropertyAnimationListener, View.OnKeyListener, GeckoLayerClient.OnMetricsChangedListener, @@ -501,16 +503,25 @@ abstract public class BrowserApp extends }); mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() { public void onFilter(String searchText, AutocompleteHandler handler) { filterEditingMode(searchText, handler); } }); + mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (isHomePagerVisible()) { + mHomePager.onToolbarFocusChange(hasFocus); + } + } + }); + mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() { public void onStartEditing() { // Temporarily disable doorhanger notifications. mDoorHangerPopup.disable(); } }); mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() { @@ -2409,17 +2420,17 @@ abstract public class BrowserApp extends if (type == GuestModeDialog.ENTERING) { titleString = R.string.new_guest_session_title; msgString = R.string.new_guest_session_text; } else { titleString = R.string.exit_guest_session_title; msgString = R.string.exit_guest_session_text; } - ps.show(res.getString(titleString), res.getString(msgString), null, false); + ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE); } public void subscribeToFeeds(Tab tab) { if (!tab.hasFeeds()) { return; } JSONObject args = new JSONObject();
--- a/mobile/android/base/EventDispatcher.java +++ b/mobile/android/base/EventDispatcher.java @@ -101,18 +101,22 @@ public final class EventDispatcher { } } public static void sendResponse(JSONObject message, JSONObject response) { try { response.put(GUID, message.getString(GUID)); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Return", response.toString())); - } catch(Exception ex) { } + } catch (Exception ex) { + Log.e(LOGTAG, "Unable to send response", ex); + } } public static void sendError(JSONObject message, JSONObject error) { try { error.put(GUID, message.getString(GUID)); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Error", error.toString())); - } catch(Exception ex) { } + } catch (Exception ex) { + Log.e(LOGTAG, "Unable to send error", ex); + } } }
--- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -411,22 +411,18 @@ public abstract class GeckoApp return onCreateOptionsMenu(menu); } return super.onCreatePanelMenu(featureId, menu); } @Override public boolean onPreparePanel(int featureId, View view, Menu menu) { - if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) { - if (menu instanceof GeckoMenu) { - ((GeckoMenu) menu).refresh(); - } + if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) return onPrepareOptionsMenu(menu); - } return super.onPreparePanel(featureId, view, menu); } @Override public boolean onMenuOpened(int featureId, Menu menu) { // exit full-screen mode whenever the menu is opened if (mLayerView != null && mLayerView.isFullScreen()) { @@ -681,16 +677,46 @@ public abstract class GeckoApp } JSONObject handlersJSON = new JSONObject(); handlersJSON.put("apps", new JSONArray(appList)); EventDispatcher.sendResponse(message, handlersJSON); } else if (event.equals("Intent:Open")) { GeckoAppShell.openUriExternal(message.optString("url"), message.optString("mime"), message.optString("packageName"), message.optString("className"), message.optString("action"), message.optString("title")); + } else if (event.equals("Intent:OpenForResult")) { + Intent intent = GeckoAppShell.getOpenURIIntent(this, + message.optString("url"), + message.optString("mime"), + message.optString("action"), + message.optString("title")); + intent.setClassName(message.optString("packageName"), message.optString("className")); + + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + + final JSONObject originalMessage = message; + ActivityHandlerHelper.startIntentForActivity(this, + intent, + new ActivityResultHandler() { + @Override + public void onActivityResult (int resultCode, Intent data) { + JSONObject response = new JSONObject(); + + try { + if (data != null) { + response.put("extras", bundleToJSON(data.getExtras())); + } + response.put("resultCode", resultCode); + } catch (JSONException e) { + Log.w(LOGTAG, "Error building JSON response.", e); + } + + EventDispatcher.sendResponse(originalMessage, response); + } + }); } else if (event.equals("Locale:Set")) { setLocale(message.getString("locale")); } else if (event.equals("NativeApp:IsDebuggable")) { JSONObject ret = new JSONObject(); ret.put("isDebuggable", getIsDebuggable() ? "true" : "false"); EventDispatcher.sendResponse(message, ret); } else if (event.equals("SystemUI:Visibility")) { setSystemUiVisible(message.getBoolean("visible")); @@ -841,16 +867,33 @@ public abstract class GeckoApp GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId)); } } }); } }); } + private JSONObject bundleToJSON(Bundle bundle) { + JSONObject json = new JSONObject(); + if (bundle == null) { + return json; + } + + for (String key : bundle.keySet()) { + try { + json.put(key, bundle.get(key)); + } catch (JSONException e) { + Log.w(LOGTAG, "Error building JSON response.", e); + } + } + + return json; + } + private void addFullScreenPluginView(View view) { if (mFullScreenPluginView != null) { Log.w(LOGTAG, "Already have a fullscreen plugin view"); return; } setFullScreen(true); @@ -1532,16 +1575,17 @@ public abstract class GeckoApp registerEventListener("Image:SetAs"); registerEventListener("Sanitize:ClearHistory"); registerEventListener("Update:Check"); registerEventListener("Update:Download"); registerEventListener("Update:Install"); registerEventListener("PrivateBrowsing:Data"); registerEventListener("Contact:Add"); registerEventListener("Intent:Open"); + registerEventListener("Intent:OpenForResult"); registerEventListener("Intent:GetHandlers"); registerEventListener("Locale:Set"); registerEventListener("NativeApp:IsDebuggable"); registerEventListener("SystemUI:Visibility"); EventListener.registerEvents(); if (SmsManager.getInstance() != null) {
--- a/mobile/android/base/GeckoApplication.java +++ b/mobile/android/base/GeckoApplication.java @@ -64,17 +64,16 @@ public class GeckoApplication extends Ap mLightweightTheme = new LightweightTheme(this); GeckoConnectivityReceiver.getInstance().init(getApplicationContext()); GeckoBatteryManager.getInstance().init(getApplicationContext()); GeckoBatteryManager.getInstance().start(); GeckoNetworkManager.getInstance().init(getApplicationContext()); MemoryMonitor.getInstance().init(getApplicationContext()); - HomeConfigInvalidator.getInstance().init(getApplicationContext()); mInited = true; } public void onActivityPause(GeckoActivityStatus activity) { mInBackground = true; if ((activity.isFinishing() == false) && @@ -111,16 +110,17 @@ public class GeckoApplication extends Ap } @Override public void onCreate() { HardwareUtils.init(getApplicationContext()); Clipboard.init(getApplicationContext()); FilePicker.init(getApplicationContext()); GeckoLoader.loadMozGlue(); + HomeConfigInvalidator.getInstance().init(getApplicationContext()); super.onCreate(); } public boolean isApplicationInBackground() { return mInBackground; } public LightweightTheme getLightweightTheme() {
--- a/mobile/android/base/home/HomeBanner.java +++ b/mobile/android/base/home/HomeBanner.java @@ -14,16 +14,17 @@ import org.mozilla.gecko.animation.Prope import org.mozilla.gecko.animation.PropertyAnimator.Property; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; import android.content.Context; import android.graphics.drawable.Drawable; +import android.os.Build; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -100,21 +101,32 @@ public class HomeBanner extends LinearLa GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null)); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this); - } + } - public void setScrollingPages(boolean scrollingPages) { - mScrollingPages = scrollingPages; - } + @Override + public void setVisibility(int visibility) { + // On pre-Honeycomb devices, setting the visibility to GONE won't actually + // hide the view unless we clear animations first. + if (Build.VERSION.SDK_INT < 11 && visibility == View.GONE) { + clearAnimation(); + } + + super.setVisibility(visibility); + } + + public void setScrollingPages(boolean scrollingPages) { + mScrollingPages = scrollingPages; + } @Override public void handleMessage(String event, JSONObject message) { try { // Store the current message id to pass back to JS in the view's OnClickListener. setTag(message.getString("id")); // Display styled text from an HTML string.
--- a/mobile/android/base/home/HomePager.java +++ b/mobile/android/base/home/HomePager.java @@ -239,16 +239,22 @@ public class HomePager extends ViewPager public boolean dispatchTouchEvent(MotionEvent event) { if (mHomeBanner != null) { mHomeBanner.handleHomeTouch(event); } return super.dispatchTouchEvent(event); } + public void onToolbarFocusChange(boolean hasFocus) { + // We should only enable the banner if the toolbar is not focused and we are on the default page + final boolean enabled = !hasFocus && getCurrentItem() == mDefaultPageIndex; + mHomeBanner.setEnabled(enabled); + } + private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) { // We only care about the adapter if HomePager is currently // loaded, which means it's visible in the activity. if (!mLoaded) { return; } if (mDecor != null) {
--- a/mobile/android/base/home/PanelGridView.java +++ b/mobile/android/base/home/PanelGridView.java @@ -22,54 +22,45 @@ import android.widget.AdapterView; import android.widget.GridView; public class PanelGridView extends GridView implements DatasetBacked, PanelView { private static final String LOGTAG = "GeckoPanelGridView"; private final ViewConfig mViewConfig; private final PanelViewAdapter mAdapter; - protected OnUrlOpenListener mUrlOpenListener; + private PanelViewUrlHandler mUrlHandler; public PanelGridView(Context context, ViewConfig viewConfig) { super(context, null, R.attr.panelGridViewStyle); + mViewConfig = viewConfig; + mUrlHandler = new PanelViewUrlHandler(viewConfig); + mAdapter = new PanelViewAdapter(context, viewConfig.getItemType()); setAdapter(mAdapter); + setOnItemClickListener(new PanelGridItemClickListener()); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); - mUrlOpenListener = null; + mUrlHandler.setOnUrlOpenListener(null); } @Override public void setDataset(Cursor cursor) { mAdapter.swapCursor(cursor); } @Override public void setOnUrlOpenListener(OnUrlOpenListener listener) { - mUrlOpenListener = listener; + mUrlHandler.setOnUrlOpenListener(listener); } private class PanelGridItemClickListener implements AdapterView.OnItemClickListener { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Cursor cursor = mAdapter.getCursor(); - if (cursor == null || !cursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); - } - - int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL); - final String url = cursor.getString(urlIndex); - - EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class); - if (mViewConfig.getItemHandler() == ItemHandler.INTENT) { - flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT); - } - - mUrlOpenListener.onUrlOpen(url, flags); + mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position); } } }
--- a/mobile/android/base/home/PanelItemView.java +++ b/mobile/android/base/home/PanelItemView.java @@ -61,17 +61,16 @@ class PanelItemView extends LinearLayout // Only try to load the image if the item has define image URL final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl); mImage.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE); if (hasImageUrl) { Picasso.with(getContext()) .load(imageUrl) - .error(R.drawable.favicon) .into(mImage); } } private static class ArticleItemView extends PanelItemView { private ArticleItemView(Context context) { super(context, R.layout.panel_article_item); setOrientation(LinearLayout.HORIZONTAL);
--- a/mobile/android/base/home/PanelListView.java +++ b/mobile/android/base/home/PanelListView.java @@ -22,43 +22,41 @@ import android.widget.AdapterView; public class PanelListView extends HomeListView implements DatasetBacked, PanelView { private static final String LOGTAG = "GeckoPanelListView"; private final PanelViewAdapter mAdapter; private final ViewConfig mViewConfig; + private final PanelViewUrlHandler mUrlHandler; public PanelListView(Context context, ViewConfig viewConfig) { super(context); + mViewConfig = viewConfig; + mUrlHandler = new PanelViewUrlHandler(viewConfig); + mAdapter = new PanelViewAdapter(context, viewConfig.getItemType()); setAdapter(mAdapter); + setOnItemClickListener(new PanelListItemClickListener()); } @Override public void setDataset(Cursor cursor) { Log.d(LOGTAG, "Setting dataset: " + mViewConfig.getDatasetId()); mAdapter.swapCursor(cursor); } + @Override + public void setOnUrlOpenListener(OnUrlOpenListener listener) { + super.setOnUrlOpenListener(listener); + mUrlHandler.setOnUrlOpenListener(listener); + } + private class PanelListItemClickListener implements AdapterView.OnItemClickListener { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Cursor cursor = mAdapter.getCursor(); - if (cursor == null || !cursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); - } - - int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL); - final String url = cursor.getString(urlIndex); - - EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class); - if (mViewConfig.getItemHandler() == ItemHandler.INTENT) { - flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT); - } - - mUrlOpenListener.onUrlOpen(url, flags); + mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position); } } }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/home/PanelViewUrlHandler.java @@ -0,0 +1,46 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.db.BrowserContract.HomeItems; +import org.mozilla.gecko.home.HomeConfig.ItemHandler; +import org.mozilla.gecko.home.HomeConfig.ViewConfig; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; + +import android.database.Cursor; + +import java.util.EnumSet; + +class PanelViewUrlHandler { + private final ViewConfig mViewConfig; + private OnUrlOpenListener mUrlOpenListener; + + public PanelViewUrlHandler(ViewConfig viewConfig) { + mViewConfig = viewConfig; + } + + public void setOnUrlOpenListener(OnUrlOpenListener listener) { + mUrlOpenListener = listener; + } + + public void openUrlAtPosition(Cursor cursor, int position) { + if (cursor == null || !cursor.moveToPosition(position)) { + throw new IllegalStateException("Couldn't move cursor to position " + position); + } + + int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL); + final String url = cursor.getString(urlIndex); + + EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class); + if (mViewConfig.getItemHandler() == ItemHandler.INTENT) { + flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT); + } + + if (mUrlOpenListener != null) { + mUrlOpenListener.onUrlOpen(url, flags); + } + } +}
--- a/mobile/android/base/menu/GeckoMenu.java +++ b/mobile/android/base/menu/GeckoMenu.java @@ -21,17 +21,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListView; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; public class GeckoMenu extends ListView implements Menu, AdapterView.OnItemClickListener, GeckoMenuItem.OnShowAsActionChangedListener { private static final String LOGTAG = "GeckoMenu"; @@ -632,28 +631,16 @@ public class GeckoMenu extends ListView if (indexOfChild(actionItem) != -1) { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) actionItem.getLayoutParams(); mWeightSum -= params.weight; removeView(actionItem); } } } - public void refresh() { - for (Iterator<GeckoMenuItem> i = mPrimaryActionItems.keySet().iterator(); i.hasNext();) { - GeckoMenuItem item = i.next(); - item.refreshIfChanged(); - } - - for (Iterator<GeckoMenuItem> i = mSecondaryActionItems.keySet().iterator(); i.hasNext();) { - GeckoMenuItem item = i.next(); - item.refreshIfChanged(); - } - } - // Adapter to bind menu items to the list. private class MenuItemsAdapter extends BaseAdapter { private static final int VIEW_TYPE_DEFAULT = 0; private static final int VIEW_TYPE_ACTION_MODE = 1; private List<GeckoMenuItem> mItems; public MenuItemsAdapter() { @@ -742,21 +729,18 @@ public class GeckoMenu extends ListView @Override public boolean hasStableIds() { return false; } @Override public boolean areAllItemsEnabled() { - for (GeckoMenuItem item : mItems) { - if (!item.isEnabled()) - return false; - } - + // Setting this to true is a workaround to fix disappearing + // dividers in the menu (bug 963249). return true; } @Override public boolean isEnabled(int position) { return getItem(position).isEnabled(); }
--- a/mobile/android/base/menu/GeckoMenuItem.java +++ b/mobile/android/base/menu/GeckoMenuItem.java @@ -217,27 +217,16 @@ public class GeckoMenuItem implements Me } }); } mShowAsActionChangedListener.onShowAsActionChanged(this); return this; } - public void refreshIfChanged() { - if (mActionProvider == null) - return; - - if (mActionProvider instanceof GeckoActionProvider) { - if (((GeckoActionProvider) mActionProvider).hasChanged()) { - mShowAsActionChangedListener.onShowAsActionChanged(GeckoMenuItem.this); - } - } - } - @Override public MenuItem setActionView(int resId) { return this; } @Override public MenuItem setActionView(View view) { return this;
--- a/mobile/android/base/menu/MenuItemDefault.java +++ b/mobile/android/base/menu/MenuItemDefault.java @@ -43,17 +43,17 @@ public class MenuItemDefault extends Tex int width = res.getDimensionPixelSize(R.dimen.menu_item_row_width); int height = res.getDimensionPixelSize(R.dimen.menu_item_row_height); setMinimumWidth(width); setMinimumHeight(height); int stateIconSize = res.getDimensionPixelSize(R.dimen.menu_item_state_icon); Rect stateIconBounds = new Rect(0, 0, stateIconSize, stateIconSize); - mState = res.getDrawable(R.drawable.menu_item_state); + mState = res.getDrawable(R.drawable.menu_item_state).mutate(); mState.setBounds(stateIconBounds); if (sIconBounds == null) { int iconSize = res.getDimensionPixelSize(R.dimen.menu_item_icon); sIconBounds = new Rect(0, 0, iconSize, iconSize); } setCompoundDrawables(mIcon, null, mState, null);
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -234,16 +234,17 @@ gbjar.sources += [ 'home/MostRecentPanel.java', 'home/MultiTypeCursorAdapter.java', 'home/PanelGridView.java', 'home/PanelItemView.java', 'home/PanelLayout.java', 'home/PanelListView.java', 'home/PanelManager.java', 'home/PanelViewAdapter.java', + 'home/PanelViewUrlHandler.java', 'home/PinSiteDialog.java', 'home/ReadingListPanel.java', 'home/SearchEngine.java', 'home/SearchEngineRow.java', 'home/SearchLoader.java', 'home/SimpleCursorLoader.java', 'home/SuggestClient.java', 'home/TabMenuStrip.java', @@ -293,16 +294,18 @@ gbjar.sources += [ 'preferences/SearchPreferenceCategory.java', 'preferences/SyncPreference.java', 'PrefsHelper.java', 'PrivateTab.java', 'prompts/ColorPickerInput.java', 'prompts/IconGridInput.java', 'prompts/Prompt.java', 'prompts/PromptInput.java', + 'prompts/PromptListAdapter.java', + 'prompts/PromptListItem.java', 'prompts/PromptService.java', 'ReaderModeUtils.java', 'ReferrerReceiver.java', 'RemoteTabs.java', 'Restarter.java', 'ScrollAnimator.java', 'ServiceNotificationClient.java', 'SessionParser.java', @@ -480,41 +483,60 @@ for var in ('ANDROID_PACKAGE_NAME', 'AND DEFINES['MANGLED_ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'].replace('fennec', 'f3nn3c') DEFINES['MOZ_APP_ABI'] = CONFIG['TARGET_XPCOM_ABI'] if '-march=armv7' in CONFIG['OS_CFLAGS']: DEFINES['MOZ_MIN_CPU_VERSION'] = 7 else: DEFINES['MOZ_MIN_CPU_VERSION'] = 5 -generated = add_android_eclipse_library_project('FennecGeneratedResources') -generated.package_name = 'org.mozilla.fennec.generatedresources' +generated = add_android_eclipse_library_project('FennecResourcesGenerated') +generated.package_name = 'org.mozilla.fennec.resources.generated' generated.res = OBJDIR + '/res' -branding = add_android_eclipse_library_project('FennecBrandingResources') -branding.package_name = 'org.mozilla.fennec.brandingresources' +branding = add_android_eclipse_library_project('FennecResourcesBranding') +branding.package_name = 'org.mozilla.fennec.resources.branding' branding.res = TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res' main = add_android_eclipse_project('Fennec', OBJDIR + '/AndroidManifest.xml') main.package_name = 'org.mozilla.gecko' -main.res = SRCDIR + '/resources' main.recursive_make_targets += ['.aapt.deps'] # Captures dependencies on Android manifest and all resources. main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in mgjar.generated_sources] main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in gbjar.generated_sources] main.included_projects += ['../' + generated.name, '../' + branding.name] main.referenced_projects += [generated.name, branding.name] main.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']] main.assets = TOPOBJDIR + '/dist/fennec/assets' main.libs = TOPOBJDIR + '/dist/fennec/lib' +main.res = None cpe = main.add_classpathentry('src', SRCDIR, dstdir='src/org/mozilla/gecko', exclude_patterns=['org/mozilla/gecko/tests/**', 'org/mozilla/gecko/resources/**']) if not CONFIG['MOZ_CRASHREPORTER']: cpe.exclude_patterns += ['org/mozilla/gecko/CrashReporter.java'] main.add_classpathentry('generated', OBJDIR + '/generated', dstdir='generated') main.add_classpathentry('thirdparty', TOPSRCDIR + '/mobile/android/thirdparty', dstdir='thirdparty', ignore_warnings=True) + +resources = add_android_eclipse_library_project('FennecResources') +resources.package_name = 'org.mozilla.fennec.resources' +resources.res = SRCDIR + '/resources' +resources.included_projects += ['../' + generated.name, '../' + branding.name] +resources.referenced_projects += [generated.name, branding.name] + +main.included_projects += ['../' + resources.name] +main.referenced_projects += [resources.name] + +if CONFIG['MOZ_CRASHREPORTER']: + crashreporter = add_android_eclipse_library_project('FennecResourcesCrashReporter') + crashreporter.package_name = 'org.mozilla.fennec.resources.crashreporter' + crashreporter.res = SRCDIR + '/crashreporter/res' + crashreporter.included_projects += ['../' + resources.name] + crashreporter.referenced_projects += [resources.name] + + main.included_projects += ['../' + crashreporter.name] + main.referenced_projects += [crashreporter.name]
--- a/mobile/android/base/prompts/Prompt.java +++ b/mobile/android/base/prompts/Prompt.java @@ -31,28 +31,30 @@ import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import android.widget.TextView; +import java.util.ArrayList; + public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener { private static final String LOGTAG = "GeckoPromptService"; private String[] mButtons; private PromptInput[] mInputs; - private boolean[] mSelected; private AlertDialog mDialog; private final LayoutInflater mInflater; private final Context mContext; private PromptCallback mCallback; private String mGuid; + private PromptListAdapter mAdapter; private static boolean mInitialized = false; private static int mGroupPaddingSize; private static int mLeftRightTextWithIconPadding; private static int mTopBottomTextWithIconPadding; private static int mIconTextPadding; private static int mIconSize; private static int mInputPaddingSize; @@ -79,42 +81,43 @@ public class Prompt implements OnClickLi mInitialized = true; } } private View applyInputStyle(View view, PromptInput input) { // Don't add padding to color picker views if (input.canApplyInputStyle()) { view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0); - } + } return view; } public void show(JSONObject message) { processMessage(message); } - public void show(String title, String text, PromptListItem[] listItems, boolean multipleSelection) { + + public void show(String title, String text, PromptListItem[] listItems, int choiceMode) { ThreadUtils.assertOnUiThread(); GeckoAppShell.getLayerView().abortPanning(); AlertDialog.Builder builder = new AlertDialog.Builder(mContext); if (!TextUtils.isEmpty(title)) { builder.setTitle(title); } if (!TextUtils.isEmpty(text)) { builder.setMessage(text); } // Because lists are currently added through the normal Android AlertBuilder interface, they're // incompatible with also adding additional input elements to a dialog. if (listItems != null && listItems.length > 0) { - addlistItems(builder, listItems, multipleSelection); + addListItems(builder, listItems, choiceMode); } else if (!addInputs(builder)) { // If we failed to add any requested input elements, don't show the dialog return; } int length = mButtons == null ? 0 : mButtons.length; if (length > 0) { builder.setPositiveButton(mButtons[0], this); @@ -141,32 +144,35 @@ public class Prompt implements OnClickLi /* Adds to a result value from the lists that can be shown in dialogs. * Will set the selected value(s) to the button attribute of the * object that's passed in. If this is a multi-select dialog, sets a * selected attribute to an array of booleans. */ private void addListResult(final JSONObject result, int which) { try { - if (mSelected != null) { - JSONArray selected = new JSONArray(); - for (int i = 0; i < mSelected.length; i++) { - if (mSelected[i]) { - selected.put(i); - } + JSONArray selected = new JSONArray(); + + // If the button has already been filled in + ArrayList<Integer> selectedItems = mAdapter.getSelected(); + for (Integer item : selectedItems) { + selected.put(item); + } + + // If we haven't assigned a button yet, or we assigned it to -1, assign the which + // parameter to both selected and the button. + if (!result.has("button") || result.optInt("button") == -1) { + if (!selectedItems.contains(which)) { + selected.put(which); } - result.put("list", selected); - } else { - // Mirror the selected array from multi choice for consistency. - JSONArray selected = new JSONArray(); - selected.put(which); - result.put("list", selected); - // Make the button be the index of the select item. + result.put("button", which); } + + result.put("list", selected); } catch(JSONException ex) { } } /* Adds to a result value from the inputs that can be shown in dialogs. * Each input will set its own value in the result. */ private void addInputValues(final JSONObject result) { try { @@ -194,113 +200,101 @@ public class Prompt implements OnClickLi } catch(JSONException ex) { } } @Override public void onClick(DialogInterface dialog, int which) { ThreadUtils.assertOnUiThread(); JSONObject ret = new JSONObject(); try { - ListView list = mDialog.getListView(); addButtonResult(ret, which); addInputValues(ret); - if (list != null || mSelected != null) { + if (mAdapter != null) { addListResult(ret, which); } } catch(Exception ex) { Log.i(LOGTAG, "Error building return: " + ex); } if (dialog != null) { dialog.dismiss(); } finishDialog(ret); } /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists, - * or multiple selection lists. If mSelected is set in the prompt before addlistItems is called, the items will be - * shown with "checkmarks" on their left side. + * or multiple selection lists. * * @param builder * The alert builder currently building this dialog. * @param listItems * The items to add. - * @param multipleSelection - * If true, and mSelected is defined to be a non-zero-length list, the list will show checkmarks on the - * left and allow multiple selection. + * @param choiceMode + * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. */ - private void addlistItems(AlertDialog.Builder builder, PromptListItem[] listItems, boolean multipleSelection) { - if (mSelected != null && mSelected.length > 0) { - if (multipleSelection) { + private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) { + switch(choiceMode) { + case ListView.CHOICE_MODE_MULTIPLE_MODAL: + case ListView.CHOICE_MODE_MULTIPLE: addMultiSelectList(builder, listItems); - } else { + break; + case ListView.CHOICE_MODE_SINGLE: addSingleSelectList(builder, listItems); - } - } else { - addMenuList(builder, listItems); + break; + case ListView.CHOICE_MODE_NONE: + default: + addMenuList(builder, listItems); } } /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things * to the rows like disabling/indenting them. * * @param builder * The alert builder currently building this dialog. * @param listItems * The items to add. */ private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { - PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); - adapter.listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); - adapter.listView.setOnItemClickListener(this); - builder.setInverseBackgroundForced(true); - adapter.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - adapter.listView.setAdapter(adapter); - builder.setView(adapter.listView); + ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); + listView.setOnItemClickListener(this); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + + mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); + listView.setAdapter(mAdapter); + builder.setView(listView); } /* Shows a single-select list with radio boxes on the side. * * @param builder * the alert builder currently building this dialog. * @param listItems * The items to add. */ private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { - PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); - // For single select, we only maintain a single index of the selected row - int selectedIndex = -1; - for (int i = 0; i < mSelected.length; i++) { - if (mSelected[i]) { - selectedIndex = i; - break; - } - } - mSelected = null; - - builder.setSingleChoiceItems(adapter, selectedIndex, this); + mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); + builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), this); } /* Shows a single-select list. * * @param builder * the alert builder currently building this dialog. * @param listItems * The items to add. */ private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) { - PromptListAdapter adapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); - builder.setAdapter(adapter, this); - mSelected = null; + mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); + builder.setAdapter(mAdapter, this); } - /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background * drawable for the view. */ private View wrapInput(final PromptInput input) { final LinearLayout linearLayout = new LinearLayout(mContext); linearLayout.setOrientation(LinearLayout.VERTICAL); applyInputStyle(linearLayout, input); @@ -328,23 +322,21 @@ public class Prompt implements OnClickLi boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView if (length == 1) { root = wrapInput(mInputs[0]); scrollable |= mInputs[0].getScrollable(); } else if (length > 1) { LinearLayout linearLayout = new LinearLayout(mContext); linearLayout.setOrientation(LinearLayout.VERTICAL); - for (int i = 0; i < length; i++) { View content = wrapInput(mInputs[i]); linearLayout.addView(content); scrollable |= mInputs[i].getScrollable(); } - root = linearLayout; } if (scrollable) { builder.setView(root); } else { ScrollView view = new ScrollView(mContext); view.addView(root); @@ -362,17 +354,17 @@ public class Prompt implements OnClickLi } /* AdapterView.OnItemClickListener * Called when a list item is clicked */ @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ThreadUtils.assertOnUiThread(); - mSelected[position] = !mSelected[position]; + mAdapter.toggleSelected(position); } /* @DialogInterface.OnCancelListener * Called when the user hits back to cancel a dialog. The dialog will close itself when this * ends. Setup the correct return values here. * * @param aDialog * A dialog interface for the dialog that's being closed. @@ -397,17 +389,16 @@ public class Prompt implements OnClickLi /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog * is closing. */ public void finishDialog(JSONObject aReturn) { mInputs = null; mButtons = null; mDialog = null; - mSelected = null; try { aReturn.put("guid", mGuid); } catch(JSONException ex) { } // poke the Gecko thread in case it's waiting for new events GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); if (mCallback != null) { @@ -428,20 +419,27 @@ public class Prompt implements OnClickLi JSONArray inputs = getSafeArray(geckoObject, "inputs"); mInputs = new PromptInput[inputs.length()]; for (int i = 0; i < mInputs.length; i++) { try { mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i)); } catch(Exception ex) { } } - PromptListItem[] menuitems = getListItemArray(geckoObject, "listitems"); - mSelected = getBooleanArray(geckoObject, "selected"); - boolean multiple = geckoObject.optBoolean("multiple"); - show(title, text, menuitems, multiple); + PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems")); + String selected = geckoObject.optString("choiceMode"); + + int choiceMode = ListView.CHOICE_MODE_NONE; + if ("single".equals(selected)) { + choiceMode = ListView.CHOICE_MODE_SINGLE; + } else if ("multiple".equals(selected)) { + choiceMode = ListView.CHOICE_MODE_MULTIPLE; + } + + show(title, text, menuitems, choiceMode); } private static JSONArray getSafeArray(JSONObject json, String key) { try { return json.getJSONArray(key); } catch (Exception e) { return new JSONArray(); } @@ -469,194 +467,12 @@ public class Prompt implements OnClickLi for (int i = 0; i < length; i++) { try { list[i] = items.getBoolean(i); } catch(Exception ex) { } } return list; } - private PromptListItem[] getListItemArray(JSONObject aObject, String aName) { - JSONArray items = getSafeArray(aObject, aName); - int length = items.length(); - PromptListItem[] list = new PromptListItem[length]; - for (int i = 0; i < length; i++) { - try { - list[i] = new PromptListItem(items.getJSONObject(i)); - } catch(Exception ex) { } - } - return list; - } - - public static class PromptListItem { - public final String label; - public final boolean isGroup; - public final boolean inGroup; - public final boolean disabled; - public final int id; - public final boolean isParent; - - // This member can't be accessible from JS, see bug 733749. - public Drawable icon; - - PromptListItem(JSONObject aObject) { - label = aObject.optString("label"); - isGroup = aObject.optBoolean("isGroup"); - inGroup = aObject.optBoolean("inGroup"); - disabled = aObject.optBoolean("disabled"); - id = aObject.optInt("id"); - isParent = aObject.optBoolean("isParent"); - } - - public PromptListItem(String aLabel) { - label = aLabel; - isGroup = false; - inGroup = false; - disabled = false; - id = 0; - isParent = false; - } - } - public interface PromptCallback { public void onPromptFinished(String jsonResult); } - - public class PromptListAdapter extends ArrayAdapter<PromptListItem> { - private static final int VIEW_TYPE_ITEM = 0; - private static final int VIEW_TYPE_GROUP = 1; - private static final int VIEW_TYPE_COUNT = 2; - - public ListView listView; - private int mResourceId = -1; - private Drawable mBlankDrawable = null; - private Drawable mMoreDrawable = null; - - PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) { - super(context, textViewResourceId, objects); - mResourceId = textViewResourceId; - } - - @Override - public int getItemViewType(int position) { - PromptListItem item = getItem(position); - return (item.isGroup ? VIEW_TYPE_GROUP : VIEW_TYPE_ITEM); - } - - @Override - public int getViewTypeCount() { - return VIEW_TYPE_COUNT; - } - - private Drawable getMoreDrawable(Resources res) { - if (mMoreDrawable == null) { - mMoreDrawable = res.getDrawable(android.R.drawable.ic_menu_more); - } - return mMoreDrawable; - } - - private Drawable getBlankDrawable(Resources res) { - if (mBlankDrawable == null) { - mBlankDrawable = res.getDrawable(R.drawable.blank); - } - return mBlankDrawable; - } - - private void maybeUpdateIcon(PromptListItem item, TextView t) { - if (item.icon == null && !item.inGroup && !item.isParent) { - t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - return; - } - - Drawable d = null; - Resources res = mContext.getResources(); - // Set the padding between the icon and the text. - t.setCompoundDrawablePadding(mIconTextPadding); - if (item.icon != null) { - // We want the icon to be of a specific size. Some do not - // follow this rule so we have to resize them. - Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap(); - d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true)); - } else if (item.inGroup) { - // We don't currently support "indenting" items with icons - d = getBlankDrawable(res); - } - - Drawable moreDrawable = null; - if (item.isParent) { - moreDrawable = getMoreDrawable(res); - } - - if (d != null || moreDrawable != null) { - t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null); - } - } - - private void maybeUpdateCheckedState(int position, PromptListItem item, ViewHolder viewHolder) { - viewHolder.textView.setEnabled(!item.disabled && !item.isGroup); - viewHolder.textView.setClickable(item.isGroup || item.disabled); - - if (mSelected == null) { - return; - } - - CheckedTextView ct; - try { - ct = (CheckedTextView) viewHolder.textView; - // Apparently just using ct.setChecked(true) doesn't work, so this - // is stolen from the android source code as a way to set the checked - // state of these items - if (listView != null) { - listView.setItemChecked(position, mSelected[position]); - } - } catch (Exception e) { - return; - } - - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - PromptListItem item = getItem(position); - ViewHolder viewHolder = null; - - if (convertView == null) { - int resourceId = mResourceId; - if (item.isGroup) { - resourceId = R.layout.list_item_header; - } - - convertView = mInflater.inflate(resourceId, null); - convertView.setMinimumHeight(mMinRowSize); - - TextView tv = (TextView) convertView.findViewById(android.R.id.text1); - viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(), - tv.getPaddingTop(), tv.getPaddingBottom()); - - convertView.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) convertView.getTag(); - } - - viewHolder.textView.setText(item.label); - maybeUpdateCheckedState(position, item, viewHolder); - maybeUpdateIcon(item, viewHolder.textView); - - return convertView; - } - - private class ViewHolder { - public final TextView textView; - public final int paddingLeft; - public final int paddingRight; - public final int paddingTop; - public final int paddingBottom; - - ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) { - textView = aTextView; - paddingLeft = aLeft; - paddingRight = aRight; - paddingTop = aTop; - paddingBottom = aBottom; - } - } - } }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/prompts/PromptListAdapter.java @@ -0,0 +1,210 @@ +package org.mozilla.gecko.prompts; + +import org.mozilla.gecko.R; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONException; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.AdapterView; +import android.widget.CheckedTextView; +import android.widget.TextView; +import android.widget.ListView; + +import java.util.ArrayList; + +public class PromptListAdapter extends ArrayAdapter<PromptListItem> { + private static final int VIEW_TYPE_ITEM = 0; + private static final int VIEW_TYPE_GROUP = 1; + private static final int VIEW_TYPE_COUNT = 2; + + private static final String LOGTAG = "GeckoPromptListAdapter"; + + private final int mResourceId; + private Drawable mBlankDrawable; + private Drawable mMoreDrawable; + private static int mGroupPaddingSize; + private static int mLeftRightTextWithIconPadding; + private static int mTopBottomTextWithIconPadding; + private static int mIconSize; + private static int mMinRowSize; + private static int mIconTextPadding; + private static boolean mInitialized = false; + + PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) { + super(context, textViewResourceId, objects); + mResourceId = textViewResourceId; + init(); + } + + private void init() { + if (!mInitialized) { + Resources res = getContext().getResources(); + mGroupPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_group_padding_size)); + mLeftRightTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_left_right_text_with_icon_padding)); + mTopBottomTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_top_bottom_text_with_icon_padding)); + mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding)); + mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size)); + mMinRowSize = (int) (res.getDimension(R.dimen.prompt_service_min_list_item_height)); + mInitialized = true; + } + } + + @Override + public int getItemViewType(int position) { + PromptListItem item = getItem(position); + if (item.isGroup) { + return VIEW_TYPE_GROUP; + } else { + return VIEW_TYPE_ITEM; + } + } + + @Override + public int getViewTypeCount() { + return VIEW_TYPE_COUNT; + } + + private Drawable getMoreDrawable(Resources res) { + if (mMoreDrawable == null) { + mMoreDrawable = res.getDrawable(R.drawable.menu_item_more); + } + return mMoreDrawable; + } + + private Drawable getBlankDrawable(Resources res) { + if (mBlankDrawable == null) { + mBlankDrawable = res.getDrawable(R.drawable.blank); + } + return mBlankDrawable; + } + + public void toggleSelected(int position) { + PromptListItem item = getItem(position); + item.selected = !item.selected; + } + + private void maybeUpdateIcon(PromptListItem item, TextView t) { + if (item.icon == null && !item.inGroup && !item.isParent) { + t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + return; + } + + Drawable d = null; + Resources res = getContext().getResources(); + // Set the padding between the icon and the text. + t.setCompoundDrawablePadding(mIconTextPadding); + if (item.icon != null) { + // We want the icon to be of a specific size. Some do not + // follow this rule so we have to resize them. + Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap(); + d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true)); + } else if (item.inGroup) { + // We don't currently support "indenting" items with icons + d = getBlankDrawable(res); + } + + Drawable moreDrawable = null; + if (item.isParent) { + moreDrawable = getMoreDrawable(res); + } + + if (d != null || moreDrawable != null) { + t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null); + } + } + + private void maybeUpdateCheckedState(ListView list, int position, PromptListItem item, ViewHolder viewHolder) { + viewHolder.textView.setEnabled(!item.disabled && !item.isGroup); + viewHolder.textView.setClickable(item.isGroup || item.disabled); + if (viewHolder.textView instanceof CheckedTextView) { + // Apparently just using ct.setChecked(true) doesn't work, so this + // is stolen from the android source code as a way to set the checked + // state of these items + list.setItemChecked(position, item.selected); + } + } + + boolean isSelected(int position){ + return getItem(position).selected; + } + + ArrayList<Integer> getSelected() { + int length = getCount(); + + ArrayList<Integer> selected = new ArrayList<Integer>(); + for (int i = 0; i< length; i++) { + if (isSelected(i)) { + selected.add(i); + } + } + + return selected; + } + + int getSelectedIndex() { + int length = getCount(); + for (int i = 0; i< length; i++) { + if (isSelected(i)) { + return i; + } + } + return -1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + PromptListItem item = getItem(position); + int type = getItemViewType(position); + ViewHolder viewHolder = null; + + if (convertView == null) { + int resourceId = mResourceId; + if (item.isGroup) { + resourceId = R.layout.list_item_header; + } + LayoutInflater mInflater = LayoutInflater.from(getContext()); + convertView = mInflater.inflate(resourceId, null); + convertView.setMinimumHeight(mMinRowSize); + + TextView tv = (TextView) convertView.findViewById(android.R.id.text1); + viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(), + tv.getPaddingTop(), tv.getPaddingBottom()); + + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + viewHolder.textView.setText(item.label); + maybeUpdateCheckedState((ListView) parent, position, item, viewHolder); + maybeUpdateIcon(item, viewHolder.textView); + + return convertView; + } + + private static class ViewHolder { + public final TextView textView; + public final int paddingLeft; + public final int paddingRight; + public final int paddingTop; + public final int paddingBottom; + + ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) { + textView = aTextView; + paddingLeft = aLeft; + paddingRight = aRight; + paddingTop = aTop; + paddingBottom = aBottom; + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/prompts/PromptListItem.java @@ -0,0 +1,60 @@ +package org.mozilla.gecko.prompts; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONException; + +import android.graphics.drawable.Drawable; +import java.util.List; +import java.util.ArrayList; + +// This class should die and be replaced with normal menu items +public class PromptListItem { + private static final String LOGTAG = "GeckoPromptListItem"; + public final String label; + public final boolean isGroup; + public final boolean inGroup; + public final boolean disabled; + public final int id; + public boolean selected; + + public boolean isParent; + public Drawable icon; + + PromptListItem(JSONObject aObject) { + label = aObject.optString("label"); + isGroup = aObject.optBoolean("isGroup"); + inGroup = aObject.optBoolean("inGroup"); + disabled = aObject.optBoolean("disabled"); + id = aObject.optInt("id"); + isParent = aObject.optBoolean("isParent"); + selected = aObject.optBoolean("selected"); + } + + public PromptListItem(String aLabel) { + label = aLabel; + isGroup = false; + inGroup = false; + disabled = false; + id = 0; + } + + static PromptListItem[] getArray(JSONArray items) { + if (items == null) { + return new PromptListItem[0]; + } + + int length = items.length(); + List<PromptListItem> list = new ArrayList<PromptListItem>(length); + for (int i = 0; i < length; i++) { + try { + PromptListItem item = new PromptListItem(items.getJSONObject(i)); + list.add(item); + } catch(Exception ex) { } + } + + PromptListItem[] arrays = new PromptListItem[length]; + list.toArray(arrays); + return arrays; + } +}
--- a/mobile/android/base/prompts/PromptService.java +++ b/mobile/android/base/prompts/PromptService.java @@ -26,25 +26,25 @@ public class PromptService implements Ge mContext = context; } public void destroy() { GeckoAppShell.getEventDispatcher().unregisterEventListener("Prompt:Show", this); GeckoAppShell.getEventDispatcher().unregisterEventListener("Prompt:ShowTop", this); } - public void show(final String aTitle, final String aText, final Prompt.PromptListItem[] aMenuList, - final boolean aMultipleSelection, final Prompt.PromptCallback callback) { + public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList, + final int aChoiceMode, final Prompt.PromptCallback callback) { // The dialog must be created on the UI thread. ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { Prompt p; p = new Prompt(mContext, callback); - p.show(aTitle, aText, aMenuList, aMultipleSelection); + p.show(aTitle, aText, aMenuList, aChoiceMode); } }); } // GeckoEventListener implementation @Override public void handleMessage(String event, final JSONObject message) { // The dialog must be created on the UI thread.
--- a/mobile/android/base/resources/layout/gecko_app.xml +++ b/mobile/android/base/resources/layout/gecko_app.xml @@ -23,17 +23,18 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_above="@+id/find_in_page"> <include layout="@layout/shared_ui_components"/> <FrameLayout android:id="@+id/home_pager_container" android:layout_width="fill_parent" - android:layout_height="fill_parent"> + android:layout_height="fill_parent" + android:visibility="gone"> <ViewStub android:id="@+id/home_pager_stub" android:layout="@layout/home_pager" android:layout_width="fill_parent" android:layout_height="fill_parent"/> <org.mozilla.gecko.home.HomeBanner android:id="@+id/home_banner" style="@style/Widget.HomeBanner"
--- a/mobile/android/base/tests/testHomeBanner.java +++ b/mobile/android/base/tests/testHomeBanner.java @@ -17,16 +17,20 @@ public class testHomeBanner extends UITe GeckoHelper.blockForReady(); // Make sure the banner is not visible to start. mAboutHome.assertVisible() .assertBannerNotVisible(); // These test methods depend on being run in this order. addBannerTest(); + + // Make sure the banner hides when the user starts interacting with the url bar. + hideOnToolbarFocusTest(); + // TODO: API doesn't actually support this but it used to work due to how the banner was // part of TopSitesPanel's lifecycle // removeBannerTest(); // Make sure to test dismissing the banner after everything else, since dismissing // the banner will prevent it from showing up again. dismissBannerTest(); } @@ -91,16 +95,28 @@ public class testHomeBanner extends UITe // Test to make sure the ondismiss handler is called when the close button is clicked. final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed"); mAboutHome.dismissBanner(); eventExpecter.blockForEvent(); mAboutHome.assertBannerNotVisible(); } + private void hideOnToolbarFocusTest() { + NavigationHelper.enterAndLoadUrl("about:home"); + mAboutHome.assertVisible() + .assertBannerVisible(); + + mToolbar.enterEditingMode(); + mAboutHome.assertBannerNotVisible(); + + mToolbar.dismissEditingMode(); + mAboutHome.assertBannerVisible(); + } + /** * Loads the roboextender page to add a message to the banner. */ private void addBannerMessage() { final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded"); NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage"); eventExpecter.blockForEvent(); }
--- a/mobile/android/base/toolbar/BrowserToolbar.java +++ b/mobile/android/base/toolbar/BrowserToolbar.java @@ -132,16 +132,17 @@ public class BrowserToolbar extends Geck private LinearLayout mActionItemBar; private MenuPopup mMenuPopup; private List<View> mFocusOrder; private OnActivateListener mActivateListener; private OnCommitListener mCommitListener; private OnDismissListener mDismissListener; private OnFilterListener mFilterListener; + private OnFocusChangeListener mFocusChangeListener; private OnStartEditingListener mStartEditingListener; private OnStopEditingListener mStopEditingListener; final private BrowserApp mActivity; private boolean mHasSoftMenuButton; private UIMode mUIMode; private boolean mAnimatingEntry; @@ -310,16 +311,19 @@ public class BrowserToolbar extends Geck setContentDescription(contentDescription); } }); mUrlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { setSelected(hasFocus); + if (mFocusChangeListener != null) { + mFocusChangeListener.onFocusChange(v, hasFocus); + } } }); mTabs.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { toggleTabs(); } @@ -780,16 +784,20 @@ public class BrowserToolbar extends Geck mUrlEditLayout.setOnDismissListener(listener); } public void setOnFilterListener(OnFilterListener listener) { mFilterListener = listener; mUrlEditLayout.setOnFilterListener(listener); } + public void setOnFocusChangeListener(OnFocusChangeListener listener) { + mFocusChangeListener = listener; + } + public void setOnStartEditingListener(OnStartEditingListener listener) { mStartEditingListener = listener; } public void setOnStopEditingListener(OnStopEditingListener listener) { mStopEditingListener = listener; }
--- a/mobile/android/base/widget/ActivityChooserModel.java +++ b/mobile/android/base/widget/ActivityChooserModel.java @@ -314,18 +314,16 @@ public class ActivityChooserModel extend */ private boolean mHistoricalRecordsChanged = true; /** * Flag whether to reload the activities for the current intent. */ private boolean mReloadActivities = false; - private long mLastChanged = 0; - /** * Policy for controlling how the model handles chosen activities. */ private OnChooseActivityListener mActivityChoserModelPolicy; /** * Gets the data model backed by the contents of the provided file with historical data. * Note that only one data model is backed by a given file, thus multiple calls with @@ -742,17 +740,16 @@ public class ActivityChooserModel extend mActivities.clear(); List<ResolveInfo> resolveInfos = mContext.getPackageManager() .queryIntentActivities(mIntent, 0); final int resolveInfoCount = resolveInfos.size(); for (int i = 0; i < resolveInfoCount; i++) { ResolveInfo resolveInfo = resolveInfos.get(i); mActivities.add(new ActivityResolveInfo(resolveInfo)); } - mLastChanged = System.currentTimeMillis(); return true; } return false; } /** * Reads the historical data if necessary which is it has * changed, there is a history file, and there is not persist @@ -1218,16 +1215,12 @@ public class ActivityChooserModel extend public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { String packageName = intent.getData().getSchemeSpecificPart(); removeHistoricalRecordsForPackage(packageName); } mReloadActivities = true; - mLastChanged = System.currentTimeMillis(); } } +} - public long getLastChanged() { - return mLastChanged; - } -}
--- a/mobile/android/base/widget/GeckoActionProvider.java +++ b/mobile/android/base/widget/GeckoActionProvider.java @@ -16,17 +16,16 @@ import android.view.ActionProvider; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.SubMenu; import android.view.View; import android.view.View.OnClickListener; public class GeckoActionProvider extends ActionProvider { private static int MAX_HISTORY_SIZE = 2; - private long mLastChanged = 0; /** * A listener to know when a target was selected. * When setting a provider, the activity can listen to this, * to close the menu. */ public interface OnTargetSelectedListener { public void onTargetSelected(); @@ -75,24 +74,16 @@ public class GeckoActionProvider extends return view; } public View getView() { return onCreateActionView(); } - public boolean hasChanged() { - ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName); - long lastChanged = dataModel.getLastChanged(); - boolean ret = lastChanged != mLastChanged; - mLastChanged = lastChanged; - return ret; - } - @Override public boolean hasSubMenu() { return true; } @Override public void onPrepareSubMenu(SubMenu subMenu) { // Clear since the order of items may change.
--- a/mobile/android/modules/HelperApps.jsm +++ b/mobile/android/modules/HelperApps.jsm @@ -22,18 +22,19 @@ function App(data) { this.name = data.name; this.isDefault = data.isDefault; this.packageName = data.packageName; this.activityName = data.activityName; this.iconUri = "-moz-icon://" + data.packageName; } App.prototype = { - launch: function(uri) { - HelperApps._launchApp(this, uri); + // callback will be null if a result is not requested + launch: function(uri, callback) { + HelperApps._launchApp(this, uri, callback); return false; } } var HelperApps = { get defaultHttpHandlers() { delete this.defaultHttpHandlers; this.defaultHttpHandlers = this.getAppsForProtocol("http"); @@ -161,23 +162,34 @@ var HelperApps = { mime: mimeType, action: options.action || "", // empty action string defaults to android.intent.action.VIEW url: uri ? uri.spec : "", packageName: options.packageName || "", className: options.className || "" }; }, - _launchApp: function launchApp(app, uri) { - let msg = this._getMessage("Intent:Open", uri, { - packageName: app.packageName, - className: app.activityName - }); + _launchApp: function launchApp(app, uri, callback) { + if (callback) { + let msg = this._getMessage("Intent:OpenForResult", uri, { + packageName: app.packageName, + className: app.activityName + }); - sendMessageToJava(msg); + sendMessageToJava(msg, function(data) { + callback(JSON.parse(data)); + }); + } else { + let msg = this._getMessage("Intent:Open", uri, { + packageName: app.packageName, + className: app.activityName + }); + + sendMessageToJava(msg); + } }, _sendMessageSync: function(msg) { let res = null; sendMessageToJava(msg, function(data) { res = data; });
--- a/mobile/android/modules/Home.jsm +++ b/mobile/android/modules/Home.jsm @@ -151,54 +151,122 @@ let HomeBanner = (function () { Services.obs.removeObserver(this, "HomeBanner:Get"); Services.obs.removeObserver(this, "HomeBanner:Click"); Services.obs.removeObserver(this, "HomeBanner:Dismiss"); } } }); })(); -function Panel(options) { - if ("id" in options) - this.id = options.id; - - if ("title" in options) - this.title = options.title; +function Panel(id, options) { + this.id = id; + this.title = options.title; if ("layout" in options) this.layout = options.layout; if ("views" in options) this.views = options.views; } +// We need this function to have access to the HomePanels +// private members without leaking it outside Home.jsm. +let handlePanelsGet; + let HomePanels = (function () { - // Holds the currrent set of registered panels. - let _panels = {}; + // Holds the current set of registered panels that can be + // installed, updated, uninstalled, or unregistered. It maps + // panel ids with the functions that dynamically generate + // their respective panel options. This is used to retrieve + // the current list of available panels in the system. + // See HomePanels:Get handler. + let _registeredPanels = {}; + + // Valid layouts for a panel. + let Layout = Object.freeze({ + FRAME: "frame" + }); + + // Valid types of views for a dataset. + let View = Object.freeze({ + LIST: "list", + GRID: "grid" + }); + + // Valid item types for a panel view. + let Item = Object.freeze({ + ARTICLE: "article", + IMAGE: "image" + }); + + // Valid item handlers for a panel view. + let ItemHandler = Object.freeze({ + BROWSER: "browser", + INTENT: "intent" + }); + + let _generatePanel = function(id) { + let panel = new Panel(id, _registeredPanels[id]()); - let _panelToJSON = function(panel) { - return { - id: panel.id, - title: panel.title, - layout: panel.layout, - views: panel.views - }; + if (!panel.id || !panel.title) { + throw "Home.panels: Can't create a home panel without an id and title!"; + } + + if (!panel.layout) { + // Use FRAME layout by default + panel.layout = Layout.FRAME; + } else if (!_valueExists(Layout, panel.layout)) { + throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout; + } + + for (let view of panel.views) { + if (!_valueExists(View, view.type)) { + throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type; + } + + if (!view.itemType) { + if (view.type == View.LIST) { + // Use ARTICLE item type by default in LIST views + view.itemType = Item.ARTICLE; + } else if (view.type == View.GRID) { + // Use IMAGE item type by default in GRID views + view.itemType = Item.IMAGE; + } + } else if (!_valueExists(Item, view.itemType)) { + throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType; + } + + if (!view.itemHandler) { + // Use BROWSER item handler by default + view.itemHandler = ItemHandler.BROWSER; + } else if (!_valueExists(ItemHandler, view.itemHandler)) { + throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler; + } + + if (!view.dataset) { + throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type; + } + } + + return panel; }; - let _handleGet = function(data) { + handlePanelsGet = function(data) { let requestId = data.requestId; let ids = data.ids || null; let panels = []; - for (let id in _panels) { - let panel = _panels[id]; - + for (let id in _registeredPanels) { // Null ids means we want to fetch all available panels - if (ids == null || ids.indexOf(panel.id) >= 0) { - panels.push(_panelToJSON(panel)); + if (ids == null || ids.indexOf(id) >= 0) { + try { + panels.push(_generatePanel(id)); + } catch(e) { + Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); + } } } sendMessageToJava({ type: "HomePanels:Data", panels: panels, requestId: requestId }); @@ -210,105 +278,52 @@ let HomePanels = (function () { if (obj[key] == value) { return true; } } return false; }; let _assertPanelExists = function(id) { - if (!(id in _panels)) { + if (!(id in _registeredPanels)) { throw "Home.panels: Panel doesn't exist: id = " + id; } }; return Object.freeze({ - // Valid layouts for a panel. - Layout: Object.freeze({ - FRAME: "frame" - }), - - // Valid types of views for a dataset. - View: Object.freeze({ - LIST: "list", - GRID: "grid" - }), - - // Valid item types for a panel view. - Item: Object.freeze({ - ARTICLE: "article", - IMAGE: "image" - }), + Layout: Layout, + View: View, + Item: Item, + ItemHandler: ItemHandler, - // Valid item handlers for a panel view. - ItemHandler: Object.freeze({ - BROWSER: "browser", - INTENT: "intent" - }), - - register: function(options) { - let panel = new Panel(options); - + register: function(id, optionsCallback) { // Bail if the panel already exists - if (panel.id in _panels) { - throw "Home.panels: Panel already exists: id = " + panel.id; - } - - if (!panel.id || !panel.title) { - throw "Home.panels: Can't create a home panel without an id and title!"; + if (id in _registeredPanels) { + throw "Home.panels: Panel already exists: id = " + id; } - if (!_valueExists(this.Layout, panel.layout)) { - throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout; + if (!optionsCallback || typeof optionsCallback !== "function") { + throw "Home.panels: Panel callback must be a function: id = " + id; } - for (let view of panel.views) { - if (!_valueExists(this.View, view.type)) { - throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type; - } - - if (!view.itemType) { - if (view.type == this.View.LIST) { - // Use ARTICLE item type by default in LIST views - view.itemType = this.Item.ARTICLE; - } else if (view.type == this.View.GRID) { - // Use IMAGE item type by default in GRID views - view.itemType = this.Item.IMAGE; - } - } else if (!_valueExists(this.Item, view.itemType)) { - throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType; - } - - if (!view.itemHandler) { - // Use BROWSER item handler by default - view.itemHandler = this.ItemHandler.BROWSER; - } else if (!_valueExists(this.ItemHandler, view.itemHandler)) { - throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler; - } - - if (!view.dataset) { - throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type; - } - } - - _panels[panel.id] = panel; + _registeredPanels[id] = optionsCallback; }, unregister: function(id) { _assertPanelExists(id); - delete _panels[id]; + delete _registeredPanels[id]; }, install: function(id) { _assertPanelExists(id); sendMessageToJava({ type: "HomePanels:Install", - panel: _panelToJSON(_panels[id]) + panel: _generatePanel(id) }); }, uninstall: function(id) { _assertPanelExists(id); sendMessageToJava({ type: "HomePanels:Uninstall", @@ -316,28 +331,28 @@ let HomePanels = (function () { }); }, update: function(id) { _assertPanelExists(id); sendMessageToJava({ type: "HomePanels:Update", - panel: _panelToJSON(_panels[id]) + panel: _generatePanel(id) }); } }); })(); // Public API this.Home = Object.freeze({ banner: HomeBanner, panels: HomePanels, // Lazy notification observer registered in browser.js observe: function(subject, topic, data) { switch(topic) { case "HomePanels:Get": - HomePanels._handleGet(JSON.parse(data)); + handlePanelsGet(JSON.parse(data)); break; } } });
--- a/mobile/android/modules/Prompt.jsm +++ b/mobile/android/modules/Prompt.jsm @@ -174,22 +174,21 @@ Prompt.prototype = { aItems.forEach(function(item) { let obj = { id: item.id }; obj.label = item.label; if (item.disabled) obj.disabled = true; - if (item.selected || hasSelected || this.msg.multiple) { - if (!this.msg.selected) { - this.msg.selected = new Array(this.msg.listitems.length); - hasSelected = true; + if (item.selected) { + if (!this.msg.choiceMode) { + this.msg.choiceMode = "single"; } - this.msg.selected[this.msg.listitems.length] = item.selected; + obj.selected = item.selected; } if (item.header) obj.isGroup = true; if (item.menu) obj.isParent = true; @@ -202,13 +201,13 @@ Prompt.prototype = { return this; }, setSingleChoiceItems: function(aItems) { return this._setListItems(aItems); }, setMultiChoiceItems: function(aItems) { - this.msg.multiple = true; + this.msg.choiceMode = "multiple"; return this._setListItems(aItems); }, }
--- a/mobile/android/tests/background/junit3/AndroidManifest.xml.in +++ b/mobile/android/tests/background/junit3/AndroidManifest.xml.in @@ -2,17 +2,17 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="@ANDROID_BACKGROUND_TEST_PACKAGE_NAME@" sharedUserId="@MOZ_ANDROID_SHARED_ID@" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" - android:targetSdkVersion="14" /> + android:targetSdkVersion="16" /> <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/> <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/> <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/> <application android:icon="@drawable/icon" android:label="@ANDROID_BACKGROUND_APP_DISPLAYNAME@">
--- a/mobile/android/tests/background/junit3/Makefile.in +++ b/mobile/android/tests/background/junit3/Makefile.in @@ -1,24 +1,25 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -ANDROID_APK_NAME := background-debug +ANDROID_APK_NAME := background-junit3-debug PP_TARGETS += manifest manifest := $(srcdir)/AndroidManifest.xml.in manifest_TARGET := AndroidManifest.xml manifest_FLAGS += \ -DANDROID_BACKGROUND_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \ - -DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.background.test' \ + -DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.gecko.background.tests' \ -DANDROID_BACKGROUND_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Background Tests' \ -DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \ -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE='$(ANDROID_PACKAGE_NAME)_sync' \ $(NULL) +ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml GARBAGE += AndroidManifest.xml include $(srcdir)/android-services-files.mk # BACKGROUND_TESTS_{JAVA,RES}_FILES are defined in android-services-files.mk. JAVAFILES := $(BACKGROUND_TESTS_JAVA_FILES)
--- a/mobile/android/tests/background/junit3/moz.build +++ b/mobile/android/tests/background/junit3/moz.build @@ -1,18 +1,19 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] + include('android-services.mozbuild') main = add_android_eclipse_project('BackgroundInstrumentationTests', OBJDIR + '/AndroidManifest.xml') -main.package_name = 'org.mozilla.background.test' +main.package_name = 'org.mozilla.gecko.background.tests' main.res = SRCDIR + '/res' main.recursive_make_targets += [ - OBJDIR + '/AndroidManifest.xml', - TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java'] + OBJDIR + '/AndroidManifest.xml'] main.referenced_projects += ['Fennec'] main.add_classpathentry('src', SRCDIR + '/src', dstdir='src/org/mozilla/gecko/background')
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/AndroidManifest.xml.in @@ -0,0 +1,26 @@ +#filter substitution +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="@ANDROID_BROWSER_TEST_PACKAGE_NAME@" + sharedUserId="@MOZ_ANDROID_SHARED_ID@" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk android:minSdkVersion="8" + android:targetSdkVersion="16" /> + + <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/> + <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/> + <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/> + + <application + android:icon="@drawable/icon" + android:label="@ANDROID_BROWSER_APP_DISPLAYNAME@"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:label="@string/app_name" + android:name="org.mozilla.browser.harness.BrowserInstrumentationTestRunner" + android:targetPackage="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@" /> +</manifest>
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/Makefile.in @@ -0,0 +1,32 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +ANDROID_APK_NAME := browser-junit3-debug + +ANDROID_EXTRA_JARS += \ + browser-junit3.jar \ + $(NULL) + +PP_TARGETS += manifest +manifest := AndroidManifest.xml.in +manifest_FLAGS += \ + -DANDROID_BROWSER_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \ + -DANDROID_BROWSER_TEST_PACKAGE_NAME='org.mozilla.gecko.browser.tests' \ + -DANDROID_BROWSER_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Browser Tests' \ + -DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \ + $(NULL) +ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml + +include $(topsrcdir)/config/rules.mk + +tools:: $(ANDROID_APK_NAME).apk + +# The test APK needs to know the contents of the target APK while not +# being linked against them. This is a best effort to avoid getting +# out of sync with base's build config. +JARS_DIR := $(DEPTH)/mobile/android/base +JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(JARS_DIR)/*.jar)) +# We also want to re-compile classes.dex when the associated base +# content changes. +classes.dex: $(wildcard $(JARS_DIR)/*.jar)
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] + +jar = add_java_jar('browser-junit3') +jar.sources += [ + 'src/harness/BrowserInstrumentationTestRunner.java', + 'src/harness/BrowserTestListener.java', + 'src/tests/BrowserTestCase.java', + 'src/tests/TestJarReader.java', +] +jar.generated_sources = [] # None yet -- try to keep it this way. +jar.javac_flags += ['-Xlint:all,-unchecked'] + +# Android Eclipse project. +main = add_android_eclipse_project('BrowserInstrumentationTests', OBJDIR + '/AndroidManifest.xml') +# The package name doesn't really matter, but it looks nicest if the +# generated classes (org.mozilla.gecko.browser.tests.{BuildConfig,R}) +# are in the same hierarchy as the rest of the source files. +main.package_name = 'org.mozilla.gecko.browser.tests' +main.res = 'res' +main.recursive_make_targets += [ + OBJDIR + '/AndroidManifest.xml', +] +main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in jar.generated_sources] +main.referenced_projects += ['Fennec'] + +main.add_classpathentry('src', SRCDIR + '/src', + dstdir='src/org/mozilla/gecko/browser')
new file mode 100644 index 0000000000000000000000000000000000000000..5c6a89f801c19b84469c7a609fca180c07e8bd41 GIT binary patch literal 7834 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 MWuRHEZWHnU0FzMS2mk;8
new file mode 100644 index 0000000000000000000000000000000000000000..7e383f14932ff0635f6bcf0807a2ce97c6a49c41 GIT binary patch literal 3279 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
new file mode 100644 index 0000000000000000000000000000000000000000..f2d917b905cb81ca29a8d528b4f0c33823332623 GIT binary patch literal 4738 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
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/res/layout/main.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="@string/app_name" /> + +</LinearLayout>
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Gecko Browser Tests</string> + +</resources>
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/src/harness/BrowserInstrumentationTestRunner.java @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browser.harness; + +import android.os.Bundle; +import android.test.AndroidTestRunner; +import android.test.InstrumentationTestRunner; +import android.util.Log; + +/** + * A test runner that installs a special test listener. + * <p> + * In future, this listener will turn JUnit 3 test events into log messages in + * the format that Mochitest parsers understand. + */ +public class BrowserInstrumentationTestRunner extends InstrumentationTestRunner { + private static final String LOG_TAG = "BInstTestRunner"; + + @Override + public void onCreate(Bundle arguments) { + Log.d(LOG_TAG, "onCreate"); + super.onCreate(arguments); + } + + @Override + protected AndroidTestRunner getAndroidTestRunner() { + Log.d(LOG_TAG, "getAndroidTestRunner"); + AndroidTestRunner testRunner = super.getAndroidTestRunner(); + testRunner.addTestListener(new BrowserTestListener()); + return testRunner; + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/src/harness/BrowserTestListener.java @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browser.harness; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestListener; +import android.util.Log; + +/** + * BrowserTestListener turns JUnit 3 test events into log messages in the format + * that Mochitest parsers understand. + * <p> + * The idea is that, on infrastructure, we'll be able to use the same test + * parsing code for Browser JUnit 3 tests as we do for Robocop tests. + * <p> + * In future, that is! + */ +public class BrowserTestListener implements TestListener { + public static final String LOG_TAG = "BTestListener"; + + @Override + public void startTest(Test test) { + Log.d(LOG_TAG, "startTest: " + test); + } + + @Override + public void endTest(Test test) { + Log.d(LOG_TAG, "endTest: " + test); + } + + @Override + public void addFailure(Test test, AssertionFailedError t) { + Log.d(LOG_TAG, "addFailure: " + test); + } + + @Override + public void addError(Test test, Throwable t) { + Log.d(LOG_TAG, "addError: " + test); + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/src/tests/BrowserTestCase.java @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browser.tests; + +import org.mozilla.gecko.AppConstants; + +import android.app.Activity; +import android.content.Context; +import android.test.ActivityInstrumentationTestCase2; + +/** + * BrowserTestCase provides helper methods for testing. + */ +public class BrowserTestCase extends ActivityInstrumentationTestCase2<Activity> { + private static String LOG_TAG = "BrowserTestCase"; + + private static final String LAUNCHER_ACTIVITY = AppConstants.ANDROID_PACKAGE_NAME + ".App"; + + private final static Class<Activity> sLauncherActivityClass; + + static { + try { + sLauncherActivityClass = (Class<Activity>) Class.forName(LAUNCHER_ACTIVITY); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public BrowserTestCase() { + super(sLauncherActivityClass); + } + + public Context getApplicationContext() { + return this.getInstrumentation().getTargetContext().getApplicationContext(); + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/junit3/src/tests/TestJarReader.java @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browser.tests; + +import java.io.InputStream; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.util.GeckoJarReader; + +/** + * A basic jar reader test. Tests reading a png from fennec's apk, as well as + * loading some invalid jar urls. + */ +public class TestJarReader extends BrowserTestCase { + public void testJarReader() { + String appPath = getActivity().getApplication().getPackageResourcePath(); + assertNotNull(appPath); + + // Test reading a file from a jar url that looks correct. + String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME; + InputStream stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + assertNotNull(stream); + + // Test looking for an non-existent file in a jar. + url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png"); + assertNull(stream); + + // Test looking for a file that doesn't exist in the APK. + url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + assertNull(stream); + + // Test looking for an jar with an invalid url. + url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png"); + assertNull(stream); + + // Test looking for a file that doesn't exist on disk. + url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + assertNull(stream); + } +}
copy from mobile/android/tests/moz.build copy to mobile/android/tests/browser/moz.build --- a/mobile/android/tests/moz.build +++ b/mobile/android/tests/browser/moz.build @@ -1,9 +1,9 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. TEST_DIRS += [ - 'background', + 'junit3', ]
--- a/mobile/android/tests/moz.build +++ b/mobile/android/tests/moz.build @@ -1,9 +1,10 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. TEST_DIRS += [ 'background', + 'browser', ]
--- a/python/mozbuild/mozbuild/backend/android_eclipse.py +++ b/python/mozbuild/mozbuild/backend/android_eclipse.py @@ -117,16 +117,22 @@ class AndroidEclipseBackend(CommonBacken def _manifest_for_project(self, srcdir, project): manifest = InstallManifest() if project.manifest: manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml') if project.res: manifest.add_symlink(mozpath.join(srcdir, project.res), 'res') + else: + # Eclipse expects a res directory no matter what, so we + # make an empty directory if the project doesn't specify. + res = os.path.abspath(mozpath.join(os.path.dirname(__file__), + 'templates', 'android_eclipse_empty_resource_directory')) + manifest.add_pattern_copy(res, '.**', 'res') if project.assets: manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets') for cpe in project._classpathentries: manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) # JARs and native libraries go in the same place. For now,
new file mode 100644 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PostBuilder.launch @@ -0,0 +1,18 @@ +#filter substitution +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType"> +<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_CLEAN_TARGETS" value="post_build,"/> +<booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/> +<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/> +<booleanAttribute key="org.eclipse.debug.core.capture_output" value="true"/> +<booleanAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON" value="true"/> +<stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${project}"/> +<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/> +<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/> +<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="@IDE_PROJECT_NAME@"/> +<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/post_build.xml}"/> +<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="clean"/> +<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/> +<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-Dbuild_type="${build_type}" -Dbuild_files="DUMMY ${build_files}""/> +</launchConfiguration>
rename from python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/Builder.launch rename to python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PreBuilder.launch --- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/Builder.launch +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PreBuilder.launch @@ -1,19 +1,19 @@ #filter substitution <?xml version="1.0" encoding="UTF-8" standalone="no"?> <launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType"> -<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="compile,"/> -<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="compile,"/> +<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="pre_build,"/> +<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="pre_build,"/> <booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/> <booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/> <booleanAttribute key="org.eclipse.debug.core.capture_output" value="true"/> <booleanAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON" value="true"/> <stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${project}"/> <booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/> <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/> <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/> <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="@IDE_PROJECT_NAME@"/> -<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/build.xml}"/> +<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/pre_build.xml}"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="incremental,auto"/> <booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-Dbuild_type="${build_type}" -Dbuild_files="DUMMY ${build_files}""/> </launchConfiguration>
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.project +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.project @@ -6,17 +6,17 @@ <projects> </projects> <buildSpec> <buildCommand> <name>org.eclipse.ui.externaltools.ExternalToolBuilder</name> <arguments> <dictionary> <key>LaunchConfigHandle</key> - <value><project>/.externalToolBuilders/Builder.launch</value> + <value><project>/.externalToolBuilders/PreBuilder.launch</value> </dictionary> </arguments> </buildCommand> <buildCommand> <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name> <arguments> </arguments> </buildCommand> @@ -30,14 +30,23 @@ <arguments> </arguments> </buildCommand> <buildCommand> <name>com.android.ide.eclipse.adt.ApkBuilder</name> <arguments> </arguments> </buildCommand> + <buildCommand> + <name>org.eclipse.ui.externaltools.ExternalToolBuilder</name> + <arguments> + <dictionary> + <key>LaunchConfigHandle</key> + <value><project>/.externalToolBuilders/PostBuilder.launch</value> + </dictionary> + </arguments> + </buildCommand> </buildSpec> <natures> <nature>com.android.ide.eclipse.adt.AndroidNature</nature> <nature>org.eclipse.jdt.core.javanature</nature> </natures> </projectDescription>
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.core.prefs +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.core.prefs @@ -5,8 +5,288 @@ org.eclipse.jdt.core.compiler.codegen.ta org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.source=1.6 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=80 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=false +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=false +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
new file mode 100644 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,57 @@ +#filter substitution +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=false +cleanup.format_source_code_changes_only=false +cleanup.make_local_variable_final=true +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=true +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=false +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_blocks=false +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup_profile=_Fennec +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_Fennec +formatter_settings_version=12
new file mode 100644 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/post_build.xml @@ -0,0 +1,45 @@ +#filter substitution +<?xml version="1.0" encoding="UTF-8"?> +<project> + <property name="topsrcdir" value="@IDE_TOPSRCDIR@"/> + <property name="topobjdir" value="@IDE_TOPOBJDIR@"/> + <property name="objdir" value="@IDE_OBJDIR@"/> + <property name="project_name" value="@IDE_PROJECT_NAME@"/> + + <!-- This file can get large (!), but for a short time we want to + log as much information for debugging build loops as possible. --> + <record name="${topobjdir}/android_eclipse/build.log" append="yes" /> + + <target name="build_needed" > + + <script language="javascript" > +<![CDATA[ + importClass(java.io.File); + + var build_files = project.getProperty("build_files").split(" "); + var after = []; + + var echo = project.createTask("echo"); + var info = Packages.org.apache.tools.ant.taskdefs.Echo.EchoLevel(); + info.setValue("info"); + echo.setLevel(info); + + // Timestamp. + echo.addText(project.getProperty("project_name") + " build type " + project.getProperty("build_type") + " started at: " + new Date()); + echo.addText(project.getProperty("line.separator")); + + echo.perform(); + + // The if below checks for the property being defined, not its value. + project.setProperty("build_needed", build_needed); +]]> + </script> + </target> + + <target name="post_build" depends="build_needed" if="build_needed"> + <exec executable="${topsrcdir}/mach" dir="${topobjdir}" failonerror="true"> + <arg value="build"/> + <arg value="${objdir}/ANDROID_ECLIPSE_PROJECT_${project_name}"/> + </exec> + </target> +</project>
rename from python/mozbuild/mozbuild/backend/templates/android_eclipse/build.xml rename to python/mozbuild/mozbuild/backend/templates/android_eclipse/pre_build.xml --- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/build.xml +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/pre_build.xml @@ -65,15 +65,15 @@ // The if below checks for the property being defined, not its value. if (build_needed) { project.setProperty("build_needed", build_needed); } ]]> </script> </target> - <target name="compile" depends="build_needed" if="build_needed"> + <target name="pre_build" depends="build_needed" if="build_needed"> <exec executable="${topsrcdir}/mach" dir="${topobjdir}" failonerror="true"> <arg value="build"/> <arg value="${objdir}/ANDROID_ECLIPSE_PROJECT_${project_name}"/> </exec> </target> </project>
new file mode 100644 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse_empty_resource_directory/.not_an_android_resource @@ -0,0 +1,5 @@ +This file is named such that it is ignored by Android aapt. The file +itself ensures that the AndroidEclipse build backend can create an +empty res/ directory for projects explicitly specifying that it has no +resource directory. This is necessary because the Android Eclipse +plugin requires that each project have a res/ directory.
--- a/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py +++ b/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py @@ -42,17 +42,18 @@ class TestAndroidEclipseBackend(BackendT def test_main_project_files(self): """Ensure we generate reasonable files for main (non-library) projects.""" self.env = self._consume('android_eclipse', AndroidEclipseBackend) for f in ['.classpath', '.externalToolBuilders', '.project', '.settings', - 'build.xml', + 'pre_build.xml', + 'post_build.xml', 'gen', 'lint.xml', 'project.properties']: self.assertExists('main1', f) def test_library_manifest(self): """Ensure we generate manifest for library projects.""" self.env = self._consume('android_eclipse', AndroidEclipseBackend)
--- a/services/fxaccounts/FxAccountsManager.jsm +++ b/services/fxaccounts/FxAccountsManager.jsm @@ -16,19 +16,16 @@ this.EXPORTED_SYMBOLS = ["FxAccountsMana const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FxAccounts.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/FxAccountsCommon.js"); -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", - "resource://gre/modules/FxAccountsClient.jsm"); - this.FxAccountsManager = { init: function() { Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); }, observe: function(aSubject, aTopic, aData) { if (aTopic !== ONLOGOUT_NOTIFICATION) { @@ -78,20 +75,23 @@ this.FxAccountsManager = { return error; }, _serverError: function(aServerResponse) { let error = this._getError({ error: aServerResponse }); return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse); }, - // As we do with _fxAccounts, we don't really need this factory, but this way - // we allow tests to mock FxAccountsClient. - _createFxAccountsClient: function() { - return new FxAccountsClient(); + // As with _fxAccounts, we don't really need this method, but this way we + // allow tests to mock FxAccountsClient. By default, we want to return the + // client used by the fxAccounts object because deep down they should have + // access to the same hawk request object which will enable them to share + // local clock skeq data. + _getFxAccountsClient: function() { + return this._fxAccounts.getAccountsClient(); }, _signInSignUp: function(aMethod, aAccountId, aPassword) { if (Services.io.offline) { return this._error(ERROR_OFFLINE); } if (!aAccountId) { @@ -104,17 +104,17 @@ this.FxAccountsManager = { // Check that there is no signed in account first. if (this._activeSession) { return this._error(ERROR_ALREADY_SIGNED_IN_USER, { user: this._user }); } - let client = this._createFxAccountsClient(); + let client = this._getFxAccountsClient(); return this._fxAccounts.getSignedInUser().then( user => { if (user) { return this._error(ERROR_ALREADY_SIGNED_IN_USER, { user: this._user }); } return client[aMethod](aAccountId, aPassword); @@ -123,19 +123,21 @@ this.FxAccountsManager = { user => { let error = this._getError(user); if (!user || !user.uid || !user.sessionToken || error) { return this._error(error ? error : ERROR_INTERNAL_INVALID_USER, { user: user }); } - // Save the credentials of the signed in user. - user.email = aAccountId; - return this._fxAccounts.setSignedInUser(user, false).then( + // If the user object includes an email field, it may differ in + // capitalization from what we sent down. This is the server's + // canonical capitalization and should be used instead. + user.email = user.email || aAccountId; + return this._fxAccounts.setSignedInUser(user).then( () => { this._activeSession = user; log.debug("User signed in: " + JSON.stringify(this._user) + " - Account created " + (aMethod == "signUp")); return Promise.resolve({ accountCreated: aMethod === "signUp", user: this._user }); @@ -166,17 +168,17 @@ this.FxAccountsManager = { // At this point the local session should already be removed. // The client can create new sessions up to the limit (100?). // Orphaned tokens on the server will eventually be garbage collected. if (Services.io.offline) { return Promise.resolve(); } // Otherwise, we try to remove the remote session. - let client = this._createFxAccountsClient(); + let client = this._getFxAccountsClient(); return client.signOut(sessionToken).then( result => { let error = this._getError(result); if (error) { return this._error(error, result); } log.debug("Signed out"); return Promise.resolve(); @@ -288,17 +290,17 @@ this.FxAccountsManager = { } let deferred = Promise.defer(); if (!aAccountId) { return this._error(ERROR_INVALID_ACCOUNTID); } - let client = this._createFxAccountsClient(); + let client = this._getFxAccountsClient(); return client.accountExists(aAccountId).then( result => { log.debug("Account " + result ? "" : "does not" + " exists"); let error = this._getError(result); if (error) { return this._error(error, result); } @@ -322,17 +324,17 @@ this.FxAccountsManager = { log.debug("Account already verified"); return Promise.resolve(this._user); } if (Services.io.offline) { return this._error(ERROR_OFFLINE); } - let client = this._createFxAccountsClient(); + let client = this._getFxAccountsClient(); return client.recoveryEmailStatus(this._activeSession.sessionToken).then( data => { let error = this._getError(data); if (error) { return this._error(error, data); } // If the verification status is different from the one that we have
--- a/services/fxaccounts/tests/xpcshell/test_manager.js +++ b/services/fxaccounts/tests/xpcshell/test_manager.js @@ -135,17 +135,18 @@ FxAccountsManager._fxAccounts = { this._signedInUser = null; Services.obs.notifyObservers(null, ONLOGOUT_NOTIFICATION, null); deferred.resolve(); return deferred.promise; } }; // Save original FxAccountsClient factory from FxAccountsManager. -const kFxAccountsClient = FxAccountsManager._createFxAccountsClient; +const kFxAccountsClient = FxAccountsManager._getFxAccountsClient; + // and change it for a fake client factory. let FakeFxAccountsClient = { _reject: false, _recoveryEmailStatusCalled: false, _signInCalled: false, _signUpCalled: false, _signOutCalled: false, @@ -196,19 +197,21 @@ let FakeFxAccountsClient = { accountExists: function() { let deferred = Promise.defer(); this._reject ? deferred.reject() : deferred.resolve(this._accountExists); return deferred.promise; } }; -FxAccountsManager._createFxAccountsClient = function() { + +FxAccountsManager._getFxAccountsClient = function() { return FakeFxAccountsClient; -} +}; + // === Global cleanup === // Unregister mocks and restore original code. do_register_cleanup(function() { // Unregister the factory so we do not leak Cm.QueryInterface(Ci.nsIComponentRegistrar) .unregisterFactory(Components.ID(kFxAccountsUIGlueUUID), @@ -220,17 +223,17 @@ do_register_cleanup(function() { "FxAccountsUIGlue", kFxAccountsUIGlueContractID, kFxAccountsUIGlueFactory); // Restore the original FxAccounts instance from FxAccountsManager. FxAccountsManager._fxAccounts = kFxAccounts; // Restore the FxAccountsClient getter from FxAccountsManager. - FxAccountsManager._createFxAccountsClient = kFxAccountsClient; + FxAccountsManager._getFxAccountsClient = kFxAccountsClient; }); // === Tests === function run_test() { run_next_test(); }
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -102,17 +102,17 @@ function lazyPathGetter(constProp, dirKe delete SharedAll.Constants.Path[constProp]; SharedAll.Constants.Path[constProp] = path; } catch (ex) { // Ignore errors if the value still isn't available. Hopefully // the next access will return it. } return path; - } + }; } for (let [constProp, dirKey] of [ ["localProfileDir", "ProfLD"], ["profileDir", "ProfD"], ["userApplicationDataDir", "UAppData"], ["winAppDataDir", "AppData"], ["winStartMenuProgsDir", "Progs"], @@ -151,26 +151,27 @@ let Scheduler = { /** * A promise resolved once all operations are complete. * * This promise is never rejected and the result is always undefined. */ queue: Promise.resolve(), /** - * The latest message sent and still waiting for a reply. This - * field is stored only in DEBUG builds, to avoid hoarding memory in - * release builds. + * The latest message sent and still waiting for a reply. In DEBUG + * builds, the entire message is stored, which may be memory-consuming. + * In non-DEBUG builds, only the method name is stored. */ latestSent: undefined, /** * The latest reply received, or null if we are waiting for a reply. - * This field is stored only in DEBUG builds, to avoid hoarding - * memory in release builds. + * In DEBUG builds, the entire response is stored, which may be + * memory-consuming. In non-DEBUG builds, only exceptions and + * method names are stored. */ latestReceived: undefined, /** * A timer used to automatically shut down the worker after some time. */ resetTimer: null, @@ -236,41 +237,50 @@ let Scheduler = { // By convention, the last argument of any message may be an |options| object. let options; let methodArgs = args[0]; if (methodArgs) { options = methodArgs[methodArgs.length - 1]; } return this.push(() => Task.spawn(function*() { + Scheduler.latestReceived = null; if (OS.Constants.Sys.DEBUG) { // Update possibly memory-expensive debugging information - Scheduler.latestReceived = null; - Scheduler.latestSent = [method, ...args]; + Scheduler.latestSent = [Date.now(), method, ...args]; + } else { + Scheduler.latestSent = [Date.now(), method]; } let data; let reply; + let isError = false; try { data = yield worker.post(method, ...args); reply = data; } catch (error if error instanceof PromiseWorker.WorkerError) { reply = error; + isError = true; throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data); } catch (error if error instanceof ErrorEvent) { reply = error; let message = error.message; if (message == "uncaught exception: [object StopIteration]") { throw StopIteration; } + isError = true; throw new Error(message, error.filename, error.lineno); } finally { + Scheduler.latestSent = Scheduler.latestSent.slice(0, 2); if (OS.Constants.Sys.DEBUG) { // Update possibly memory-expensive debugging information - Scheduler.latestSent = null; - Scheduler.latestReceived = reply; + Scheduler.latestReceived = [Date.now(), reply]; + } else if (isError) { + Scheduler.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber]; + } else { + Scheduler.latestReceived = [Date.now()]; } if (firstLaunch) { Scheduler._updateTelemetry(); } // Don't restart the timer when reseting the worker, since that will // lead to an endless "resetWorker()" loop. if (method != "Meta_reset") { @@ -1294,22 +1304,40 @@ this.OS.Path = Path; // Auto-flush OS.File during profile-before-change. This ensures that any I/O // that has been queued *before* profile-before-change is properly completed. // To ensure that I/O queued *during* profile-before-change is completed, // clients should register using AsyncShutdown.addBlocker. AsyncShutdown.profileBeforeChange.addBlocker( "OS.File: flush I/O queued before profile-before-change", // Wait until the latest currently enqueued promise is satisfied/rejected - (() => Scheduler.queue), + function() { + let DEBUG = false; + try { + DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown"); + } catch (ex) { + // Ignore + } + if (DEBUG) { + // Return a promise that will never be satisfied + return Promise.defer().promise; + } else { + return Scheduler.queue; + } + }, function getDetails() { let result = { launched: Scheduler.launched, shutdown: Scheduler.shutdown, + worker: !!worker, pendingReset: !!Scheduler.resetTimer, + latestSent: Scheduler.latestSent, + latestReceived: Scheduler.latestReceived }; - if (OS.Constants.Sys.DEBUG) { - result.latestSent = Scheduler.latestSent; - result.latestReceived - Scheduler.latestReceived; - }; + // Convert dates to strings for better readability + for (let key of ["latestSent", "latestReceived"]) { + if (result[key] && typeof result[key][0] == "number") { + result[key][0] = Date(result[key][0]); + } + } return result; } );
--- a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm +++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm @@ -1233,22 +1233,25 @@ exports.normalizeToPointer = normalizeTo /** * An OS error. * * This class is provided mostly for type-matching. If you need more * details about an error, you should use the platform-specific error * codes provided by subclasses of |OS.Shared.Error|. * * @param {string} operation The operation that failed. + * @param {string=} path The path of the file on which the operation failed, + * or nothing if there was no file involved in the failure. * * @constructor */ -function OSError(operation) { +function OSError(operation, path = "") { Error.call(this); this.operation = operation; + this.path = path; } exports.OSError = OSError; ///////////////////// Temporary boilerplate // Boilerplate, to simplify the transition to require() // Do not rely upon this symbol, it will disappear with // bug 883050.
--- a/toolkit/components/osfile/modules/osfile_shared_front.jsm +++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm @@ -20,33 +20,38 @@ let Lz4 = require("resource://gre/modules/workers/lz4.js"); let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end"); let clone = SharedAll.clone; /** * Code shared by implementations of File. * * @param {*} fd An OS-specific file handle. + * @param {string} path File path of the file handle, used for error-reporting. * @constructor */ -let AbstractFile = function AbstractFile(fd) { +let AbstractFile = function AbstractFile(fd, path) { this._fd = fd; + if (!path) { + throw new TypeError("path is expected"); + } + this._path = path; }; AbstractFile.prototype = { /** * Return the file handle. * * @throw OS.File.Error if the file has been closed. */ get fd() { if (this._fd) { return this._fd; } - throw OS.File.Error.closed(); + throw OS.File.Error.closed("accessing file", this._path); }, /** * Read bytes from this file to a new buffer. * * @param {number=} bytes If unspecified, read all the remaining bytes from * this file. If specified, read |bytes| bytes, or less if the file does notclone * contain that many bytes. * @param {JSON} options @@ -178,17 +183,17 @@ AbstractFile.openUnique = function openU return { path: uniquePath, file: OS.File.open(uniquePath, mode) }; } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { // keep trying ... } } - throw OS.File.Error.exists("could not find an unused file name."); + throw OS.File.Error.exists("could not find an unused file name.", path); } }; /** * Code shared by iterators. */ AbstractFile.AbstractIterator = function AbstractIterator() { }; @@ -390,17 +395,17 @@ AbstractFile.writeAtomic = function writeAtomic(path, buffer, options = {}) { // Verify that path is defined and of the correct type if (typeof path != "string" || path == "") { throw new TypeError("File path should be a (non-empty) string"); } let noOverwrite = options.noOverwrite; if (noOverwrite && OS.File.exists(path)) { - throw OS.File.Error.exists("writeAtomic"); + throw OS.File.Error.exists("writeAtomic", path); } if (typeof buffer == "string") { // Normalize buffer to a C buffer by encoding it let encoding = options.encoding || "utf-8"; buffer = new TextEncoder(encoding).encode(buffer); }
--- a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm +++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm @@ -68,29 +68,33 @@ libc.declareLazy(LazyBindings, "strerror * of this field against the error constants of |OS.Constants.libc|. * * @param {string=} operation The operation that failed. If unspecified, * the name of the calling function is taken to be the operation that * failed. * @param {number=} lastError The OS-specific constant detailing the * reason of the error. If unspecified, this is fetched from the system * status. + * @param {string=} path The file path that manipulated. If unspecified, + * assign the empty string. * * @constructor * @extends {OS.Shared.Error} */ -let OSError = function OSError(operation, errno) { - operation = operation || "unknown operation"; - SharedAll.OSError.call(this, operation); - this.unixErrno = errno || ctypes.errno; +let OSError = function OSError(operation = "unknown operation", + errno = ctypes.errno, path = "") { + operation = operation; + SharedAll.OSError.call(this, operation, path); + this.unixErrno = errno; }; OSError.prototype = Object.create(SharedAll.OSError.prototype); OSError.prototype.toString = function toString() { return "Unix error " + this.unixErrno + " during operation " + this.operation + + (this.path? " on file " + this.path : "") + " (" + LazyBindings.strerror(this.unixErrno).readString() + ")"; }; /** * |true| if the error was raised because a file or directory * already exists, |false| otherwise. */ Object.defineProperty(OSError.prototype, "becauseExists", { @@ -138,49 +142,59 @@ Object.defineProperty(OSError.prototype, /** * Serialize an instance of OSError to something that can be * transmitted across threads (not necessarily a string). */ OSError.toMsg = function toMsg(error) { return { operation: error.operation, - unixErrno: error.unixErrno + unixErrno: error.unixErrno, + path: error.path }; }; /** * Deserialize a message back to an instance of OSError */ OSError.fromMsg = function fromMsg(msg) { - return new OSError(msg.operation, msg.unixErrno); + return new OSError(msg.operation, msg.unixErrno, msg.path); }; exports.Error = OSError; /** * Code shared by implementations of File.Info on Unix * * @constructor */ -let AbstractInfo = function AbstractInfo(isDir, isSymLink, size, lastAccessDate, +let AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size, lastAccessDate, lastModificationDate, unixLastStatusChangeDate, unixOwner, unixGroup, unixMode) { + this._path = path; this._isDir = isDir; this._isSymlLink = isSymLink; this._size = size; this._lastAccessDate = lastAccessDate; this._lastModificationDate = lastModificationDate; this._unixLastStatusChangeDate = unixLastStatusChangeDate; this._unixOwner = unixOwner; this._unixGroup = unixGroup; this._unixMode = unixMode; }; AbstractInfo.prototype = { /** + * The path of the file, used for error-reporting. + * + * @type {string} + */ + get path() { + return this._path; + }, + /** * |true| if this file is a directory, |false| otherwise */ get isDir() { return this._isDir; }, /** * |true| if this file is a symbolink link, |false| otherwise */ @@ -300,26 +314,26 @@ exports.Type = Type; * Native paths * * Under Unix, expressed as C strings */ Type.path = Type.cstring.withName("[in] path"); Type.out_path = Type.out_cstring.withName("[out] path"); // Special constructors that need to be defined on all threads -OSError.closed = function closed(operation) { - return new OSError(operation, Const.EBADF); +OSError.closed = function closed(operation, path) { + return new OSError(operation, Const.EBADF, path); }; -OSError.exists = function exists(operation) { - return new OSError(operation, Const.EEXIST); +OSError.exists = function exists(operation, path) { + return new OSError(operation, Const.EEXIST, path); }; -OSError.noSuchFile = function noSuchFile(operation) { - return new OSError(operation, Const.ENOENT); +OSError.noSuchFile = function noSuchFile(operation, path) { + return new OSError(operation, Const.ENOENT, path); }; let EXPORTED_SYMBOLS = [ "declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry",
--- a/toolkit/components/osfile/modules/osfile_unix_front.jsm +++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm @@ -35,20 +35,21 @@ /** * Representation of a file. * * You generally do not need to call this constructor yourself. Rather, * to open a file, use function |OS.File.open|. * * @param fd A OS-specific file descriptor. + * @param {string} path File path of the file handle, used for error-reporting. * @constructor */ - let File = function File(fd) { - exports.OS.Shared.AbstractFile.call(this, fd); + let File = function File(fd, path) { + exports.OS.Shared.AbstractFile.call(this, fd, path); this._closeResult = null; }; File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype); /** * Close the file. * * This method has no effect if the file is already closed. However, @@ -65,17 +66,17 @@ // Call |close(fd)|, detach finalizer if any // (|fd| may not be a CDataFinalizer if it has been // instantiated from a controller thread). let result = UnixFile._close(fd); if (typeof fd == "object" && "forget" in fd) { fd.forget(); } if (result == -1) { - this._closeResult = new File.Error("close"); + this._closeResult = new File.Error("close", ctypes.errno, this._path); } } if (this._closeResult) { throw this._closeResult; } return; }; @@ -98,17 +99,18 @@ // Populate the page cache with data from a file so the subsequent reads // from that file will not block on disk I/O. if (typeof(UnixFile.posix_fadvise) === 'function' && (options.sequential || !("sequential" in options))) { UnixFile.posix_fadvise(this.fd, 0, nbytes, OS.Constants.libc.POSIX_FADV_SEQUENTIAL); } return throw_on_negative("read", - UnixFile.read(this.fd, buffer, nbytes) + UnixFile.read(this.fd, buffer, nbytes), + this._path ); }; /** * Write some bytes to a file. * * @param {C pointer} buffer A buffer holding the data that must be * written. @@ -117,17 +119,18 @@ * @param {*=} options Additional options for writing. Ignored in * this implementation. * * @return {number} The number of bytes effectively written. * @throws {OS.File.Error} In case of I/O error. */ File.prototype._write = function _write(buffer, nbytes, options = {}) { return throw_on_negative("write", - UnixFile.write(this.fd, buffer, nbytes) + UnixFile.write(this.fd, buffer, nbytes), + this._path ); }; /** * Return the current position in the file. */ File.prototype.getPosition = function getPosition(pos) { return this.setPosition(0, File.POS_CURRENT); @@ -149,28 +152,30 @@ * * @return The new position in the file. */ File.prototype.setPosition = function setPosition(pos, whence) { if (whence === undefined) { whence = Const.SEEK_SET; } return throw_on_negative("setPosition", - UnixFile.lseek(this.fd, pos, whence) + UnixFile.lseek(this.fd, pos, whence), + this._path ); }; /** * Fetch the information on the file. * * @return File.Info The information on |this| file. */ File.prototype.stat = function stat() { - throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr)); - return new File.Info(gStatData); + throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr), + this._path); + return new File.Info(gStatData, this._path); }; /** * Set the last access and modification date of the file. * The time stamp resolution is 1 second at best, but might be worse * depending on the platform. * * @param {Date,number=} accessDate The last access date. If numeric, @@ -187,32 +192,33 @@ accessDate = normalizeDate("File.prototype.setDates", accessDate); modificationDate = normalizeDate("File.prototype.setDates", modificationDate); gTimevals[0].tv_sec = (accessDate / 1000) | 0; gTimevals[0].tv_usec = 0; gTimevals[1].tv_sec = (modificationDate / 1000) | 0; gTimevals[1].tv_usec = 0; throw_on_negative("setDates", - UnixFile.futimes(this.fd, gTimevalsPtr)); + UnixFile.futimes(this.fd, gTimevalsPtr), + this._path); }; /** * Flushes the file's buffers and causes all buffered data * to be written. * Disk flushes are very expensive and therefore should be used carefully, * sparingly and only in scenarios where it is vital that data survives * system crashes. Even though the function will be executed off the * main-thread, it might still affect the overall performance of any * running application. * * @throws {OS.File.Error} In case of I/O error. */ File.prototype.flush = function flush() { - throw_on_negative("flush", UnixFile.fsync(this.fd)); + throw_on_negative("flush", UnixFile.fsync(this.fd), this._path); }; // The default unix mode for opening (0600) const DEFAULT_UNIX_MODE = 384; /** * Open a file * @@ -288,17 +294,17 @@ // flags are sufficient } else if (!mode.existing) { flags |= Const.O_CREAT; } if (mode.append) { flags |= Const.O_APPEND; } } - return error_or_file(UnixFile.open(path, flags, omode)); + return error_or_file(UnixFile.open(path, flags, omode), path); }; /** * Checks if a file exists * * @param {string} path The path to the file. * * @return {bool} true if the file exists, false otherwise. @@ -323,17 +329,17 @@ */ File.remove = function remove(path, options = {}) { let result = UnixFile.unlink(path); if (result == -1) { if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && ctypes.errno == Const.ENOENT) { return; } - throw new File.Error("remove"); + throw new File.Error("remove", ctypes.errno, path); } }; /** * Remove an empty directory. * * @param {string} path The name of the directory to remove. * @param {*=} options Additional options. @@ -342,17 +348,17 @@ */ File.removeEmptyDir = function removeEmptyDir(path, options = {}) { let result = UnixFile.rmdir(path); if (result == -1) { if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && ctypes.errno == Const.ENOENT) { return; } - throw new File.Error("removeEmptyDir"); + throw new File.Error("removeEmptyDir", ctypes.errno, path); } }; /** * Gets the number of bytes available on disk to the current user. * * @param {string} sourcePath Platform-specific path to a directory on * the disk to query for free available bytes. @@ -394,17 +400,17 @@ File.makeDir = function makeDir(path, options = {}) { let omode = options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE_DIR; let result = UnixFile.mkdir(path, omode); if (result == -1) { if ((!("ignoreExisting" in options) || options.ignoreExisting) && (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR)) { return; } - throw new File.Error("makeDir"); + throw new File.Error("makeDir", ctypes.errno, path); } }; /** * Copy a file to a destination. * * @param {string} sourcePath The platform-specific path at which * the file may currently be found. @@ -461,17 +467,18 @@ // Adding copying of hierarchies and/or attributes is just a flag // away. File.copy = function copyfile(sourcePath, destPath, options = {}) { let flags = Const.COPYFILE_DATA; if (options.noOverwrite) { flags |= Const.COPYFILE_EXCL; } throw_on_negative("copy", - UnixFile.copyfile(sourcePath, destPath, null, flags) + UnixFile.copyfile(sourcePath, destPath, null, flags), + sourcePath ); }; } else { // If the OS does not implement file copying for us, we need to // implement it ourselves. For this purpose, we need to define // a pumping function. /** @@ -639,34 +646,34 @@ // across file systems // If necessary, fail if the destination file exists if (options.noOverwrite) { let fd = UnixFile.open(destPath, Const.O_RDONLY, 0); if (fd != -1) { fd.dispose(); // The file exists and we have access - throw new File.Error("move", Const.EEXIST); + throw new File.Error("move", Const.EEXIST, sourcePath); } else if (ctypes.errno == Const.EACCESS) { // The file exists and we don't have access - throw new File.Error("move", Const.EEXIST); + throw new File.Error("move", Const.EEXIST, sourcePath); } } // If we can, rename the file let result = UnixFile.rename(sourcePath, destPath); if (result != -1) return; // If the error is not EXDEV ("not on the same device"), // or if the error is EXDEV and we have passed an option // that prevents us from crossing devices, throw the // error. if (ctypes.errno != Const.EXDEV || options.noCopy) { - throw new File.Error("move"); + throw new File.Error("move", ctypes.errno, sourcePath); } // Otherwise, copy and remove. File.copy(sourcePath, destPath, options); // FIXME: Clean-up in case of copy error? File.remove(sourcePath); }; @@ -684,17 +691,17 @@ */ File.DirectoryIterator = function DirectoryIterator(path, options) { exports.OS.Shared.AbstractFile.AbstractIterator.call(this); this._path = path; this._dir = UnixFile.opendir(this._path); if (this._dir == null) { let error = ctypes.errno; if (error != Const.ENOENT) { - throw new File.Error("DirectoryIterator", error); + throw new File.Error("DirectoryIterator", error, path); } this._exists = false; this._closed = true; } else { this._exists = true; this._closed = false; } }; @@ -707,17 +714,17 @@ * Skip special directories "." and "..". * * @return {File.Entry} The next entry in the directory. * @throws {StopIteration} Once all files in the directory have been * encountered. */ File.DirectoryIterator.prototype.next = function next() { if (!this._exists) { - throw File.Error.noSuchFile("DirectoryIterator.prototype.next"); + throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path); } if (this._closed) { throw StopIteration; } for (let entry = UnixFile.readdir(this._dir); entry != null && !entry.isNull(); entry = UnixFile.readdir(this._dir)) { let contents = entry.contents; @@ -725,17 +732,17 @@ if (name == "." || name == "..") { continue; } let isDir, isSymLink; if (!("d_type" in contents)) { // |dirent| doesn't have d_type on some platforms (e.g. Solaris). let path = Path.join(this._path, name); - throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr)); + throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path); isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR; isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK; } else { isDir = contents.d_type == Const.DT_DIR; isSymLink = contents.d_type == Const.DT_LNK; } return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path); @@ -763,18 +770,18 @@ File.DirectoryIterator.prototype.exists = function exists() { return this._exists; }; /** * Return directory as |File| */ File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() { - if (!this._dir) throw File.Error.closed(); - return error_or_file(UnixFile.dirfd(this._dir)); + if (!this._dir) throw File.Error.closed("unixAsFile", this._path); + return error_or_file(UnixFile.dirfd(this._dir), this._path); }; /** * An entry in a directory. */ File.DirectoryIterator.Entry = function Entry(isDir, isSymLink, name, parent) { // Copy the relevant part of |unix_entry| to ensure that // our data is not overwritten prematurely. @@ -805,31 +812,31 @@ return serialized; }; let gStatData = new Type.stat.implementation(); let gStatDataPtr = gStatData.address(); let gTimevals = new Type.timevals.implementation(); let gTimevalsPtr = gTimevals.address(); let MODE_MASK = 4095 /*= 07777*/; - File.Info = function Info(stat) { + File.Info = function Info(stat, path) { let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR; let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK; let size = Type.off_t.importFromC(stat.st_size); let lastAccessDate = new Date(stat.st_atime * 1000); let lastModificationDate = new Date(stat.st_mtime * 1000); let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000); let unixOwner = Type.uid_t.importFromC(stat.st_uid); let unixGroup = Type.gid_t.importFromC(stat.st_gid); let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK); - SysAll.AbstractInfo.call(this, isDir, isSymLink, size, lastAccessDate, - lastModificationDate, unixLastStatusChangeDate, + SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size, + lastAccessDate, lastModificationDate, unixLastStatusChangeDate, unixOwner, unixGroup, unixMode); // Some platforms (e.g. MacOS X, some BSDs) store a file creation date if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) { let date = new Date(stat.st_birthtime * 1000); /** * The date of creation of this file. @@ -880,21 +887,21 @@ * - {bool} unixNoFollowingLinks If set and |true|, if |path| * represents a symbolic link, the call will return the information * of the link itself, rather than that of the target file. * * @return {File.Information} */ File.stat = function stat(path, options = {}) { if (options.unixNoFollowingLinks) { - throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr)); + throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path); } else { - throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr)); + throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path); } - return new File.Info(gStatData); + return new File.Info(gStatData, path); }; /** * Set the last access and modification date of the file. * The time stamp resolution is 1 second at best, but might be worse * depending on the platform. * * @param {string} path The full name of the file to set the dates for. @@ -911,40 +918,42 @@ File.setDates = function setDates(path, accessDate, modificationDate) { accessDate = normalizeDate("File.setDates", accessDate); modificationDate = normalizeDate("File.setDates", modificationDate); gTimevals[0].tv_sec = (accessDate / 1000) | 0; gTimevals[0].tv_usec = 0; gTimevals[1].tv_sec = (modificationDate / 1000) | 0; gTimevals[1].tv_usec = 0; throw_on_negative("setDates", - UnixFile.utimes(path, gTimevalsPtr)); + UnixFile.utimes(path, gTimevalsPtr), + path); }; File.read = exports.OS.Shared.AbstractFile.read; File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic; File.openUnique = exports.OS.Shared.AbstractFile.openUnique; File.removeDir = exports.OS.Shared.AbstractFile.removeDir; /** * Get the current directory by getCurrentDirectory. */ File.getCurrentDirectory = function getCurrentDirectory() { let path = UnixFile.get_current_dir_name?UnixFile.get_current_dir_name(): UnixFile.getwd_auto(null); - throw_on_null("getCurrentDirectory",path); + throw_on_null("getCurrentDirectory", path); return path.readString(); }; /** * Set the current directory by setCurrentDirectory. */ File.setCurrentDirectory = function setCurrentDirectory(path) { throw_on_negative("setCurrentDirectory", - UnixFile.chdir(path) + UnixFile.chdir(path), + path ); }; /** * Get/set the current directory. */ Object.defineProperty(File, "curDir", { set: function(path) { @@ -955,43 +964,58 @@ } } ); // Utility functions /** * Turn the result of |open| into an Error or a File + * @param {number} maybe The result of the |open| operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.errno, otherwise it returns the opened file. + * @param {string=} path The path of the file. */ - function error_or_file(maybe) { + function error_or_file(maybe, path) { if (maybe == -1) { - throw new File.Error("open"); + throw new File.Error("open", ctypes.errno, path); } - return new File(maybe); + return new File(maybe, path); } /** * Utility function to sort errors represented as "-1" from successes. * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. * @param {number} result The result of the operation that may * represent either an error or a success. If -1, this function raises * an error holding ctypes.errno, otherwise it returns |result|. - * @param {string=} operation The name of the operation. If unspecified, - * the name of the caller function. + * @param {string=} path The path of the file. */ - function throw_on_negative(operation, result) { + function throw_on_negative(operation, result, path) { if (result < 0) { - throw new File.Error(operation); + throw new File.Error(operation, ctypes.errno, path); } return result; } - function throw_on_null(operation, result) { + /** + * Utility function to sort errors represented as |null| from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {pointer} result The result of the operation that may + * represent either an error or a success. If |null|, this function raises + * an error holding ctypes.errno, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_null(operation, result, path) { if (result == null || (result.isNull && result.isNull())) { - throw new File.Error(operation); + throw new File.Error(operation, ctypes.errno, path); } return result; } /** * Normalize and verify a Date or numeric date value. * * @param {string} fn Function name of the calling function.
--- a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm +++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm @@ -74,24 +74,27 @@ libc.declareLazy(Scope, "FormatMessage", * of this field against the error constants of |OS.Constants.Win|. * * @param {string=} operation The operation that failed. If unspecified, * the name of the calling function is taken to be the operation that * failed. * @param {number=} lastError The OS-specific constant detailing the * reason of the error. If unspecified, this is fetched from the system * status. + * @param {string=} path The file path that manipulated. If unspecified, + * assign the empty string. * * @constructor * @extends {OS.Shared.Error} */ -let OSError = function OSError(operation, lastError) { - operation = operation || "unknown operation"; - SharedAll.OSError.call(this, operation); - this.winLastError = lastError || ctypes.winLastError; +let OSError = function OSError(operation = "unknown operation", + lastError = ctypes.winLastError, path = "") { + operation = operation; + SharedAll.OSError.call(this, operation, path); + this.winLastError = lastError; }; OSError.prototype = Object.create(SharedAll.OSError.prototype); OSError.prototype.toString = function toString() { let buf = new (ctypes.ArrayType(ctypes.jschar, 1024))(); let result = Scope.FormatMessage( Const.FORMAT_MESSAGE_FROM_SYSTEM | Const.FORMAT_MESSAGE_IGNORE_INSERTS, null, @@ -102,17 +105,18 @@ OSError.prototype.toString = function to /* Format args*/ null ); if (!result) { buf = "additional error " + ctypes.winLastError + " while fetching system error message"; } return "Win error " + this.winLastError + " during operation " - + this.operation + " (" + buf.readString() + ")"; + + this.operation + (this.path? " on file " + this.path : "") + + " (" + buf.readString() + ")"; }; /** * |true| if the error was raised because a file or directory * already exists, |false| otherwise. */ Object.defineProperty(OSError.prototype, "becauseExists", { get: function becauseExists() { @@ -160,45 +164,56 @@ Object.defineProperty(OSError.prototype, /** * Serialize an instance of OSError to something that can be * transmitted across threads (not necessarily a string). */ OSError.toMsg = function toMsg(error) { return { operation: error.operation, - winLastError: error.winLastError + winLastError: error.winLastError, + path: error.path }; }; /** * Deserialize a message back to an instance of OSError */ OSError.fromMsg = function fromMsg(msg) { - return new OSError(msg.operation, msg.winLastError); + return new OSError(msg.operation, msg.winLastError, msg.path); }; exports.Error = OSError; /** * Code shared by implementation of File.Info on Windows * * @constructor */ -let AbstractInfo = function AbstractInfo(isDir, isSymLink, size, winBirthDate, +let AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size, + winBirthDate, lastAccessDate, lastWriteDate) { + this._path = path; this._isDir = isDir; this._isSymLink = isSymLink; this._size = size; this._winBirthDate = winBirthDate; this._lastAccessDate = lastAccessDate; this._lastModificationDate = lastWriteDate; }; AbstractInfo.prototype = { /** + * The path of the file, used for error-reporting. + * + * @type {string} + */ + get path() { + return this._path; + }, + /** * |true| if this file is a directory, |false| otherwise */ get isDir() { return this._isDir; }, /** * |true| if this file is a symbolic link, |false| otherwise */ @@ -336,26 +351,26 @@ exports.Type = Type; * Native paths * * Under Windows, expressed as wide strings */ Type.path = Type.wstring.withName("[in] path"); Type.out_path = Type.out_wstring.withName("[out] path"); // Special constructors that need to be defined on all threads -OSError.closed = function closed(operation) { - return new OSError(operation, Const.ERROR_INVALID_HANDLE); +OSError.closed = function closed(operation, path) { + return new OSError(operation, Const.ERROR_INVALID_HANDLE, path); }; -OSError.exists = function exists(operation) { - return new OSError(operation, Const.ERROR_FILE_EXISTS); +OSError.exists = function exists(operation, path) { + return new OSError(operation, Const.ERROR_FILE_EXISTS, path); }; -OSError.noSuchFile = function noSuchFile(operation) { - return new OSError(operation, Const.ERROR_FILE_NOT_FOUND); +OSError.noSuchFile = function noSuchFile(operation, path) { + return new OSError(operation, Const.ERROR_FILE_NOT_FOUND, path); }; let EXPORTED_SYMBOLS = [ "declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry",
--- a/toolkit/components/osfile/modules/osfile_win_front.jsm +++ b/toolkit/components/osfile/modules/osfile_win_front.jsm @@ -53,20 +53,21 @@ /** * Representation of a file. * * You generally do not need to call this constructor yourself. Rather, * to open a file, use function |OS.File.open|. * * @param fd A OS-specific file descriptor. + * @param {string} path File path of the file handle, used for error-reporting. * @constructor */ - let File = function File(fd) { - exports.OS.Shared.AbstractFile.call(this, fd); + let File = function File(fd, path) { + exports.OS.Shared.AbstractFile.call(this, fd, path); this._closeResult = null; }; File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype); /** * Close the file. * * This method has no effect if the file is already closed. However, @@ -83,17 +84,17 @@ // Call |close(fd)|, detach finalizer if any // (|fd| may not be a CDataFinalizer if it has been // instantiated from a controller thread). let result = WinFile._CloseHandle(fd); if (typeof fd == "object" && "forget" in fd) { fd.forget(); } if (result == -1) { - this._closeResult = new File.Error("close"); + this._closeResult = new File.Error("close", ctypes.winLastError, this._path); } } if (this._closeResult) { throw this._closeResult; } return; }; @@ -110,17 +111,18 @@ * * @return {number} The number of bytes effectively read. If zero, * the end of the file has been reached. * @throws {OS.File.Error} In case of I/O error. */ File.prototype._read = function _read(buffer, nbytes, options) { // |gBytesReadPtr| is a pointer to |gBytesRead|. throw_on_zero("read", - WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null) + WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null), + this._path ); return gBytesRead.value; }; /** * Write some bytes to a file. * * @param {C pointer} buffer A buffer holding the data that must be @@ -136,17 +138,18 @@ File.prototype._write = function _write(buffer, nbytes, options) { if (this._appendMode) { // Need to manually seek on Windows, as O_APPEND is not supported. // This is, of course, a race, but there is no real way around this. this.setPosition(0, File.POS_END); } // |gBytesWrittenPtr| is a pointer to |gBytesWritten|. throw_on_zero("write", - WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null) + WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null), + this._path ); return gBytesWritten.value; }; /** * Return the current position in the file. */ File.prototype.getPosition = function getPosition(pos) { @@ -169,28 +172,30 @@ * * @return The new position in the file. */ File.prototype.setPosition = function setPosition(pos, whence) { if (whence === undefined) { whence = Const.FILE_BEGIN; } return throw_on_negative("setPosition", - WinFile.SetFilePointer(this.fd, pos, null, whence)); + WinFile.SetFilePointer(this.fd, pos, null, whence), + this._path); }; /** * Fetch the information on the file. * * @return File.Info The information on |this| file. */ File.prototype.stat = function stat() { throw_on_zero("stat", - WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr)); - return new File.Info(gFileInfo); + WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr), + this._path); + return new File.Info(gFileInfo, this._path); }; /** * Set the last access and modification date of the file. * The time stamp resolution is 1 second at best, but might be worse * depending on the platform. * * @param {Date,number=} accessDate The last access date. If numeric, @@ -199,37 +204,39 @@ * @param {Date,number=} modificationDate The last modification date. If * numeric, milliseconds since epoch. If omitted or null, then the current * date will be used. * * @throws {TypeError} In case of invalid parameters. * @throws {OS.File.Error} In case of I/O error. */ File.prototype.setDates = function setDates(accessDate, modificationDate) { - accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate); + accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate, this._path); modificationDate = Date_to_FILETIME("File.prototype.setDates", - modificationDate); + modificationDate, + this._path); throw_on_zero("setDates", WinFile.SetFileTime(this.fd, null, accessDate.address(), - modificationDate.address())); + modificationDate.address()), + this._path); }; /** * Flushes the file's buffers and causes all buffered data * to be written. * Disk flushes are very expensive and therefore should be used carefully, * sparingly and only in scenarios where it is vital that data survives * system crashes. Even though the function will be executed off the * main-thread, it might still affect the overall performance of any * running application. * * @throws {OS.File.Error} In case of I/O error. */ File.prototype.flush = function flush() { - throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd)); + throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd), this._path); }; // The default sharing mode for opening files: files are not // locked against being reopened for reading/writing or against // being deleted by the same process or another process. // This is consistent with the default Unix policy. const DEFAULT_SHARE = Const.FILE_SHARE_READ | Const.FILE_SHARE_WRITE | Const.FILE_SHARE_DELETE; @@ -335,27 +342,28 @@ } else if (mode.existing) { disposition = Const.OPEN_EXISTING; } else { disposition = Const.OPEN_ALWAYS; } } let file = error_or_file(WinFile.CreateFile(path, - access, share, security, disposition, flags, template)); + access, share, security, disposition, flags, template), path); file._appendMode = !!mode.append; if (!(mode.trunc && mode.existing)) { return file; } // Now, perform manual truncation file.setPosition(0, File.POS_START); throw_on_zero("open", - WinFile.SetEndOfFile(file.fd)); + WinFile.SetEndOfFile(file.fd), + path); return file; }; /** * Checks if a file or directory exists * * @param {string} path The path to the file. * @@ -397,17 +405,17 @@ let newAttributes = attributes & ~Const.FILE_ATTRIBUTE_READONLY; if (WinFile.SetFileAttributes(path, newAttributes) && WinFile.DeleteFile(path)) { return; } } } - throw new File.Error("remove"); + throw new File.Error("remove", ctypes.winLastError, path); }; /** * Remove an empty directory. * * @param {string} path The name of the directory to remove. * @param {*=} options Additional options. * - {bool} ignoreAbsent If |false|, throw an error if the directory @@ -415,17 +423,17 @@ */ File.removeEmptyDir = function removeEmptyDir(path, options = {}) { let result = WinFile.RemoveDirectory(path); if (!result) { if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) { return; } - throw new File.Error("removeEmptyDir"); + throw new File.Error("removeEmptyDir", ctypes.winLastError, path); } }; /** * Create a directory. * * @param {string} path The name of the directory. * @param {*=} options Additional options. This @@ -442,17 +450,17 @@ let security = options.winSecurity || null; let result = WinFile.CreateDirectory(path, security); if (result) { return; } if (("ignoreExisting" in options) && !options.ignoreExisting) { - throw new File.Error("makeDir"); + throw new File.Error("makeDir", ctypes.winLastError, path); } if (ctypes.winLastError == Const.ERROR_ALREADY_EXISTS) { return; } // If the user has no access, but it's a root directory, no error should be thrown let splitPath = OS.Path.split(path); @@ -464,17 +472,17 @@ } // One component consisting of a drive letter implies a directory root. if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED && splitPath.winDrive && splitPath.components.length === 1 ) { return; } - throw new File.Error("makeDir"); + throw new File.Error("makeDir", ctypes.winLastError, path); }; /** * Copy a file to a destination. * * @param {string} sourcePath The platform-specific path at which * the file may currently be found. * @param {string} destPath The platform-specific path at which the @@ -492,17 +500,18 @@ * behavior is undefined and may not be the same across all platforms. * * General note: The behavior of this function with respect to metadata * is unspecified. Metadata may or may not be copied with the file. The * behavior may not be the same across all platforms. */ File.copy = function copy(sourcePath, destPath, options = {}) { throw_on_zero("copy", - WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false) + WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false), + sourcePath ); }; /** * Move a file to a destination. * * @param {string} sourcePath The platform-specific path at which * the file may currently be found. @@ -531,17 +540,18 @@ let flags = 0; if (!options.noCopy) { flags = Const.MOVEFILE_COPY_ALLOWED; } if (!options.noOverwrite) { flags = flags | Const.MOVEFILE_REPLACE_EXISTING; } throw_on_zero("move", - WinFile.MoveFileEx(sourcePath, destPath, flags) + WinFile.MoveFileEx(sourcePath, destPath, flags), + sourcePath ); // Inherit NTFS permissions from the destination directory // if possible. if (Path.dirname(sourcePath) === Path.dirname(destPath)) { // Skip if the move operation was the simple rename, return; } @@ -595,23 +605,24 @@ * A global value used to receive data during time conversions. */ let gSystemTime = new Type.SystemTime.implementation(); let gSystemTimePtr = gSystemTime.address(); /** * Utility function: convert a FILETIME to a JavaScript Date. */ - let FILETIME_to_Date = function FILETIME_to_Date(fileTime) { + let FILETIME_to_Date = function FILETIME_to_Date(fileTime, path) { if (fileTime == null) { throw new TypeError("Expecting a non-null filetime"); } throw_on_zero("FILETIME_to_Date", WinFile.FileTimeToSystemTime(fileTime.address(), - gSystemTimePtr)); + gSystemTimePtr), + path); // Windows counts hours, minutes, seconds from UTC, // JS counts from local time, so we need to go through UTC. let utc = Date.UTC(gSystemTime.wYear, gSystemTime.wMonth - 1 /*Windows counts months from 1, JS from 0*/, gSystemTime.wDay, gSystemTime.wHour, gSystemTime.wMinute, gSystemTime.wSecond, gSystemTime.wMilliSeconds); @@ -621,17 +632,17 @@ /** * Utility function: convert Javascript Date to FileTime. * * @param {string} fn Name of the calling function. * @param {Date,number} date The date to be converted. If omitted or null, * then the current date will be used. If numeric, assumed to be the date * in milliseconds since epoch. */ - let Date_to_FILETIME = function Date_to_FILETIME(fn, date) { + let Date_to_FILETIME = function Date_to_FILETIME(fn, date, path) { if (typeof date === "number") { date = new Date(date); } else if (!date) { date = new Date(); } else if (typeof date.getUTCFullYear !== "function") { throw new TypeError("|date| parameter of " + fn + " must be a " + "|Date| instance or number"); } @@ -641,17 +652,18 @@ gSystemTime.wDay = date.getUTCDate(); gSystemTime.wHour = date.getUTCHours(); gSystemTime.wMinute = date.getUTCMinutes(); gSystemTime.wSecond = date.getUTCSeconds(); gSystemTime.wMilliseconds = date.getUTCMilliseconds(); let result = new OS.Shared.Type.FILETIME.implementation(); throw_on_zero("Date_to_FILETIME", WinFile.SystemTimeToFileTime(gSystemTimePtr, - result.address())); + result.address()), + path); return result; }; /** * Iterate on one directory. * * This iterator will not enter subdirectories. * @@ -688,17 +700,17 @@ this._closed = true; this._exists = true; } else if (error == Const.ERROR_PATH_NOT_FOUND) { // Directory does not exist, let's throw if we attempt to walk it SharedAll.LOG("Directory does not exist"); this._closed = true; this._exists = false; } else { - throw new File.Error("DirectoryIterator", error); + throw new File.Error("DirectoryIterator", error, this._path); } } else { this._closed = false; this._exists = true; } }; File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype); @@ -707,17 +719,17 @@ /** * Fetch the next entry in the directory. * * @return null If we have reached the end of the directory. */ File.DirectoryIterator.prototype._next = function _next() { // Bailout if the directory does not exist if (!this._exists) { - throw File.Error.noSuchFile("DirectoryIterator.prototype.next"); + throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path); } // Bailout if the iterator is closed. if (this._closed) { return null; } // If this is the first entry, we have obtained it already // during construction. if (this._first) { @@ -728,17 +740,17 @@ if (WinFile.FindNextFile(this._handle, this._findDataPtr)) { return this._findData; } else { let error = ctypes.winLastError; this.close(); if (error == Const.ERROR_NO_MORE_FILES) { return null; } else { - throw new File.Error("iter (FindNextFile)", error); + throw new File.Error("iter (FindNextFile)", error, this._path); } } }, /** * Return the next entry in the directory, if any such entry is * available. * @@ -766,17 +778,18 @@ if (this._closed) { return; } this._closed = true; if (this._handle) { // We might not have a handle if the iterator is closed // before being used. throw_on_zero("FindClose", - WinFile.FindClose(this._handle)); + WinFile.FindClose(this._handle), + this._path); this._handle = null; } }; /** * Determine whether the directory exists. * * @return {boolean} @@ -790,19 +803,19 @@ !win_entry.ftLastAccessTime || !win_entry.ftLastWriteTime) throw new TypeError(); // Copy the relevant part of |win_entry| to ensure that // our data is not overwritten prematurely. let isDir = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY); let isSymLink = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT); - let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime); - let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime); - let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime); + let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime, this._path); + let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime, this._path); + let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime, this._path); let name = win_entry.cFileName.readString(); if (!name) { throw new TypeError("Empty name"); } if (!parent) { throw new TypeError("Empty parent"); @@ -842,30 +855,29 @@ * Information on a file. * * To obtain the latest information on a file, use |File.stat| * (for an unopened file) or |File.prototype.stat| (for an * already opened file). * * @constructor */ - File.Info = function Info(stat) { + File.Info = function Info(stat, path) { let isDir = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY); let isSymLink = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT); - - let winBirthDate = FILETIME_to_Date(stat.ftCreationTime); - let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime); - let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime); + + let winBirthDate = FILETIME_to_Date(stat.ftCreationTime, this._path); + let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime, this._path); + let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime, this._path); let value = ctypes.UInt64.join(stat.nFileSizeHigh, stat.nFileSizeLow); let size = Type.uint64_t.importFromC(value); - SysAll.AbstractInfo.call(this, isDir, isSymLink, size, - winBirthDate, lastAccessDate, - lastWriteDate); + SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size, + winBirthDate, lastAccessDate, lastWriteDate); }; File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype); /** * Return a version of an instance of File.Info that can be sent * from a worker thread to the main thread. Note that deserialization * is asymmetric and returns an object with a different implementation. */ @@ -999,54 +1011,96 @@ } }; /** * Set the current directory by setCurrentDirectory. */ File.setCurrentDirectory = function setCurrentDirectory(path) { throw_on_zero("setCurrentDirectory", - WinFile.SetCurrentDirectory(path)); + WinFile.SetCurrentDirectory(path), + path); }; /** * Get/set the current directory by |curDir|. */ Object.defineProperty(File, "curDir", { set: function(path) { this.setCurrentDirectory(path); }, get: function() { return this.getCurrentDirectory(); } } ); // Utility functions, used for error-handling - function error_or_file(maybe) { + + /** + * Turn the result of |open| into an Error or a File + * @param {number} maybe The result of the |open| operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.winLastError, otherwise it returns the opened file. + * @param {string=} path The path of the file. + */ + function error_or_file(maybe, path) { if (maybe == Const.INVALID_HANDLE_VALUE) { - throw new File.Error("open"); + throw new File.Error("open", ctypes.winLastError, path); } - return new File(maybe); + return new File(maybe, path); } - function throw_on_zero(operation, result) { + + /** + * Utility function to sort errors represented as "0" from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {number} result The result of the operation that may + * represent either an error or a success. If 0, this function raises + * an error holding ctypes.winLastError, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_zero(operation, result, path) { if (result == 0) { - throw new File.Error(operation); + throw new File.Error(operation, ctypes.winLastError, path); } return result; } - function throw_on_negative(operation, result) { + + /** + * Utility function to sort errors represented as "-1" from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {number} result The result of the operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.winLastError, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_negative(operation, result, path) { if (result < 0) { - throw new File.Error(operation); + throw new File.Error(operation, ctypes.winLastError, path); } return result; } - function throw_on_null(operation, result) { + + /** + * Utility function to sort errors represented as |null| from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {pointer} result The result of the operation that may + * represent either an error or a success. If |null|, this function raises + * an error holding ctypes.winLastError, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_null(operation, result, path) { if (result == null || (result.isNull && result.isNull())) { - throw new File.Error(operation); + throw new File.Error(operation, ctypes.winLastError, path); } return result; } File.Win = exports.OS.Win.File; File.Error = SysAll.Error; exports.OS.File = File; exports.OS.Shared.Type = Type;
new file mode 100644 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {}); +Components.utils.import("resource://gre/modules/Task.jsm"); + +function run_test() { + run_next_test(); +} + +add_task(function* testFileError_with_writeAtomic() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let path = Path.join(Constants.Path.tmpDir, + "testFileError.tmp"); + yield File.remove(path); + yield File.writeAtomic(path, DEFAULT_CONTENTS); + let exception; + try { + yield File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + do_check_true(exception instanceof File.Error); + do_check_true(exception.path == path); +}); + +add_task(function* testFileError_with_makeDir() { + let path = Path.join(Constants.Path.tmpDir, + "directory"); + yield File.removeDir(path); + yield File.makeDir(path); + let exception; + try { + yield File.makeDir(path, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + do_check_true(exception instanceof File.Error); + do_check_true(exception.path == path); +}); + +add_task(function* testFileError_with_move() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let sourcePath = Path.join(Constants.Path.tmpDir, + "src.tmp"); + let destPath = Path.join(Constants.Path.tmpDir, + "dest.tmp"); + yield File.remove(sourcePath); + yield File.remove(destPath); + yield File.writeAtomic(sourcePath, DEFAULT_CONTENTS); + yield File.writeAtomic(destPath, DEFAULT_CONTENTS); + let exception; + try { + yield File.move(sourcePath, destPath, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + do_print(exception); + do_check_true(exception instanceof File.Error); + do_check_true(exception.path == sourcePath); +});
--- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -22,8 +22,9 @@ tail = [test_reset.js] [test_shutdown.js] [test_unique.js] [test_open.js] [test_telemetry.js] [test_duration.js] [test_compression.js] [test_osfile_writeAtomic_backupTo_option.js] +[test_osfile_error.js]
--- a/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js +++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js @@ -1,11 +1,13 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ +// Test that AsyncShutdown report errors correctly + function setup_crash() { Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this); Components.utils.import("resource://gre/modules/Services.jsm", this); Components.utils.import("resource://gre/modules/Promise.jsm", this); Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10); @@ -24,11 +26,69 @@ function setup_crash() { function after_crash(mdump, extra) { do_print("after crash: " + extra.AsyncShutdownTimeout); let info = JSON.parse(extra.AsyncShutdownTimeout); do_check_eq(info.phase, "testing-async-shutdown-crash"); do_print("Condition: " + JSON.stringify(info.conditions)); do_check_true(JSON.stringify(info.conditions).indexOf("A blocker that is never satisfied") != -1); } +// Test that AsyncShutdown + OS.File reports errors correctly, in a case in which +// the latest operation succeeded + +function setup_osfile_crash_noerror() { + Components.utils.import("resource://gre/modules/Services.jsm", this); + Components.utils.import("resource://gre/modules/osfile.jsm", this); + + Services.prefs.setBoolPref("toolkit.osfile.debug.failshutdown", true); + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1); + + OS.File.getCurrentDirectory(); + Services.obs.notifyObservers(null, "profile-before-change", null); + dump("Waiting for crash\n"); +}; + +function after_osfile_crash_noerror(mdump, extra) { + do_print("after OS.File crash: " + JSON.stringify(extra.AsyncShutdownTimeout)); + let info = JSON.parse(extra.AsyncShutdownTimeout); + let state = info.conditions[0].state; + do_print("Keys: " + Object.keys(state).join(", ")); + do_check_eq(info.phase, "profile-before-change"); + do_check_true(state.launched); + do_check_false(state.shutdown); + do_check_true(state.worker); + do_check_true(!!state.latestSent); + do_check_eq(state.latestSent[1], "getCurrentDirectory"); +} + +// Test that AsyncShutdown + OS.File reports errors correctly, in a case in which +// the latest operation failed + +function setup_osfile_crash_exn() { + Components.utils.import("resource://gre/modules/Services.jsm", this); + Components.utils.import("resource://gre/modules/osfile.jsm", this); + + Services.prefs.setBoolPref("toolkit.osfile.debug.failshutdown", true); + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1); + + OS.File.read("I do not exist"); + Services.obs.notifyObservers(null, "profile-before-change", null); + dump("Waiting for crash\n"); +}; + +function after_osfile_crash_exn(mdump, extra) { + do_print("after OS.File crash: " + JSON.stringify(extra.AsyncShutdownTimeout)); + let info = JSON.parse(extra.AsyncShutdownTimeout); + let state = info.conditions[0].state; + do_print("Keys: " + Object.keys(state).join(", ")); + do_check_eq(info.phase, "profile-before-change"); + do_check_true(state.launched); + do_check_false(state.shutdown); + do_check_true(state.worker); + do_check_true(!!state.latestSent); + do_check_eq(state.latestSent[1], "read"); +} + function run_test() { do_crash(setup_crash, after_crash); + do_crash(setup_osfile_crash_noerror, after_osfile_crash_noerror); + do_crash(setup_osfile_crash_exn, after_osfile_crash_exn); }
--- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -676,22 +676,17 @@ var NodeListActor = exports.NodeListActo length: this.nodeList.length } }, /** * Get a single node from the node list. */ item: method(function(index) { - let node = this.walker._ref(this.nodeList[index]); - let newParents = [node for (node of this.walker.ensurePathToRoot(node))]; - return { - node: node, - newParents: newParents - } + return this.walker.attachElement(this.nodeList[index]); }, { request: { item: Arg(0) }, response: RetVal("disconnectedNode") }), /** * Get a range of the items from the node list. */
--- a/toolkit/devtools/webconsole/utils.js +++ b/toolkit/devtools/webconsole/utils.js @@ -1038,17 +1038,22 @@ let DebuggerEnvironmentSupport = { getProperties: function(aObj) { return aObj.names(); }, getProperty: function(aObj, aName) { // TODO: we should use getVariableDescriptor() here - bug 725815. - let result = aObj.getVariable(aName); + let result = undefined; + try { + result = aObj.getVariable(aName); + } catch (ex) { + // getVariable() throws for invalid identifiers. + } return result === undefined ? null : { value: result }; }, }; exports.JSPropertyProvider = JSPropertyProvider; })(WebConsoleUtils);
--- a/toolkit/modules/Log.jsm +++ b/toolkit/modules/Log.jsm @@ -452,35 +452,35 @@ Appender.prototype = { }; /* * DumpAppender * Logs to standard out */ function DumpAppender(formatter) { + Appender.call(this, formatter); this._name = "DumpAppender"; - Appender.call(this, formatter); } DumpAppender.prototype = { __proto__: Appender.prototype, doAppend: function DApp_doAppend(message) { dump(message); } }; /* * ConsoleAppender * Logs to the javascript console */ function ConsoleAppender(formatter) { + Appender.call(this, formatter); this._name = "ConsoleAppender"; - Appender.call(this, formatter); } ConsoleAppender.prototype = { __proto__: Appender.prototype, doAppend: function CApp_doAppend(message) { if (message.level > Log.Level.Warn) { Cu.reportError(message); return; @@ -494,18 +494,18 @@ ConsoleAppender.prototype = { * Append to an nsIStorageStream * * This writes logging output to an in-memory stream which can later be read * back as an nsIInputStream. It can be used to avoid expensive I/O operations * during logging. Instead, one can periodically consume the input stream and * e.g. write it to disk asynchronously. */ function StorageStreamAppender(formatter) { + Appender.call(this, formatter); this._name = "StorageStreamAppender"; - Appender.call(this, formatter); } StorageStreamAppender.prototype = { __proto__: Appender.prototype, _converterStream: null, // holds the nsIConverterOutputStream _outputStream: null, // holds the underlying nsIOutputStream @@ -576,25 +576,25 @@ StorageStreamAppender.prototype = { }; /** * File appender * * Writes output to file using OS.File. */ function FileAppender(path, formatter) { + Appender.call(this, formatter); this._name = "FileAppender"; this._encoder = new TextEncoder(); this._path = path; this._file = null; this._fileReadyPromise = null; // This is a promise exposed for testing/debugging the logger itself. this._lastWritePromise = null; - Appender.call(this, formatter); } FileAppender.prototype = { __proto__: Appender.prototype, _openFile: function () { return Task.spawn(function _openFile() { try { @@ -649,21 +649,21 @@ FileAppender.prototype = { /** * Bounded File appender * * Writes output to file using OS.File. After the total message size * (as defined by message.length) exceeds maxSize, existing messages * will be discarded, and subsequent writes will be appended to a new log file. */ function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) { + FileAppender.call(this, path, formatter); this._name = "BoundedFileAppender"; this._size = 0; this._maxSize = maxSize; this._closeFilePromise = null; - FileAppender.call(this, path, formatter); } BoundedFileAppender.prototype = { __proto__: FileAppender.prototype, doAppend: function (message) { if (!this._removeFilePromise) { if (this._size < this._maxSize) {
--- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -375,21 +375,26 @@ ROBOCOP_PATH = $(abspath $(_ABS_DIST)/.. # is used in a series of commands that run under a "cd something", while # $(NSINSTALL) is relative. INNER_ROBOCOP_PACKAGE= \ cp $(GECKO_APP_AP_PATH)/fennec_ids.txt $(_ABS_DIST) && \ $(call RELEASE_SIGN_ANDROID_APK,$(ROBOCOP_PATH)/robocop-debug-unsigned-unaligned.apk,$(_ABS_DIST)/robocop.apk) BACKGROUND_TESTS_PATH = $(abspath $(_ABS_DIST)/../mobile/android/tests/background/junit3) INNER_BACKGROUND_TESTS_PACKAGE= \ - $(call RELEASE_SIGN_ANDROID_APK,$(BACKGROUND_TESTS_PATH)/background-debug-unsigned-unaligned.apk,$(_ABS_DIST)/background.apk) + $(call RELEASE_SIGN_ANDROID_APK,$(BACKGROUND_TESTS_PATH)/background-junit3-debug-unsigned-unaligned.apk,$(_ABS_DIST)/background-junit3.apk) + +BROWSER_TESTS_PATH = $(abspath $(_ABS_DIST)/../mobile/android/tests/browser/junit3) +INNER_BROWSER_TESTS_PACKAGE= \ + $(call RELEASE_SIGN_ANDROID_APK,$(BROWSER_TESTS_PATH)/browser-junit3-debug-unsigned-unaligned.apk,$(_ABS_DIST)/browser-junit3.apk) endif else INNER_ROBOCOP_PACKAGE=echo 'Testing is disabled - No Android Robocop for you' -INNER_BACKGROUND_TESTS_PACKAGE=echo 'Testing is disabled - No Android Background tests for you' +INNER_BACKGROUND_TESTS_PACKAGE=echo 'Testing is disabled - No Android Background JUnit 3 tests for you' +INNER_BROWSER_TESTS_PACKAGE=echo 'Testing is disabled - No Android Browser JUnit 3tests for you' endif # Create geckoview_library/geckoview_{assets,library}.zip for third-party GeckoView consumers. ifdef NIGHTLY_BUILD ifndef MOZ_DISABLE_GECKOVIEW INNER_MAKE_GECKOVIEW_LIBRARY= \ $(MAKE) -C ../mobile/android/geckoview_library package else @@ -472,16 +477,17 @@ INNER_MAKE_PACKAGE = \ rm -f $(_ABS_DIST)/gecko.apk && \ cp $(_ABS_DIST)/gecko.ap_ $(_ABS_DIST)/gecko.apk && \ $(ZIP) -j0 $(_ABS_DIST)/gecko.apk $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/classes.dex && \ cp $(_ABS_DIST)/gecko.apk $(_ABS_DIST)/gecko-unsigned-unaligned.apk && \ $(RELEASE_JARSIGNER) $(_ABS_DIST)/gecko.apk && \ $(ZIPALIGN) -f -v 4 $(_ABS_DIST)/gecko.apk $(PACKAGE) && \ $(INNER_ROBOCOP_PACKAGE) && \ $(INNER_BACKGROUND_TESTS_PACKAGE) && \ + $(INNER_BROWSER_TESTS_PACKAGE) && \ $(INNER_MAKE_GECKOVIEW_LIBRARY) # Language repacks root the resources contained in assets/omni.ja # under assets/, but the repacks expect them to be rooted at /. # Therefore, we we move the omnijar back to / so the resources are # under the root here, in INNER_UNMAKE_PACKAGE. See comments about # OMNIJAR_NAME earlier in this file and in configure.in.
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -121,19 +121,16 @@ #ifdef NECKO_PROTOCOL_rtsp #include "nsIScriptSecurityManager.h" #include "nsIMessageManager.h" #endif using namespace mozilla; using namespace mozilla::ipc; -// Buffer file writes in 32kb chunks -#define BUFFERED_OUTPUT_SIZE (1024 * 32) - // Download Folder location constants #define NS_PREF_DOWNLOAD_DIR "browser.download.dir" #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList" enum { NS_FOLDER_VALUE_DESKTOP = 0 , NS_FOLDER_VALUE_DOWNLOADS = 1 , NS_FOLDER_VALUE_CUSTOM = 2 };