author | Ryan VanderMeulen <ryanvm@gmail.com> |
Mon, 27 Jul 2015 10:39:37 -0400 | |
changeset 254724 | 21ca97268bae2d746e09ad6c612f4fbf3df0fe6e |
parent 254690 | 09fbdc0559f5984fb0d315b8ff0eb77e7ee3cfef (current diff) |
parent 254723 | 4791b25ea75bfe1ac5259ead3036a03d75824635 (diff) |
child 254734 | d576f2cec511a21fef54b61c54b69d5e5d5fc992 |
child 254754 | c2e8b03c28bcd3ed17fa46ae5041841204028450 |
child 254780 | e039b166890f312c61b58e6ecc205ba6ad584264 |
push id | 29110 |
push user | ryanvm@gmail.com |
push date | Mon, 27 Jul 2015 14:39:38 +0000 |
treeherder | mozilla-central@21ca97268bae [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 42.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/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -706,16 +706,17 @@ ; Modules @RESPATH@/modules/* ; Safe Browsing @RESPATH@/components/nsURLClassifier.manifest @RESPATH@/components/nsUrlClassifierHashCompleter.js @RESPATH@/components/nsUrlClassifierListManager.js @RESPATH@/components/nsUrlClassifierLib.js +@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js @RESPATH@/components/url-classifier.xpt ; GNOME hooks #ifdef MOZ_ENABLE_GNOME_COMPONENT @RESPATH@/components/@DLL_PREFIX@mozgnome@DLL_SUFFIX@ #endif ; ANGLE on Win32
--- a/browser/base/content/aboutaccounts/aboutaccounts.js +++ b/browser/base/content/aboutaccounts/aboutaccounts.js @@ -143,18 +143,23 @@ let wrapper = { * * @param accountData the user's account data and credentials */ onLogin: function (accountData) { log("Received: 'login'. Data:" + JSON.stringify(accountData)); if (accountData.customizeSync) { Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); - delete accountData.customizeSync; } + delete accountData.customizeSync; + // sessionTokenContext is erroneously sent by the content server. + // https://github.com/mozilla/fxa-content-server/issues/2766 + // To avoid having the FxA storage manager not knowing what to do with + // it we delete it here. + delete accountData.sessionTokenContext; // We need to confirm a relink - see shouldAllowRelink for more let newAccountEmail = accountData.email; // The hosted code may have already checked for the relink situation // by sending the can_link_account command. If it did, then // it will indicate we don't need to ask twice. if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) { // we need to tell the page we successfully received the message, but
--- a/browser/base/content/browser-trackingprotection.js +++ b/browser/base/content/browser-trackingprotection.js @@ -111,35 +111,42 @@ let TrackingProtection = { // Any scheme turned into https is correct. let normalizedUrl = Services.io.newURI( "https://" + gBrowser.selectedBrowser.currentURI.hostPort, null, null); // Add the current host in the 'trackingprotection' consumer of // the permission manager using a normalized URI. This effectively // places this host on the tracking protection allowlist. - Services.perms.add(normalizedUrl, - "trackingprotection", Services.perms.ALLOW_ACTION); + if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) { + PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl); + } else { + Services.perms.add(normalizedUrl, + "trackingprotection", Services.perms.ALLOW_ACTION); + } // Telemetry for disable protection. this.eventsHistogram.add(1); BrowserReload(); }, enableForCurrentPage() { // Remove the current host from the 'trackingprotection' consumer // of the permission manager. This effectively removes this host // from the tracking protection allowlist. let normalizedUrl = Services.io.newURI( "https://" + gBrowser.selectedBrowser.currentURI.hostPort, null, null); - Services.perms.remove(normalizedUrl, - "trackingprotection"); + if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) { + PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl); + } else { + Services.perms.remove(normalizedUrl, "trackingprotection"); + } // Telemetry for enable protection. this.eventsHistogram.add(2); BrowserReload(); }, showIntroPanel: Task.async(function*() {
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -426,16 +426,20 @@ support-files = benignPage.html [browser_trackingUI_3.js] tags = trackingprotection [browser_trackingUI_4.js] tags = trackingprotection support-files = trackingPage.html benignPage.html +[browser_trackingUI_5.js] +tags = trackingprotection +support-files = + trackingPage.html [browser_typeAheadFind.js] skip-if = buildapp == 'mulet' [browser_unknownContentType_title.js] [browser_unloaddialogs.js] skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't [browser_urlHighlight.js] [browser_urlbarAutoFillTrimURLs.js] [browser_urlbarCopying.js]
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/general/browser_trackingUI_5.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that sites added to the Tracking Protection whitelist in private +// browsing mode don't persist once the private browsing window closes. + +const PB_PREF = "privacy.trackingprotection.pbmode.enabled"; +const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html"; +let TrackingProtection = null; +let browser = null; +let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {}); + +registerCleanupFunction(function() { + TrackingProtection = browser = null; + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +function hidden(sel) { + let win = browser.ownerGlobal; + let el = win.document.querySelector(sel); + let display = win.getComputedStyle(el).getPropertyValue("display", null); + return display === "none"; +} + +function clickButton(sel) { + let win = browser.ownerGlobal; + let el = win.document.querySelector(sel); + el.doCommand(); +} + +function testTrackingPage(window) { + info("Tracking content must be blocked"); + ok(!TrackingProtection.container.hidden, "The container is visible"); + is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content", + 'content: state="blocked-tracking-content"'); + is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content", + 'icon: state="blocked-tracking-content"'); + + ok(!hidden("#tracking-protection-icon"), "icon is visible"); + ok(hidden("#tracking-action-block"), "blockButton is hidden"); + + ok(hidden("#tracking-action-unblock"), "unblockButton is hidden"); + ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible"); + + // Make sure that the blocked tracking elements message appears + ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden"); + ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden"); + ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible"); +} + +function testTrackingPageUnblocked() { + info("Tracking content must be white-listed and not blocked"); + ok(!TrackingProtection.container.hidden, "The container is visible"); + is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content", + 'content: state="loaded-tracking-content"'); + is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content", + 'icon: state="loaded-tracking-content"'); + + ok(!hidden("#tracking-protection-icon"), "icon is visible"); + ok(!hidden("#tracking-action-block"), "blockButton is visible"); + ok(hidden("#tracking-action-unblock"), "unblockButton is hidden"); + + // Make sure that the blocked tracking elements message appears + ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden"); + ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible"); + ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden"); +} + +add_task(function* testExceptionAddition() { + yield UrlClassifierTestUtils.addTestTrackers(); + let privateWin = yield promiseOpenAndLoadWindow({private: true}, true); + browser = privateWin.gBrowser; + let tab = browser.selectedTab = browser.addTab(); + + TrackingProtection = browser.ownerGlobal.TrackingProtection; + yield pushPrefs([PB_PREF, true]); + + ok(TrackingProtection.enabled, "TP is enabled after setting the pref"); + + info("Load a test page containing tracking elements"); + yield promiseTabLoadEvent(tab, TRACKING_PAGE); + + testTrackingPage(tab.ownerDocument.defaultView); + + info("Disable TP for the page (which reloads the page)"); + let tabReloadPromise = promiseTabLoadEvent(tab); + clickButton("#tracking-action-unblock"); + yield tabReloadPromise; + testTrackingPageUnblocked(); + + info("Test that the exception is remembered across tabs in the same private window"); + tab = browser.selectedTab = browser.addTab(); + + info("Load a test page containing tracking elements"); + yield promiseTabLoadEvent(tab, TRACKING_PAGE); + testTrackingPageUnblocked(); + + yield promiseWindowClosed(privateWin); +}); + +add_task(function* testExceptionPersistence() { + info("Open another private browsing window"); + let privateWin = yield promiseOpenAndLoadWindow({private: true}, true); + browser = privateWin.gBrowser; + let tab = browser.selectedTab = browser.addTab(); + + TrackingProtection = browser.ownerGlobal.TrackingProtection; + ok(TrackingProtection.enabled, "TP is still enabled"); + + info("Load a test page containing tracking elements"); + yield promiseTabLoadEvent(tab, TRACKING_PAGE); + + testTrackingPage(tab.ownerDocument.defaultView); + + info("Disable TP for the page (which reloads the page)"); + let tabReloadPromise = promiseTabLoadEvent(tab); + clickButton("#tracking-action-unblock"); + yield tabReloadPromise; + testTrackingPageUnblocked(); + + privateWin.close(); +});
--- a/browser/components/customizableui/CustomizableWidgets.jsm +++ b/browser/components/customizableui/CustomizableWidgets.jsm @@ -952,33 +952,40 @@ const CustomizableWidgets = [ let win = aEvent.view; win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser) } }, { id: "loop-button", type: "custom", label: "loop-call-button3.label", tooltiptext: "loop-call-button3.tooltiptext", + privateBrowsingTooltiptext: "loop-call-button3-pb.tooltiptext", defaultArea: CustomizableUI.AREA_NAVBAR, - // Not in private browsing, see bug 1108187. - showInPrivateBrowsing: false, introducedInVersion: 4, onBuild: function(aDocument) { // If we're not supposed to see the button, return zip. if (!Services.prefs.getBoolPref("loop.enabled")) { return null; } + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView); + let node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); node.setAttribute("id", this.id); node.classList.add("toolbarbutton-1"); node.classList.add("chromeclass-toolbar-additional"); node.classList.add("badged-button"); node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); - node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); + if (isWindowPrivate) + node.setAttribute("disabled", "true"); + let tooltiptext = isWindowPrivate ? + CustomizableUI.getLocalizedProperty(this, "privateBrowsingTooltiptext", + [CustomizableUI.getLocalizedProperty(this, "label")]) : + CustomizableUI.getLocalizedProperty(this, "tooltiptext"); + node.setAttribute("tooltiptext", tooltiptext); node.setAttribute("removable", "true"); node.addEventListener("command", function(event) { aDocument.defaultView.LoopUI.togglePanel(event); }); return node; } }, {
--- a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js +++ b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js @@ -114,17 +114,17 @@ function configureFxAccountIdentity() { }; let MockInternal = { newAccountState(credentials) { isnot(credentials, "not expecting credentials"); let storageManager = new MockFxaStorageManager(); // and init storage with our user. storageManager.initialize(user); - return new AccountState(this, storageManager); + return new AccountState(storageManager); }, getCertificate(data, keyPair, mustBeValidUntil) { this.cert = { validUntil: this.now() + 10000, cert: "certificate", }; return Promise.resolve(this.cert.cert); },
--- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -565,23 +565,25 @@ loop.conversationViews = (function(mozL1 ) ) ); } }); var OngoingConversationView = React.createClass({displayName: "OngoingConversationView", mixins: [ - loop.store.StoreMixin("conversationStore"), sharedMixins.MediaSetupMixin ], propTypes: { // local audio: React.PropTypes.object, + // We pass conversationStore here rather than use the mixin, to allow + // easy configurability for the ui-showcase. + conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, // The poster URLs are for UI-showcase testing and development. localPosterUrl: React.PropTypes.string, // This is used from the props rather than the state to make it easier for // the ui-showcase. mediaConnected: React.PropTypes.bool, remotePosterUrl: React.PropTypes.string, remoteVideoEnabled: React.PropTypes.bool, @@ -592,17 +594,27 @@ loop.conversationViews = (function(mozL1 getDefaultProps: function() { return { video: {enabled: true, visible: true}, audio: {enabled: true, visible: true} }; }, getInitialState: function() { - return this.getStoreState(); + return this.props.conversationStore.getStoreState(); + }, + + componentWillMount: function() { + this.props.conversationStore.on("change", function() { + this.setState(this.props.conversationStore.getStoreState()); + }, this); + }, + + componentWillUnmount: function() { + this.props.conversationStore.off("change", null, this); }, componentDidMount: function() { // The SDK needs to know about the configuration and the elements to use // for display. So the best way seems to pass the information here - ideally // the sdk wouldn't need to know this, but we can't change that. this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({ publisherConfig: this.getDefaultPublisherConfig({ @@ -628,64 +640,79 @@ loop.conversationViews = (function(mozL1 publishStream: function(type, enabled) { this.props.dispatcher.dispatch( new sharedActions.SetMute({ type: type, enabled: enabled })); }, + /** + * Should we render a visual cue to the user (e.g. a spinner) that a local + * stream is on its way from the camera? + * + * @returns {boolean} + * @private + */ + _isLocalLoading: function () { + return !this.state.localSrcVideoObject && !this.props.localPosterUrl; + }, + + /** + * Should we render a visual cue to the user (e.g. a spinner) that a remote + * stream is on its way from the other user? + * + * @returns {boolean} + * @private + */ + _isRemoteLoading: function() { + return !!(!this.state.remoteSrcVideoObject && + !this.props.remotePosterUrl && + !this.state.mediaConnected); + }, + shouldRenderRemoteVideo: function() { if (this.props.mediaConnected) { // If remote video is not enabled, we're muted, so we'll show an avatar // instead. return this.props.remoteVideoEnabled; } // We're not yet connected, but we don't want to show the avatar, and in // the common case, we'll just transition to the video. return true; }, render: function() { - var localStreamClasses = React.addons.classSet({ - local: true, - "local-stream": true, - "local-stream-audio": !this.props.video.enabled - }); - return ( - React.createElement("div", {className: "video-layout-wrapper"}, - React.createElement("div", {className: "conversation"}, - React.createElement("div", {className: "media nested"}, - React.createElement("div", {className: "video_wrapper remote_wrapper"}, - React.createElement("div", {className: "video_inner remote focus-stream"}, - React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), - isLoading: false, - mediaType: "remote", - posterUrl: this.props.remotePosterUrl, - srcVideoObject: this.state.remoteSrcVideoObject}) - ) - ), - React.createElement("div", {className: localStreamClasses}, - React.createElement(sharedViews.MediaView, {displayAvatar: !this.props.video.enabled, - isLoading: false, - mediaType: "local", - posterUrl: this.props.localPosterUrl, - srcVideoObject: this.state.localSrcVideoObject}) - ) - ), - React.createElement(loop.shared.views.ConversationToolbar, { - audio: this.props.audio, - dispatcher: this.props.dispatcher, - edit: { visible: false, enabled: false}, - hangup: this.hangup, - publishStream: this.publishStream, - video: this.props.video}) - ) + React.createElement("div", {className: "desktop-call-wrapper"}, + React.createElement(sharedViews.MediaLayoutView, { + dispatcher: this.props.dispatcher, + displayScreenShare: false, + isLocalLoading: this._isLocalLoading(), + isRemoteLoading: this._isRemoteLoading(), + isScreenShareLoading: false, + localPosterUrl: this.props.localPosterUrl, + localSrcVideoObject: this.state.localSrcVideoObject, + localVideoMuted: !this.props.video.enabled, + matchMedia: this.state.matchMedia || window.matchMedia.bind(window), + remotePosterUrl: this.props.remotePosterUrl, + remoteSrcVideoObject: this.state.remoteSrcVideoObject, + renderRemoteVideo: this.shouldRenderRemoteVideo(), + screenSharePosterUrl: null, + screenShareVideoObject: this.state.screenShareVideoObject, + showContextRoomName: false, + useDesktopPaths: true}), + React.createElement(loop.shared.views.ConversationToolbar, { + audio: this.props.audio, + dispatcher: this.props.dispatcher, + edit: { visible: false, enabled: false}, + hangup: this.hangup, + publishStream: this.publishStream, + video: this.props.video}) ) ); } }); /** * Master View Controller for outgoing calls. This manages * the different views that need displaying. @@ -773,16 +800,17 @@ loop.conversationViews = (function(mozL1 return (React.createElement(CallFailedView, { contact: this.state.contact, dispatcher: this.props.dispatcher, outgoing: this.state.outgoing})); } case CALL_STATES.ONGOING: { return (React.createElement(OngoingConversationView, { audio: {enabled: !this.state.audioMuted}, + conversationStore: this.getStore(), dispatcher: this.props.dispatcher, mediaConnected: this.state.mediaConnected, remoteSrcVideoObject: this.state.remoteSrcVideoObject, remoteVideoEnabled: this.state.remoteVideoEnabled, video: {enabled: !this.state.videoMuted}}) ); } case CALL_STATES.FINISHED: {
--- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -565,23 +565,25 @@ loop.conversationViews = (function(mozL1 </div> </div> ); } }); var OngoingConversationView = React.createClass({ mixins: [ - loop.store.StoreMixin("conversationStore"), sharedMixins.MediaSetupMixin ], propTypes: { // local audio: React.PropTypes.object, + // We pass conversationStore here rather than use the mixin, to allow + // easy configurability for the ui-showcase. + conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, // The poster URLs are for UI-showcase testing and development. localPosterUrl: React.PropTypes.string, // This is used from the props rather than the state to make it easier for // the ui-showcase. mediaConnected: React.PropTypes.bool, remotePosterUrl: React.PropTypes.string, remoteVideoEnabled: React.PropTypes.bool, @@ -592,17 +594,27 @@ loop.conversationViews = (function(mozL1 getDefaultProps: function() { return { video: {enabled: true, visible: true}, audio: {enabled: true, visible: true} }; }, getInitialState: function() { - return this.getStoreState(); + return this.props.conversationStore.getStoreState(); + }, + + componentWillMount: function() { + this.props.conversationStore.on("change", function() { + this.setState(this.props.conversationStore.getStoreState()); + }, this); + }, + + componentWillUnmount: function() { + this.props.conversationStore.off("change", null, this); }, componentDidMount: function() { // The SDK needs to know about the configuration and the elements to use // for display. So the best way seems to pass the information here - ideally // the sdk wouldn't need to know this, but we can't change that. this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({ publisherConfig: this.getDefaultPublisherConfig({ @@ -628,64 +640,79 @@ loop.conversationViews = (function(mozL1 publishStream: function(type, enabled) { this.props.dispatcher.dispatch( new sharedActions.SetMute({ type: type, enabled: enabled })); }, + /** + * Should we render a visual cue to the user (e.g. a spinner) that a local + * stream is on its way from the camera? + * + * @returns {boolean} + * @private + */ + _isLocalLoading: function () { + return !this.state.localSrcVideoObject && !this.props.localPosterUrl; + }, + + /** + * Should we render a visual cue to the user (e.g. a spinner) that a remote + * stream is on its way from the other user? + * + * @returns {boolean} + * @private + */ + _isRemoteLoading: function() { + return !!(!this.state.remoteSrcVideoObject && + !this.props.remotePosterUrl && + !this.state.mediaConnected); + }, + shouldRenderRemoteVideo: function() { if (this.props.mediaConnected) { // If remote video is not enabled, we're muted, so we'll show an avatar // instead. return this.props.remoteVideoEnabled; } // We're not yet connected, but we don't want to show the avatar, and in // the common case, we'll just transition to the video. return true; }, render: function() { - var localStreamClasses = React.addons.classSet({ - local: true, - "local-stream": true, - "local-stream-audio": !this.props.video.enabled - }); - return ( - <div className="video-layout-wrapper"> - <div className="conversation"> - <div className="media nested"> - <div className="video_wrapper remote_wrapper"> - <div className="video_inner remote focus-stream"> - <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()} - isLoading={false} - mediaType="remote" - posterUrl={this.props.remotePosterUrl} - srcVideoObject={this.state.remoteSrcVideoObject} /> - </div> - </div> - <div className={localStreamClasses}> - <sharedViews.MediaView displayAvatar={!this.props.video.enabled} - isLoading={false} - mediaType="local" - posterUrl={this.props.localPosterUrl} - srcVideoObject={this.state.localSrcVideoObject} /> - </div> - </div> - <loop.shared.views.ConversationToolbar - audio={this.props.audio} - dispatcher={this.props.dispatcher} - edit={{ visible: false, enabled: false }} - hangup={this.hangup} - publishStream={this.publishStream} - video={this.props.video} /> - </div> + <div className="desktop-call-wrapper"> + <sharedViews.MediaLayoutView + dispatcher={this.props.dispatcher} + displayScreenShare={false} + isLocalLoading={this._isLocalLoading()} + isRemoteLoading={this._isRemoteLoading()} + isScreenShareLoading={false} + localPosterUrl={this.props.localPosterUrl} + localSrcVideoObject={this.state.localSrcVideoObject} + localVideoMuted={!this.props.video.enabled} + matchMedia={this.state.matchMedia || window.matchMedia.bind(window)} + remotePosterUrl={this.props.remotePosterUrl} + remoteSrcVideoObject={this.state.remoteSrcVideoObject} + renderRemoteVideo={this.shouldRenderRemoteVideo()} + screenSharePosterUrl={null} + screenShareVideoObject={this.state.screenShareVideoObject} + showContextRoomName={false} + useDesktopPaths={true} /> + <loop.shared.views.ConversationToolbar + audio={this.props.audio} + dispatcher={this.props.dispatcher} + edit={{ visible: false, enabled: false }} + hangup={this.hangup} + publishStream={this.publishStream} + video={this.props.video} /> </div> ); } }); /** * Master View Controller for outgoing calls. This manages * the different views that need displaying. @@ -773,16 +800,17 @@ loop.conversationViews = (function(mozL1 return (<CallFailedView contact={this.state.contact} dispatcher={this.props.dispatcher} outgoing={this.state.outgoing} />); } case CALL_STATES.ONGOING: { return (<OngoingConversationView audio={{enabled: !this.state.audioMuted}} + conversationStore={this.getStore()} dispatcher={this.props.dispatcher} mediaConnected={this.state.mediaConnected} remoteSrcVideoObject={this.state.remoteSrcVideoObject} remoteVideoEnabled={this.state.remoteVideoEnabled} video={{enabled: !this.state.videoMuted}} /> ); } case CALL_STATES.FINISHED: {
--- a/browser/components/loop/content/js/roomViews.js +++ b/browser/components/loop/content/js/roomViews.js @@ -660,29 +660,29 @@ loop.roomViews = (function(mozL10n) { /** * Should we render a visual cue to the user (e.g. a spinner) that a local * stream is on its way from the camera? * * @returns {boolean} * @private */ - _shouldRenderLocalLoading: function () { + _isLocalLoading: function () { return this.state.roomState === ROOM_STATES.MEDIA_WAIT && !this.state.localSrcVideoObject; }, /** * Should we render a visual cue to the user (e.g. a spinner) that a remote * stream is on its way from the other user? * * @returns {boolean} * @private */ - _shouldRenderRemoteLoading: function() { + _isRemoteLoading: function() { return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcVideoObject && !this.state.mediaConnected); }, handleAddContextClick: function() { this.setState({ showEditContext: true }); }, @@ -736,73 +736,64 @@ loop.roomViews = (function(mozL10n) { ); } case ROOM_STATES.ENDED: { // When conversation ended we either display a feedback form or // close the window. This is decided in the AppControllerView. return null; } default: { - return ( - React.createElement("div", {className: "room-conversation-wrapper"}, - React.createElement("div", {className: "video-layout-wrapper"}, - React.createElement("div", {className: "conversation room-conversation"}, - React.createElement("div", {className: "media nested"}, - React.createElement(DesktopRoomInvitationView, { - dispatcher: this.props.dispatcher, - error: this.state.error, - mozLoop: this.props.mozLoop, - onAddContextClick: this.handleAddContextClick, - onEditContextClose: this.handleEditContextClose, - roomData: roomData, - savingContext: this.state.savingContext, - show: shouldRenderInvitationOverlay, - showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView, - socialShareProviders: this.state.socialShareProviders}), - React.createElement("div", {className: "video_wrapper remote_wrapper"}, - React.createElement("div", {className: "video_inner remote focus-stream"}, - React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), - isLoading: this._shouldRenderRemoteLoading(), - mediaType: "remote", - posterUrl: this.props.remotePosterUrl, - srcVideoObject: this.state.remoteSrcVideoObject}) - ) - ), - React.createElement("div", {className: localStreamClasses}, - React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, - isLoading: this._shouldRenderLocalLoading(), - mediaType: "local", - posterUrl: this.props.localPosterUrl, - srcVideoObject: this.state.localSrcVideoObject}) - ), - React.createElement(DesktopRoomEditContextView, { - dispatcher: this.props.dispatcher, - error: this.state.error, - mozLoop: this.props.mozLoop, - onClose: this.handleEditContextClose, - roomData: roomData, - savingContext: this.state.savingContext, - show: !shouldRenderInvitationOverlay && shouldRenderEditContextView}) - ), - React.createElement(sharedViews.ConversationToolbar, { - audio: {enabled: !this.state.audioMuted, visible: true}, - dispatcher: this.props.dispatcher, - edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext}, - hangup: this.leaveRoom, - onEditClick: this.handleEditContextClick, - publishStream: this.publishStream, - screenShare: screenShareData, - video: {enabled: !this.state.videoMuted, visible: true}}) - ) + React.createElement("div", {className: "room-conversation-wrapper desktop-room-wrapper"}, + React.createElement(sharedViews.MediaLayoutView, { + dispatcher: this.props.dispatcher, + displayScreenShare: false, + isLocalLoading: this._isLocalLoading(), + isRemoteLoading: this._isRemoteLoading(), + isScreenShareLoading: false, + localPosterUrl: this.props.localPosterUrl, + localSrcVideoObject: this.state.localSrcVideoObject, + localVideoMuted: this.state.videoMuted, + matchMedia: this.state.matchMedia || window.matchMedia.bind(window), + remotePosterUrl: this.props.remotePosterUrl, + remoteSrcVideoObject: this.state.remoteSrcVideoObject, + renderRemoteVideo: this.shouldRenderRemoteVideo(), + screenSharePosterUrl: null, + screenShareVideoObject: this.state.screenShareVideoObject, + showContextRoomName: false, + useDesktopPaths: true}, + React.createElement(DesktopRoomInvitationView, { + dispatcher: this.props.dispatcher, + error: this.state.error, + mozLoop: this.props.mozLoop, + onAddContextClick: this.handleAddContextClick, + onEditContextClose: this.handleEditContextClose, + roomData: roomData, + savingContext: this.state.savingContext, + show: shouldRenderInvitationOverlay, + showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView, + socialShareProviders: this.state.socialShareProviders}), + React.createElement(DesktopRoomEditContextView, { + dispatcher: this.props.dispatcher, + error: this.state.error, + mozLoop: this.props.mozLoop, + onClose: this.handleEditContextClose, + roomData: roomData, + savingContext: this.state.savingContext, + show: !shouldRenderInvitationOverlay && shouldRenderEditContextView}) ), - React.createElement(sharedViews.chat.TextChatView, { + React.createElement(sharedViews.ConversationToolbar, { + audio: {enabled: !this.state.audioMuted, visible: true}, dispatcher: this.props.dispatcher, - showRoomName: false, - useDesktopPaths: true}) + edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext}, + hangup: this.leaveRoom, + onEditClick: this.handleEditContextClick, + publishStream: this.publishStream, + screenShare: screenShareData, + video: {enabled: !this.state.videoMuted, visible: true}}) ) ); } } } }); return {
--- a/browser/components/loop/content/js/roomViews.jsx +++ b/browser/components/loop/content/js/roomViews.jsx @@ -660,29 +660,29 @@ loop.roomViews = (function(mozL10n) { /** * Should we render a visual cue to the user (e.g. a spinner) that a local * stream is on its way from the camera? * * @returns {boolean} * @private */ - _shouldRenderLocalLoading: function () { + _isLocalLoading: function () { return this.state.roomState === ROOM_STATES.MEDIA_WAIT && !this.state.localSrcVideoObject; }, /** * Should we render a visual cue to the user (e.g. a spinner) that a remote * stream is on its way from the other user? * * @returns {boolean} * @private */ - _shouldRenderRemoteLoading: function() { + _isRemoteLoading: function() { return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcVideoObject && !this.state.mediaConnected); }, handleAddContextClick: function() { this.setState({ showEditContext: true }); }, @@ -736,73 +736,64 @@ loop.roomViews = (function(mozL10n) { ); } case ROOM_STATES.ENDED: { // When conversation ended we either display a feedback form or // close the window. This is decided in the AppControllerView. return null; } default: { - return ( - <div className="room-conversation-wrapper"> - <div className="video-layout-wrapper"> - <div className="conversation room-conversation"> - <div className="media nested"> - <DesktopRoomInvitationView - dispatcher={this.props.dispatcher} - error={this.state.error} - mozLoop={this.props.mozLoop} - onAddContextClick={this.handleAddContextClick} - onEditContextClose={this.handleEditContextClose} - roomData={roomData} - savingContext={this.state.savingContext} - show={shouldRenderInvitationOverlay} - showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView} - socialShareProviders={this.state.socialShareProviders} /> - <div className="video_wrapper remote_wrapper"> - <div className="video_inner remote focus-stream"> - <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()} - isLoading={this._shouldRenderRemoteLoading()} - mediaType="remote" - posterUrl={this.props.remotePosterUrl} - srcVideoObject={this.state.remoteSrcVideoObject} /> - </div> - </div> - <div className={localStreamClasses}> - <sharedViews.MediaView displayAvatar={this.state.videoMuted} - isLoading={this._shouldRenderLocalLoading()} - mediaType="local" - posterUrl={this.props.localPosterUrl} - srcVideoObject={this.state.localSrcVideoObject} /> - </div> - <DesktopRoomEditContextView - dispatcher={this.props.dispatcher} - error={this.state.error} - mozLoop={this.props.mozLoop} - onClose={this.handleEditContextClose} - roomData={roomData} - savingContext={this.state.savingContext} - show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} /> - </div> - <sharedViews.ConversationToolbar - audio={{enabled: !this.state.audioMuted, visible: true}} - dispatcher={this.props.dispatcher} - edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }} - hangup={this.leaveRoom} - onEditClick={this.handleEditContextClick} - publishStream={this.publishStream} - screenShare={screenShareData} - video={{enabled: !this.state.videoMuted, visible: true}} /> - </div> - </div> - <sharedViews.chat.TextChatView + <div className="room-conversation-wrapper desktop-room-wrapper"> + <sharedViews.MediaLayoutView dispatcher={this.props.dispatcher} - showRoomName={false} - useDesktopPaths={true} /> + displayScreenShare={false} + isLocalLoading={this._isLocalLoading()} + isRemoteLoading={this._isRemoteLoading()} + isScreenShareLoading={false} + localPosterUrl={this.props.localPosterUrl} + localSrcVideoObject={this.state.localSrcVideoObject} + localVideoMuted={this.state.videoMuted} + matchMedia={this.state.matchMedia || window.matchMedia.bind(window)} + remotePosterUrl={this.props.remotePosterUrl} + remoteSrcVideoObject={this.state.remoteSrcVideoObject} + renderRemoteVideo={this.shouldRenderRemoteVideo()} + screenSharePosterUrl={null} + screenShareVideoObject={this.state.screenShareVideoObject} + showContextRoomName={false} + useDesktopPaths={true}> + <DesktopRoomInvitationView + dispatcher={this.props.dispatcher} + error={this.state.error} + mozLoop={this.props.mozLoop} + onAddContextClick={this.handleAddContextClick} + onEditContextClose={this.handleEditContextClose} + roomData={roomData} + savingContext={this.state.savingContext} + show={shouldRenderInvitationOverlay} + showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView} + socialShareProviders={this.state.socialShareProviders} /> + <DesktopRoomEditContextView + dispatcher={this.props.dispatcher} + error={this.state.error} + mozLoop={this.props.mozLoop} + onClose={this.handleEditContextClose} + roomData={roomData} + savingContext={this.state.savingContext} + show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} /> + </sharedViews.MediaLayoutView> + <sharedViews.ConversationToolbar + audio={{enabled: !this.state.audioMuted, visible: true}} + dispatcher={this.props.dispatcher} + edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }} + hangup={this.leaveRoom} + onEditClick={this.handleEditContextClick} + publishStream={this.publishStream} + screenShare={screenShareData} + video={{enabled: !this.state.videoMuted, visible: true}} /> </div> ); } } } }); return {
--- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -499,38 +499,16 @@ .feedback .info { display: block; font-size: 10px; color: #CCC; text-align: center; } -.fx-embedded .local-stream { - position: absolute; - right: 3px; - bottom: 5px; - /* next two lines are workaround for lack of object-fit; see bug 1020445 */ - max-width: 140px; - width: 30%; - height: 28%; - max-height: 105px; -} - -.fx-embedded .local-stream.room-preview { - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - height: 100%; - width: 100%; - max-width: none; - max-height: none; -} - .conversation .media.nested .focus-stream { display: inline-block; position: absolute; /* workaround for lack of object-fit; see bug 1020445 */ width: 100%; top: 0; bottom: 0; left: 0; right: 0; @@ -587,25 +565,21 @@ } } .conversation .local .avatar { position: absolute; z-index: 1; } -.remote .avatar { +.remote > .avatar { /* make visually distinct from local avatar */ opacity: 0.25; } -.fx-embedded .media.nested { - min-height: 200px; -} - .fx-embedded-call-identifier { display: inline; width: 100%; padding: 1.2em; } .fx-embedded-call-identifier-item { height: 50px; @@ -670,17 +644,19 @@ } /* Force full height on all parents up to the video elements * this way we can ensure the aspect ratio and use height 100% * on the video element * */ html, .fx-embedded, #main, .video-layout-wrapper, -.conversation { +.conversation, +.desktop-call-wrapper, +.desktop-room-wrapper { height: 100%; } /* We use 641px rather than 640, as min-width and max-width are inclusive */ @media screen and (min-width: 641px) { .standalone .conversation .conversation-toolbar { position: absolute; bottom: 0; @@ -930,33 +906,32 @@ body[platform="win"] .share-service-drop background-color: #E8F6FE; } .room-context { background: rgba(0,0,0,.8); border-top: 2px solid #444; border-bottom: 2px solid #444; padding: .5rem; - max-height: 400px; position: absolute; left: 0; bottom: 0; width: 100%; /* Stretch to the maximum available space whilst not covering the conversation toolbar (26px). */ height: calc(100% - 26px); font-size: .9em; display: flex; flex-flow: column nowrap; align-content: flex-start; align-items: flex-start; overflow-x: hidden; overflow-y: auto; /* Make the context view float atop the video elements. */ - z-index: 2; + z-index: 3; } .room-invitation-overlay .room-context { position: relative; left: auto; bottom: auto; flex: 0 1 auto; height: 100%; @@ -1082,22 +1057,22 @@ html[dir="rtl"] .room-context-btn-close .media-layout { height: 100%; } .standalone-room-wrapper > .media-layout { /* 50px is the header, 64px for toolbar, 3em is the footer. */ height: calc(100% - 50px - 64px - 3em); + margin: 0 10px; } .media-layout > .media-wrapper { display: flex; flex-flow: column wrap; - margin: 0 10px; height: 100%; } .media-wrapper > .focus-stream { /* We want this to be the width, minus 200px which is for the right-side text chat and video displays. */ width: calc(100% - 200px); /* 100% height to fill up media-layout, thus forcing other elements into the @@ -1134,16 +1109,24 @@ html[dir="rtl"] .room-context-btn-close } .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view { /* When we're displaying the local streams, then we need to make the text chat view a bit shorter to give room. */ height: calc(100% - 300px); } +.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view, +.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view { + /* Account for height of .conversation-toolbar on desktop */ + /* When we change the toolbar in bug 1184559 we can remove this. */ + margin-top: 26px; + height: calc(100% - 150px - 26px); +} + /* Temporarily slaved from .media-wrapper until we use it in more places to avoid affecting the conversation window on desktop. */ .media-wrapper > .text-chat-view > .text-chat-entries { /* 40px is the height of .text-chat-box. */ height: calc(100% - 40px); } .media-wrapper > .text-chat-disabled > .text-chat-entries { @@ -1199,41 +1182,47 @@ html[dir="rtl"] .room-context-btn-close } .media-wrapper.receiving-screen-share > .focus-stream { height: 50%; } /* Temporarily slaved from .media-wrapper until we use it in more places to avoid affecting the conversation window on desktop. */ - .media-wrapper > .text-chat-view > .text-chat-entries { + .text-chat-view > .text-chat-entries { /* 40px is the height of .text-chat-box. */ height: calc(100% - 40px); width: 100%; } .media-wrapper > .text-chat-disabled > .text-chat-entries { /* When text chat is disabled, the entries box should be 100% height. */ height: 100%; } - .media-wrapper > .local { + .media-wrapper > .focus-stream > .local { /* Position over the remote video */ position: absolute; /* Make sure its on top */ - z-index: 1001; + z-index: 2; margin: 3px; right: 0; /* 29px is (30% of 50px high header) + (height toolbar (38px) + height footer (25px) - height header (50px)) */ - bottom: calc(30% + 29px); + bottom: 0; width: 120px; height: 120px; } + .standalone-room-wrapper > .media-layout > .media-wrapper > .local { + /* Add 10px for the margin on standalone */ + right: 10px; + } + + html[dir="rtl"] .media-wrapper > .local { right: auto; left: 0; } .media-wrapper > .text-chat-view { order: 3; flex: 1 1 auto; @@ -1242,16 +1231,25 @@ html[dir="rtl"] .room-context-btn-close .media-wrapper > .text-chat-view, .media-wrapper.showing-local-streams > .text-chat-view, .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view { /* The remaining 30% that the .focus-stream doesn't use. */ height: 30%; } + .desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view, + .desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view { + /* When we change the toolbar in bug 1184559 we can remove this. */ + /* Reset back to 0 for .conversation-toolbar override on desktop */ + margin-top: 0; + /* This is temp, to echo the .media-wrapper > .text-chat-view above */ + height: 30%; + } + .media-wrapper.receiving-screen-share > .screen { order: 1; } .media-wrapper.receiving-screen-share > .remote { /* Screen shares have remote & local video side-by-side on narrow screens */ order: 2; flex: 1 1 auto; @@ -1283,16 +1281,57 @@ html[dir="rtl"] .room-context-btn-close margin: 0; } .media-wrapper.receiving-screen-share > .text-chat-view { order: 4; } } +/* e.g. very narrow widths similar to conversation window */ +@media screen and (max-width:300px) { + .media-layout > .media-wrapper { + flex-flow: column nowrap; + } + + .media-wrapper > .focus-stream > .local { + position: absolute; + right: 0; + /* 30% is the height of the text chat. As we have a margin, + we don't need to worry about any offset for a border */ + bottom: 0; + margin: 3px; + object-fit: contain; + /* These make the avatar look reasonable and the local + video not too big */ + width: 25%; + height: 25%; + } + + .media-wrapper:not(.showing-remote-streams) > .focus-stream > .no-video { + display: none; + } + + .media-wrapper:not(.showing-remote-streams) > .focus-stream > .local { + position: relative; + margin: 0; + right: auto; + left: auto; + bottom: auto; + width: 100%; + height: 100%; + background-color: black; + } + + .media-wrapper > .focus-stream { + flex: 1 1 auto; + height: auto; + } +} + .standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar { border: none; } /* Standalone rooms */ .standalone .room-conversation-wrapper { position: relative; @@ -1410,47 +1449,22 @@ html[dir="rtl"] .standalone .room-conver display: block; } .standalone .room-conversation-wrapper .ended-conversation { position: relative; height: auto; } -/* Text chat in rooms styles */ - -.fx-embedded .room-conversation-wrapper { - display: flex; - flex-flow: column nowrap; -} - -.fx-embedded .video-layout-wrapper { - flex: 1 1 auto; -} +/* Text chat in styles */ .text-chat-view { background: white; } -.fx-embedded .text-chat-view { - flex: 1 0 auto; - display: flex; - flex-flow: column nowrap; -} - -.fx-embedded .text-chat-entries { - flex: 1 1 auto; - max-height: 120px; - min-height: 60px; -} - -.fx-embedded .text-chat-view > .text-chat-entries-empty { - display: none; -} - .text-chat-box { flex: 0 0 auto; max-height: 40px; min-height: 40px; width: 100%; } .text-chat-entries { @@ -1735,16 +1749,57 @@ html[dir="rtl"] .text-chat-entry.receive } .standalone .media.nested { /* This forces the remote video stream to fit within wrapper's height */ min-height: 0px; } } +/* e.g. very narrow widths similar to conversation window */ +@media screen and (max-width:300px) { + .text-chat-view { + flex: 0 0 auto; + display: flex; + flex-flow: column nowrap; + /* 120px max-height of .text-chat-entries plus 40px of .text-chat-box */ + max-height: 160px; + /* 60px min-height of .text-chat-entries plus 40px of .text-chat-box */ + min-height: 100px; + /* The !important is to override the values defined above which have more + specificity when we fix bug 1184559, we should be able to remove it, + but this should be tests first. */ + height: auto !important; + } + + .text-chat-entries { + /* The !important is to override the values defined above which have more + specificity when we fix bug 1184559, we should be able to remove it, + but this should be tests first. */ + flex: 1 1 auto !important; + max-height: 120px; + min-height: 60px; + } + + .text-chat-entries-empty.text-chat-disabled { + display: none; + } + + /* When the text chat entries are not present, then hide the entries view + and just show the chat box. */ + .text-chat-entries-empty { + max-height: 40px; + min-height: 40px; + } + + .text-chat-entries-empty > .text-chat-entries { + display: none; + } +} + .self-view-hidden-message { /* Not displayed by default; display is turned on elsewhere when the * self-view is actually hidden. */ display: none; } /* Avoid the privacy problem where a user can size the window so small that
--- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -575,31 +575,16 @@ loop.store.ActiveRoomStore = (function() }, /** * Handles disconnection of this local client from the sdk servers. * * @param {sharedActions.ConnectionFailure} actionData */ connectionFailure: function(actionData) { - /** - * XXX This is a workaround for desktop machines that do not have a - * camera installed. As we don't yet have device enumeration, when - * we do, this can be removed (bug 1138851), and the sdk should handle it. - */ - if (this._isDesktop && - actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA && - this.getStoreState().videoMuted === false) { - // We failed to publish with media, so due to the bug, we try again without - // video. - this.setStoreState({videoMuted: true}); - this._sdkDriver.retryPublishWithoutVideo(); - return; - } - var exitState = this._storeState.roomState === ROOM_STATES.FAILED ? this._storeState.failureExitState : this._storeState.roomState; // Treat all reasons as something failed. In theory, clientDisconnected // could be a success case, but there's no way we should be intentionally // sending that and still have the window open. this.setStoreState({ failureReason: actionData.reason,
--- a/browser/components/loop/content/shared/js/conversationStore.js +++ b/browser/components/loop/content/shared/js/conversationStore.js @@ -141,31 +141,16 @@ loop.store = loop.store || {}; /** * Handles the connection failure action, setting the state to * terminated. * * @param {sharedActions.ConnectionFailure} actionData The action data. */ connectionFailure: function(actionData) { - /** - * XXX This is a workaround for desktop machines that do not have a - * camera installed. As we don't yet have device enumeration, when - * we do, this can be removed (bug 1138851), and the sdk should handle it. - */ - if (this._isDesktop && - actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA && - this.getStoreState().videoMuted === false) { - // We failed to publish with media, so due to the bug, we try again without - // video. - this.setStoreState({videoMuted: true}); - this.sdkDriver.retryPublishWithoutVideo(); - return; - } - this._endSession(); this.setStoreState({ callState: CALL_STATES.TERMINATED, callStateReason: actionData.reason }); }, /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -56,25 +56,36 @@ loop.OTSdkDriver = (function() { // about:config, or use // // localStorage.setItem("debug.twoWayMediaTelemetry", true); this._debugTwoWayMediaTelemetry = loop.shared.utils.getBoolPreference("debug.twoWayMediaTelemetry"); /** * XXX This is a workaround for desktop machines that do not have a - * camera installed. As we don't yet have device enumeration, when - * we do, this can be removed (bug 1138851), and the sdk should handle it. + * camera installed. The SDK doesn't currently do use the new device + * enumeration apis, when it does (bug 1138851), we can drop this part. */ - if (this._isDesktop && !window.MediaStreamTrack.getSources) { + if (this._isDesktop) { // If there's no getSources function, the sdk defines its own and caches - // the result. So here we define the "normal" one which doesn't get cached, so - // we can change it later. + // the result. So here we define our own one which wraps around the + // real device enumeration api. window.MediaStreamTrack.getSources = function(callback) { - callback([{kind: "audio"}, {kind: "video"}]); + navigator.mediaDevices.enumerateDevices().then(function(devices) { + var result = []; + devices.forEach(function(device) { + if (device.kind === "audioinput") { + result.push({kind: "audio"}); + } + if (device.kind === "videoinput") { + result.push({kind: "video"}); + } + }); + callback(result); + }); }; } }; OTSdkDriver.prototype = { /** * Clones the publisher config into a new object, as the sdk modifies the * properties object. @@ -104,54 +115,35 @@ loop.OTSdkDriver = (function() { * @param {sharedActions.SetupStreamElements} actionData The data associated * with the action. See action.js. */ setupStreamElements: function(actionData) { this.publisherConfig = actionData.publisherConfig; this.sdk.on("exception", this._onOTException.bind(this)); - // At this state we init the publisher, even though we might be waiting for - // the initial connect of the session. This saves time when setting up - // the media. - this._publishLocalStreams(); - }, - - /** - * Internal function to publish a local stream. - * XXX This can be simplified when bug 1138851 is actioned. - */ - _publishLocalStreams: function() { // We expect the local video to be muted automatically by the SDK. Hence // we don't mute it manually here. this._mockPublisherEl = document.createElement("div"); + // At this state we init the publisher, even though we might be waiting for + // the initial connect of the session. This saves time when setting up + // the media. this.publisher = this.sdk.initPublisher(this._mockPublisherEl, _.extend(this._getDataChannelSettings, this._getCopyPublisherConfig)); this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this)); this.publisher.on("streamDestroyed", this._onLocalStreamDestroyed.bind(this)); this.publisher.on("accessAllowed", this._onPublishComplete.bind(this)); this.publisher.on("accessDenied", this._onPublishDenied.bind(this)); this.publisher.on("accessDialogOpened", this._onAccessDialogOpened.bind(this)); }, /** - * Forces the sdk into not using video, and starts publishing again. - * XXX This is part of the work around that will be removed by bug 1138851. - */ - retryPublishWithoutVideo: function() { - window.MediaStreamTrack.getSources = function(callback) { - callback([{kind: "audio"}]); - }; - this._publishLocalStreams(); - }, - - /** * Handles the setMute action. Informs the published stream to mute * or unmute audio as appropriate. * * @param {sharedActions.SetMute} actionData The data associated with the * action. See action.js. */ setMute: function(actionData) { if (actionData.type === "audio") {
--- a/browser/components/loop/content/shared/js/textChatView.js +++ b/browser/components/loop/content/shared/js/textChatView.js @@ -145,18 +145,17 @@ loop.shared.views.chat = (function(mozL1 } }, render: function() { /* Keep track of the last printed timestamp. */ var lastTimestamp = 0; var entriesClasses = React.addons.classSet({ - "text-chat-entries": true, - "text-chat-entries-empty": !this.props.messageList.length + "text-chat-entries": true }); return ( React.createElement("div", {className: entriesClasses}, React.createElement("div", {className: "text-chat-scroller"}, this.props.messageList.map(function(entry, i) { if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) { @@ -377,17 +376,18 @@ loop.shared.views.chat = (function(mozL1 return item.type !== CHAT_MESSAGE_TYPES.SPECIAL || item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME; }); hasNonSpecialMessages = !!messageList.length; } var textChatViewClasses = React.addons.classSet({ "text-chat-view": true, - "text-chat-disabled": !this.state.textChatEnabled + "text-chat-disabled": !this.state.textChatEnabled, + "text-chat-entries-empty": !messageList.length }); return ( React.createElement("div", {className: textChatViewClasses}, React.createElement(TextChatEntriesView, { dispatcher: this.props.dispatcher, messageList: messageList, useDesktopPaths: this.props.useDesktopPaths}),
--- a/browser/components/loop/content/shared/js/textChatView.jsx +++ b/browser/components/loop/content/shared/js/textChatView.jsx @@ -145,18 +145,17 @@ loop.shared.views.chat = (function(mozL1 } }, render: function() { /* Keep track of the last printed timestamp. */ var lastTimestamp = 0; var entriesClasses = React.addons.classSet({ - "text-chat-entries": true, - "text-chat-entries-empty": !this.props.messageList.length + "text-chat-entries": true }); return ( <div className={entriesClasses}> <div className="text-chat-scroller"> { this.props.messageList.map(function(entry, i) { if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) { @@ -377,17 +376,18 @@ loop.shared.views.chat = (function(mozL1 return item.type !== CHAT_MESSAGE_TYPES.SPECIAL || item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME; }); hasNonSpecialMessages = !!messageList.length; } var textChatViewClasses = React.addons.classSet({ "text-chat-view": true, - "text-chat-disabled": !this.state.textChatEnabled + "text-chat-disabled": !this.state.textChatEnabled, + "text-chat-entries-empty": !messageList.length }); return ( <div className={textChatViewClasses}> <TextChatEntriesView dispatcher={this.props.dispatcher} messageList={messageList} useDesktopPaths={this.props.useDesktopPaths} />
--- a/browser/components/loop/content/shared/js/views.js +++ b/browser/components/loop/content/shared/js/views.js @@ -941,83 +941,141 @@ loop.shared.views = (function(_, mozL10n {className: this.props.mediaType + "-video", muted: true})) ); } }); var MediaLayoutView = React.createClass({displayName: "MediaLayoutView", propTypes: { + children: React.PropTypes.node, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, displayScreenShare: React.PropTypes.bool.isRequired, isLocalLoading: React.PropTypes.bool.isRequired, isRemoteLoading: React.PropTypes.bool.isRequired, isScreenShareLoading: React.PropTypes.bool.isRequired, // The poster URLs are for UI-showcase testing and development. localPosterUrl: React.PropTypes.string, localSrcVideoObject: React.PropTypes.object, localVideoMuted: React.PropTypes.bool.isRequired, + // Passing in matchMedia, allows it to be overriden for ui-showcase's + // benefit. We expect either the override or window.matchMedia. + matchMedia: React.PropTypes.func.isRequired, remotePosterUrl: React.PropTypes.string, remoteSrcVideoObject: React.PropTypes.object, renderRemoteVideo: React.PropTypes.bool.isRequired, screenSharePosterUrl: React.PropTypes.string, screenShareVideoObject: React.PropTypes.object, showContextRoomName: React.PropTypes.bool.isRequired, useDesktopPaths: React.PropTypes.bool.isRequired }, + isLocalMediaAbsolutelyPositioned: function(matchMedia) { + if (!matchMedia) { + matchMedia = this.props.matchMedia; + } + return matchMedia && + // The screen width is less than 640px and we are not screen sharing. + ((matchMedia("screen and (max-width:640px)").matches && + !this.props.displayScreenShare) || + // or the screen width is less than 300px. + (matchMedia("screen and (max-width:300px)").matches)); + }, + + getInitialState: function() { + return { + localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned() + }; + }, + + componentWillReceiveProps: function(nextProps) { + // This is all for the ui-showcase's benefit. + if (this.props.matchMedia != nextProps.matchMedia) { + this.updateLocalMediaState(null, nextProps.matchMedia); + } + }, + + componentDidMount: function() { + window.addEventListener("resize", this.updateLocalMediaState); + }, + + componentWillUnmount: function() { + window.removeEventListener("resize", this.updateLocalMediaState); + }, + + updateLocalMediaState: function(event, matchMedia) { + var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia); + if (this.state.localMediaAboslutelyPositioned != newState) { + this.setState({ + localMediaAboslutelyPositioned: newState + }); + } + }, + + renderLocalVideo: function() { + return ( + React.createElement("div", {className: "local"}, + React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted, + isLoading: this.props.isLocalLoading, + mediaType: "local", + posterUrl: this.props.localPosterUrl, + srcVideoObject: this.props.localSrcVideoObject}) + ) + ); + }, + render: function() { var remoteStreamClasses = React.addons.classSet({ "remote": true, "focus-stream": !this.props.displayScreenShare }); var screenShareStreamClasses = React.addons.classSet({ "screen": true, "focus-stream": this.props.displayScreenShare }); var mediaWrapperClasses = React.addons.classSet({ "media-wrapper": true, "receiving-screen-share": this.props.displayScreenShare, "showing-local-streams": this.props.localSrcVideoObject || - this.props.localPosterUrl + this.props.localPosterUrl, + "showing-remote-streams": this.props.remoteSrcVideoObject || + this.props.remotePosterUrl || this.props.isRemoteLoading }); return ( React.createElement("div", {className: "media-layout"}, React.createElement("div", {className: mediaWrapperClasses}, React.createElement("span", {className: "self-view-hidden-message"}, mozL10n.get("self_view_hidden_message") ), React.createElement("div", {className: remoteStreamClasses}, React.createElement(MediaView, {displayAvatar: !this.props.renderRemoteVideo, isLoading: this.props.isRemoteLoading, mediaType: "remote", posterUrl: this.props.remotePosterUrl, - srcVideoObject: this.props.remoteSrcVideoObject}) + srcVideoObject: this.props.remoteSrcVideoObject}), + this.state.localMediaAboslutelyPositioned ? + this.renderLocalVideo() : null, + this.props.children ), React.createElement("div", {className: screenShareStreamClasses}, React.createElement(MediaView, {displayAvatar: false, isLoading: this.props.isScreenShareLoading, mediaType: "screen-share", posterUrl: this.props.screenSharePosterUrl, srcVideoObject: this.props.screenShareVideoObject}) ), React.createElement(loop.shared.views.chat.TextChatView, { dispatcher: this.props.dispatcher, showRoomName: this.props.showContextRoomName, useDesktopPaths: false}), - React.createElement("div", {className: "local"}, - React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted, - isLoading: this.props.isLocalLoading, - mediaType: "local", - posterUrl: this.props.localPosterUrl, - srcVideoObject: this.props.localSrcVideoObject}) - ) + this.state.localMediaAboslutelyPositioned ? + null : this.renderLocalVideo() ) ) ); } }); return { AvatarView: AvatarView,
--- a/browser/components/loop/content/shared/js/views.jsx +++ b/browser/components/loop/content/shared/js/views.jsx @@ -941,83 +941,141 @@ loop.shared.views = (function(_, mozL10n className={this.props.mediaType + "-video"} muted /> ); } }); var MediaLayoutView = React.createClass({ propTypes: { + children: React.PropTypes.node, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, displayScreenShare: React.PropTypes.bool.isRequired, isLocalLoading: React.PropTypes.bool.isRequired, isRemoteLoading: React.PropTypes.bool.isRequired, isScreenShareLoading: React.PropTypes.bool.isRequired, // The poster URLs are for UI-showcase testing and development. localPosterUrl: React.PropTypes.string, localSrcVideoObject: React.PropTypes.object, localVideoMuted: React.PropTypes.bool.isRequired, + // Passing in matchMedia, allows it to be overriden for ui-showcase's + // benefit. We expect either the override or window.matchMedia. + matchMedia: React.PropTypes.func.isRequired, remotePosterUrl: React.PropTypes.string, remoteSrcVideoObject: React.PropTypes.object, renderRemoteVideo: React.PropTypes.bool.isRequired, screenSharePosterUrl: React.PropTypes.string, screenShareVideoObject: React.PropTypes.object, showContextRoomName: React.PropTypes.bool.isRequired, useDesktopPaths: React.PropTypes.bool.isRequired }, + isLocalMediaAbsolutelyPositioned: function(matchMedia) { + if (!matchMedia) { + matchMedia = this.props.matchMedia; + } + return matchMedia && + // The screen width is less than 640px and we are not screen sharing. + ((matchMedia("screen and (max-width:640px)").matches && + !this.props.displayScreenShare) || + // or the screen width is less than 300px. + (matchMedia("screen and (max-width:300px)").matches)); + }, + + getInitialState: function() { + return { + localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned() + }; + }, + + componentWillReceiveProps: function(nextProps) { + // This is all for the ui-showcase's benefit. + if (this.props.matchMedia != nextProps.matchMedia) { + this.updateLocalMediaState(null, nextProps.matchMedia); + } + }, + + componentDidMount: function() { + window.addEventListener("resize", this.updateLocalMediaState); + }, + + componentWillUnmount: function() { + window.removeEventListener("resize", this.updateLocalMediaState); + }, + + updateLocalMediaState: function(event, matchMedia) { + var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia); + if (this.state.localMediaAboslutelyPositioned != newState) { + this.setState({ + localMediaAboslutelyPositioned: newState + }); + } + }, + + renderLocalVideo: function() { + return ( + <div className="local"> + <MediaView displayAvatar={this.props.localVideoMuted} + isLoading={this.props.isLocalLoading} + mediaType="local" + posterUrl={this.props.localPosterUrl} + srcVideoObject={this.props.localSrcVideoObject} /> + </div> + ); + }, + render: function() { var remoteStreamClasses = React.addons.classSet({ "remote": true, "focus-stream": !this.props.displayScreenShare }); var screenShareStreamClasses = React.addons.classSet({ "screen": true, "focus-stream": this.props.displayScreenShare }); var mediaWrapperClasses = React.addons.classSet({ "media-wrapper": true, "receiving-screen-share": this.props.displayScreenShare, "showing-local-streams": this.props.localSrcVideoObject || - this.props.localPosterUrl + this.props.localPosterUrl, + "showing-remote-streams": this.props.remoteSrcVideoObject || + this.props.remotePosterUrl || this.props.isRemoteLoading }); return ( <div className="media-layout"> <div className={mediaWrapperClasses}> <span className="self-view-hidden-message"> {mozL10n.get("self_view_hidden_message")} </span> <div className={remoteStreamClasses}> <MediaView displayAvatar={!this.props.renderRemoteVideo} isLoading={this.props.isRemoteLoading} mediaType="remote" posterUrl={this.props.remotePosterUrl} srcVideoObject={this.props.remoteSrcVideoObject} /> + { this.state.localMediaAboslutelyPositioned ? + this.renderLocalVideo() : null } + { this.props.children } </div> <div className={screenShareStreamClasses}> <MediaView displayAvatar={false} isLoading={this.props.isScreenShareLoading} mediaType="screen-share" posterUrl={this.props.screenSharePosterUrl} srcVideoObject={this.props.screenShareVideoObject} /> </div> <loop.shared.views.chat.TextChatView dispatcher={this.props.dispatcher} showRoomName={this.props.showContextRoomName} useDesktopPaths={false} /> - <div className="local"> - <MediaView displayAvatar={this.props.localVideoMuted} - isLoading={this.props.isLocalLoading} - mediaType="local" - posterUrl={this.props.localPosterUrl} - srcVideoObject={this.props.localSrcVideoObject} /> - </div> + { this.state.localMediaAboslutelyPositioned ? + null : this.renderLocalVideo() } </div> </div> ); } }); return { AvatarView: AvatarView,
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -251,21 +251,22 @@ loop.standaloneRoomViews = (function(moz ); } }); var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView", mixins: [ Backbone.Events, sharedMixins.MediaSetupMixin, - sharedMixins.RoomsAudioMixin, - loop.store.StoreMixin("activeRoomStore") + sharedMixins.RoomsAudioMixin ], propTypes: { + // We pass conversationStore here rather than use the mixin, to allow + // easy configurability for the ui-showcase. activeRoomStore: React.PropTypes.oneOfType([ React.PropTypes.instanceOf(loop.store.ActiveRoomStore), React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore) ]).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, isFirefox: React.PropTypes.bool.isRequired, // The poster URLs are for UI-showcase testing and development localPosterUrl: React.PropTypes.string, @@ -277,16 +278,26 @@ loop.standaloneRoomViews = (function(moz getInitialState: function() { var storeState = this.props.activeRoomStore.getStoreState(); return _.extend({}, storeState, { // Used by the UI showcase. roomState: this.props.roomState || storeState.roomState }); }, + componentWillMount: function() { + this.props.activeRoomStore.on("change", function() { + this.setState(this.props.activeRoomStore.getStoreState()); + }, this); + }, + + componentWillUnmount: function() { + this.props.activeRoomStore.off("change", null, this); + }, + componentDidMount: function() { // Adding a class to the document body element from here to ease styling it. document.body.classList.add("is-standalone-room"); }, /** * Watches for when we transition to MEDIA_WAIT room state, so we can request * user media access. @@ -424,17 +435,18 @@ loop.standaloneRoomViews = (function(moz * Should we render a visual cue to the user (e.g. a spinner) that a remote * screen-share is on its way from the other user? * * @returns {boolean} * @private */ _isScreenShareLoading: function() { return this.state.receivingScreenShare && - !this.state.screenShareVideoObject; + !this.state.screenShareVideoObject && + !this.props.screenSharePosterUrl; }, render: function() { var displayScreenShare = !!(this.state.receivingScreenShare || this.props.screenSharePosterUrl); return ( React.createElement("div", {className: "room-conversation-wrapper standalone-room-wrapper"}, @@ -451,16 +463,17 @@ loop.standaloneRoomViews = (function(moz dispatcher: this.props.dispatcher, displayScreenShare: displayScreenShare, isLocalLoading: this._isLocalLoading(), isRemoteLoading: this._isRemoteLoading(), isScreenShareLoading: this._isScreenShareLoading(), localPosterUrl: this.props.localPosterUrl, localSrcVideoObject: this.state.localSrcVideoObject, localVideoMuted: this.state.videoMuted, + matchMedia: this.state.matchMedia || window.matchMedia.bind(window), remotePosterUrl: this.props.remotePosterUrl, remoteSrcVideoObject: this.state.remoteSrcVideoObject, renderRemoteVideo: this.shouldRenderRemoteVideo(), screenSharePosterUrl: this.props.screenSharePosterUrl, screenShareVideoObject: this.state.screenShareVideoObject, showContextRoomName: true, useDesktopPaths: false}), React.createElement(sharedViews.ConversationToolbar, {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -251,21 +251,22 @@ loop.standaloneRoomViews = (function(moz ); } }); var StandaloneRoomView = React.createClass({ mixins: [ Backbone.Events, sharedMixins.MediaSetupMixin, - sharedMixins.RoomsAudioMixin, - loop.store.StoreMixin("activeRoomStore") + sharedMixins.RoomsAudioMixin ], propTypes: { + // We pass conversationStore here rather than use the mixin, to allow + // easy configurability for the ui-showcase. activeRoomStore: React.PropTypes.oneOfType([ React.PropTypes.instanceOf(loop.store.ActiveRoomStore), React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore) ]).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, isFirefox: React.PropTypes.bool.isRequired, // The poster URLs are for UI-showcase testing and development localPosterUrl: React.PropTypes.string, @@ -277,16 +278,26 @@ loop.standaloneRoomViews = (function(moz getInitialState: function() { var storeState = this.props.activeRoomStore.getStoreState(); return _.extend({}, storeState, { // Used by the UI showcase. roomState: this.props.roomState || storeState.roomState }); }, + componentWillMount: function() { + this.props.activeRoomStore.on("change", function() { + this.setState(this.props.activeRoomStore.getStoreState()); + }, this); + }, + + componentWillUnmount: function() { + this.props.activeRoomStore.off("change", null, this); + }, + componentDidMount: function() { // Adding a class to the document body element from here to ease styling it. document.body.classList.add("is-standalone-room"); }, /** * Watches for when we transition to MEDIA_WAIT room state, so we can request * user media access. @@ -424,17 +435,18 @@ loop.standaloneRoomViews = (function(moz * Should we render a visual cue to the user (e.g. a spinner) that a remote * screen-share is on its way from the other user? * * @returns {boolean} * @private */ _isScreenShareLoading: function() { return this.state.receivingScreenShare && - !this.state.screenShareVideoObject; + !this.state.screenShareVideoObject && + !this.props.screenSharePosterUrl; }, render: function() { var displayScreenShare = !!(this.state.receivingScreenShare || this.props.screenSharePosterUrl); return ( <div className="room-conversation-wrapper standalone-room-wrapper"> @@ -451,16 +463,17 @@ loop.standaloneRoomViews = (function(moz dispatcher={this.props.dispatcher} displayScreenShare={displayScreenShare} isLocalLoading={this._isLocalLoading()} isRemoteLoading={this._isRemoteLoading()} isScreenShareLoading={this._isScreenShareLoading()} localPosterUrl={this.props.localPosterUrl} localSrcVideoObject={this.state.localSrcVideoObject} localVideoMuted={this.state.videoMuted} + matchMedia={this.state.matchMedia || window.matchMedia.bind(window)} remotePosterUrl={this.props.remotePosterUrl} remoteSrcVideoObject={this.state.remoteSrcVideoObject} renderRemoteVideo={this.shouldRenderRemoteVideo()} screenSharePosterUrl={this.props.screenSharePosterUrl} screenShareVideoObject={this.state.screenShareVideoObject} showContextRoomName={true} useDesktopPaths={false} /> <sharedViews.ConversationToolbar
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -469,63 +469,46 @@ describe("loop.conversationViews", funct sinon.assert.calledWith(document.mozL10n.get, "generic_contact_unavailable_title"); }); }); describe("OngoingConversationView", function() { function mountTestComponent(extraProps) { var props = _.extend({ - dispatcher: dispatcher + conversationStore: conversationStore, + dispatcher: dispatcher, + matchMedia: window.matchMedia }, extraProps); return TestUtils.renderIntoDocument( React.createElement(loop.conversationViews.OngoingConversationView, props)); } it("should dispatch a setupStreamElements action when the view is created", function() { view = mountTestComponent(); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithMatch(dispatcher.dispatch, sinon.match.hasOwn("name", "setupStreamElements")); }); - it("should display an avatar for remote video when the stream is not enabled", function() { - view = mountTestComponent({ - mediaConnected: true, - remoteVideoEnabled: false - }); - - TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView); - }); - it("should display the remote video when the stream is enabled", function() { conversationStore.setStoreState({ remoteSrcVideoObject: { fake: 1 } }); view = mountTestComponent({ mediaConnected: true, remoteVideoEnabled: true }); expect(view.getDOMNode().querySelector(".remote video")).not.eql(null); }); - it("should display an avatar for local video when the stream is not enabled", function() { - view = mountTestComponent({ - video: { - enabled: false - } - }); - - TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView); - }); - it("should display the local video when the stream is enabled", function() { conversationStore.setStoreState({ localSrcVideoObject: { fake: 1 } }); view = mountTestComponent({ video: { enabled: true
--- a/browser/components/loop/test/desktop-local/index.html +++ b/browser/components/loop/test/desktop-local/index.html @@ -90,17 +90,17 @@ describe("Uncaught Error Check", function() { it("should load the tests without errors", function() { chai.expect(uncaughtError && uncaughtError.message).to.be.undefined; }); }); describe("Unexpected Warnings Check", function() { it("should long only the warnings we expect", function() { - chai.expect(caughtWarnings.length).to.eql(27); + chai.expect(caughtWarnings.length).to.eql(28); }); }); mocha.run(function () { var completeNode = document.createElement("p"); completeNode.setAttribute("id", "complete"); completeNode.appendChild(document.createTextNode("Complete")); document.getElementById("mocha").appendChild(completeNode);
--- a/browser/components/loop/test/desktop-local/roomViews_test.js +++ b/browser/components/loop/test/desktop-local/roomViews_test.js @@ -593,31 +593,16 @@ describe("loop.roomViews", function () { view = mountTestComponent(); expect(view.getDOMNode().querySelector(".local video")).not.eql(null); }); }); - describe("Mute", function() { - it("should render local media as audio-only if video is muted", - function() { - activeRoomStore.setStoreState({ - roomState: ROOM_STATES.SESSION_CONNECTED, - videoMuted: true - }); - - view = mountTestComponent(); - - expect(view.getDOMNode().querySelector(".local-stream-audio")) - .not.eql(null); - }); - }); - describe("Edit Context", function() { it("should show the form when the edit button is clicked", function() { view = mountTestComponent(); var node = view.getDOMNode(); expect(node.querySelector(".room-context")).to.eql(null); var editButton = node.querySelector(".btn-mute-edit");
--- a/browser/components/loop/test/functional/test_1_browser_call.py +++ b/browser/components/loop/test/functional/test_1_browser_call.py @@ -94,17 +94,17 @@ class Test1BrowserCall(MarionetteTestCas self.wait_for_element_enabled(button, 120) button.click() def local_check_room_self_video(self): self.switch_to_chatbox() # expect a video container on desktop side - media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media") + media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media-layout") self.assertEqual(media_container.tag_name, "div", "expect a video container") self.check_video(".local-video") def local_get_and_verify_room_url(self): self.switch_to_chatbox() button = self.wait_for_element_displayed(By.CLASS_NAME, "btn-copy")
--- a/browser/components/loop/test/mochitest/.eslintrc +++ b/browser/components/loop/test/mochitest/.eslintrc @@ -16,16 +16,17 @@ "HAWK_TOKEN_LENGTH": true, "checkLoggedOutState": false, "checkFxAOAuthTokenData": false, "loadLoopPanel": false, "getLoopString": false, "gMozLoopAPI": true, "mockDb": true, "mockPushHandler": true, + "OpenBrowserWindow": true, "promiseDeletedOAuthParams": false, "promiseOAuthGetRegistration": false, "promiseOAuthParamsSetup": false, "promiseObserverNotified": false, "promiseWaitForCondition": false, "resetFxA": true, // Loop specific items "MozLoopServiceInternal": true,
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js +++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js @@ -162,8 +162,24 @@ add_task(function* test_panelToggle_on_c add_task(function* test_screen_share() { Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state"); MozLoopService.setScreenShareState("1", true); Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "action", "Check button is in action state"); MozLoopService.setScreenShareState("1", false); Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state"); }); + +add_task(function* test_private_browsing_window() { + let win = OpenBrowserWindow({ private: true }); + yield new Promise(resolve => { + win.addEventListener("load", function listener() { + win.removeEventListener("load", listener); + resolve(); + }); + }); + + let button = win.LoopUI.toolbarButton.node; + Assert.ok(button, "Loop button should be present"); + Assert.ok(button.getAttribute("disabled"), "Disabled attribute should be set"); + + win.close(); +});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -911,36 +911,16 @@ describe("loop.store.ActiveRoomStore", f sessionToken: "1627384950" }); connectionFailureAction = new sharedActions.ConnectionFailure({ reason: "FAIL" }); }); - it("should retry publishing if on desktop, and in the videoMuted state", function() { - store._isDesktop = true; - - store.connectionFailure(new sharedActions.ConnectionFailure({ - reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA - })); - - sinon.assert.calledOnce(fakeSdkDriver.retryPublishWithoutVideo); - }); - - it("should set videoMuted to try when retrying publishing", function() { - store._isDesktop = true; - - store.connectionFailure(new sharedActions.ConnectionFailure({ - reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA - })); - - expect(store.getStoreState().videoMuted).eql(true); - }); - it("should store the failure reason", function() { store.connectionFailure(connectionFailureAction); expect(store.getStoreState().failureReason).eql("FAIL"); }); it("should reset the multiplexGum", function() { store.connectionFailure(connectionFailureAction);
--- a/browser/components/loop/test/shared/conversationStore_test.js +++ b/browser/components/loop/test/shared/conversationStore_test.js @@ -142,36 +142,16 @@ describe("loop.store.ConversationStore", }); describe("#connectionFailure", function() { beforeEach(function() { store._websocket = fakeWebsocket; store.setStoreState({windowId: "42"}); }); - it("should retry publishing if on desktop, and in the videoMuted state", function() { - store._isDesktop = true; - - store.connectionFailure(new sharedActions.ConnectionFailure({ - reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA - })); - - sinon.assert.calledOnce(sdkDriver.retryPublishWithoutVideo); - }); - - it("should set videoMuted to try when retrying publishing", function() { - store._isDesktop = true; - - store.connectionFailure(new sharedActions.ConnectionFailure({ - reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA - })); - - expect(store.getStoreState().videoMuted).eql(true); - }); - it("should disconnect the session", function() { store.connectionFailure( new sharedActions.ConnectionFailure({reason: "fake"})); sinon.assert.calledOnce(sdkDriver.disconnectSession); }); it("should ensure the websocket is closed", function() {
--- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -128,53 +128,16 @@ describe("loop.OTSdkDriver", function () sinon.assert.calledOnce(sdk.initPublisher); sinon.assert.calledWith(sdk.initPublisher, sinon.match.instanceOf(HTMLDivElement), expectedConfig); }); }); - describe("#retryPublishWithoutVideo", function() { - beforeEach(function() { - sdk.initPublisher.returns(publisher); - - driver.setupStreamElements(new sharedActions.SetupStreamElements({ - publisherConfig: publisherConfig - })); - }); - - it("should make MediaStreamTrack.getSources return without a video source", function(done) { - driver.retryPublishWithoutVideo(); - - window.MediaStreamTrack.getSources(function(sources) { - expect(sources.some(function(src) { - return src.kind === "video"; - })).eql(false); - - done(); - }); - }); - - it("should call initPublisher", function() { - driver.retryPublishWithoutVideo(); - - var expectedConfig = _.extend({ - channels: { - text: {} - } - }, publisherConfig); - - sinon.assert.calledTwice(sdk.initPublisher); - sinon.assert.calledWith(sdk.initPublisher, - sinon.match.instanceOf(HTMLDivElement), - expectedConfig); - }); - }); - describe("#setMute", function() { beforeEach(function() { sdk.initPublisher.returns(publisher); driver.setupStreamElements(new sharedActions.SetupStreamElements({ publisherConfig: publisherConfig })); });
--- a/browser/components/loop/test/shared/textChatView_test.js +++ b/browser/components/loop/test/shared/textChatView_test.js @@ -51,37 +51,16 @@ describe("loop.shared.views.TextChatView React.createElement(loop.shared.views.chat.TextChatEntriesView, _.extend(basicProps, extraProps))); } beforeEach(function() { store.setStoreState({ textChatEnabled: true }); }); - it("should add an empty class when the list is empty", function() { - view = mountTestComponent({ - messageList: [] - }); - - expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true); - }); - - it("should not add an empty class when the list is has items", function() { - view = mountTestComponent({ - messageList: [{ - type: CHAT_MESSAGE_TYPES.RECEIVED, - contentType: CHAT_CONTENT_TYPES.TEXT, - message: "Hello!", - receivedTimestamp: "2015-06-25T17:53:55.357Z" - }] - }); - - expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false); - }); - it("should render message entries when message were sent/ received", function() { view = mountTestComponent({ messageList: [{ type: CHAT_MESSAGE_TYPES.RECEIVED, contentType: CHAT_CONTENT_TYPES.TEXT, message: "Hello!", receivedTimestamp: "2015-06-25T17:53:55.357Z" }, { @@ -292,16 +271,51 @@ describe("loop.shared.views.TextChatView fakeServer = sinon.fakeServer.create(); store.setStoreState({ textChatEnabled: true }); }); afterEach(function() { fakeServer.restore(); }); + it("should add a disabled class when text chat is disabled", function() { + view = mountTestComponent(); + + store.setStoreState({ textChatEnabled: false }); + + expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(true); + }); + + it("should not a disabled class when text chat is enabled", function() { + view = mountTestComponent(); + + store.setStoreState({ textChatEnabled: true }); + + expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(false); + }); + + it("should add an empty class when the entries list is empty", function() { + view = mountTestComponent(); + + expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true); + }); + + it("should not add an empty class when the entries list is has items", function() { + view = mountTestComponent(); + + store.sendTextChatMessage({ + contentType: CHAT_CONTENT_TYPES.TEXT, + message: "Hello!", + sentTimestamp: "1970-01-01T00:02:00.000Z", + receivedTimestamp: "1970-01-01T00:02:00.000Z" + }); + + expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false); + }); + it("should show timestamps from msgs sent more than 1 min apart", function() { view = mountTestComponent(); store.sendTextChatMessage({ contentType: CHAT_CONTENT_TYPES.TEXT, message: "Hello!", sentTimestamp: "1970-01-01T00:02:00.000Z", receivedTimestamp: "1970-01-01T00:02:00.000Z" @@ -321,22 +335,16 @@ describe("loop.shared.views.TextChatView }); var node = view.getDOMNode(); expect(node.querySelectorAll(".text-chat-entry-timestamp").length) .to.eql(2); }); - it("should display the view if no messages and text chat is enabled", function() { - view = mountTestComponent(); - - expect(view.getDOMNode()).not.eql(null); - }); - it("should render message entries when message were sent/ received", function() { view = mountTestComponent(); store.receivedTextChatMessage({ contentType: CHAT_CONTENT_TYPES.TEXT, message: "Hello!", sentTimestamp: "1970-01-01T00:03:00.000Z", receivedTimestamp: "1970-01-01T00:03:00.000Z"
--- a/browser/components/loop/test/shared/views_test.js +++ b/browser/components/loop/test/shared/views_test.js @@ -1052,16 +1052,17 @@ describe("loop.shared.views", function() function mountTestComponent(extraProps) { var defaultProps = { dispatcher: dispatcher, displayScreenShare: false, isLocalLoading: false, isRemoteLoading: false, isScreenShareLoading: false, localVideoMuted: false, + matchMedia: window.matchMedia, renderRemoteVideo: false, showContextRoomName: false, useDesktopPaths: false }; return TestUtils.renderIntoDocument( React.createElement(sharedViews.MediaLayoutView, _.extend(defaultProps, extraProps))); @@ -1139,10 +1140,40 @@ describe("loop.shared.views", function() view = mountTestComponent({ localSrcVideoObject: {}, localPosterUrl: "fake/url" }); expect(view.getDOMNode().querySelector(".media-wrapper") .classList.contains("showing-local-streams")).eql(true); }); + + it("should not mark the wrapper as showing remote streams when not displaying a stream", function() { + view = mountTestComponent({ + remoteSrcVideoObject: null, + remotePosterUrl: null + }); + + expect(view.getDOMNode().querySelector(".media-wrapper") + .classList.contains("showing-remote-streams")).eql(false); + }); + + it("should mark the wrapper as showing remote streams when displaying a stream", function() { + view = mountTestComponent({ + remoteSrcVideoObject: {}, + remotePosterUrl: null + }); + + expect(view.getDOMNode().querySelector(".media-wrapper") + .classList.contains("showing-remote-streams")).eql(true); + }); + + it("should mark the wrapper as showing remote streams when displaying a poster url", function() { + view = mountTestComponent({ + remoteSrcVideoObject: {}, + remotePosterUrl: "fake/url" + }); + + expect(view.getDOMNode().querySelector(".media-wrapper") + .classList.contains("showing-remote-streams")).eql(true); + }); }); });
--- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -70,23 +70,35 @@ } window.removeEventListener(eventName, func); }; loop.shared.mixins.setRootObject(rootObject); var dispatcher = new loop.Dispatcher(); - var mockSDK = _.extend({ + var MockSDK = function() { + dispatcher.register(this, [ + "setupStreamElements" + ]); + }; + + MockSDK.prototype = { + setupStreamElements: function() { + // Dummy function to stop warnings. + }, + sendTextChatMessage: function(message) { dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({ message: message.message })); } - }, Backbone.Events); + }; + + var mockSDK = new MockSDK(); /** * Every view that uses an activeRoomStore needs its own; if they shared * an active store, they'd interfere with each other. * * @param options * @returns {loop.store.ActiveRoomStore} */ @@ -111,17 +123,16 @@ remoteVideoEnabled: options.remoteVideoEnabled, roomName: "A Very Long Conversation Name", roomState: options.roomState, used: !!options.roomUsed, videoMuted: !!options.videoMuted }); store.forcedUpdate = function forcedUpdate(contentWindow) { - // Since this is called by setTimeout, we don't want to lose any // exceptions if there's a problem and we need to debug, so... try { // the dimensions here are taken from the poster images that we're // using, since they give the <video> elements their initial intrinsic // size. This ensures that the right aspect ratios are calculated. // These are forced to 640x480, because it makes it visually easy to // validate that the showcase looks like the real app on a chine @@ -131,16 +142,27 @@ camera: {height: 480, orientation: 0, width: 640} }, mediaConnected: options.mediaConnected, receivingScreenShare: !!options.receivingScreenShare, remoteVideoDimensions: { camera: {height: 480, orientation: 0, width: 640} }, remoteVideoEnabled: options.remoteVideoEnabled, + // Override the matchMedia, this is so that the correct version is + // used for the frame. + // + // Currently, we use an icky hack, and the showcase conspires with + // react-frame-component to set iframe.contentWindow.matchMedia onto + // the store. Once React context matures a bit (somewhere between + // 0.14 and 1.0, apparently): + // + // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based + // + // we should be able to use those to clean this up. matchMedia: contentWindow.matchMedia.bind(contentWindow), roomState: options.roomState, videoMuted: !!options.videoMuted }; if (options.receivingScreenShare) { // Note that the image we're using had to be scaled a bit, and // it still ended up a bit narrower than the live thing that @@ -180,38 +202,49 @@ mediaConnected: false, roomState: ROOM_STATES.READY }); var updatingActiveRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS }); + var updatingMobileActiveRoomStore = makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }); + var localFaceMuteRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, videoMuted: true }); var remoteFaceMuteRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, remoteVideoEnabled: false, mediaConnected: true }); var updatingSharingRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, receivingScreenShare: true }); + var updatingSharingRoomMobileStore = makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS, + receivingScreenShare: true + }); + var loadingRemoteLoadingScreenStore = makeActiveRoomStore({ mediaConnected: false, + receivingScreenShare: true, roomState: ROOM_STATES.HAS_PARTICIPANTS, remoteSrcVideoObject: false }); var loadingScreenSharingRoomStore = makeActiveRoomStore({ + receivingScreenShare: true, roomState: ROOM_STATES.HAS_PARTICIPANTS }); /* Set up the stores for pending screen sharing */ loadingScreenSharingRoomStore.receivingScreenShare({ receiving: true, srcVideoObject: false }); @@ -229,17 +262,20 @@ }); var endedRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.ENDED, roomUsed: true }); var invitationRoomStore = new loop.store.RoomStore(dispatcher, { - mozLoop: navigator.mozLoop + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.INIT + }) }); var roomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS }) }); @@ -248,16 +284,30 @@ mozLoop: navigator.mozLoop, activeRoomStore: makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, mediaConnected: false, remoteSrcVideoObject: false }) }); + var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, { + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }) + }); + + var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, { + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }) + }); + var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, videoMuted: true }); var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: desktopLocalFaceMuteActiveRoomStore }); @@ -267,25 +317,69 @@ remoteVideoEnabled: false, mediaConnected: true }); var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: desktopRemoteFaceMuteActiveRoomStore }); - var conversationStore = new loop.store.ConversationStore(dispatcher, { - client: {}, - mozLoop: navigator.mozLoop, - sdkDriver: mockSDK - }); var textChatStore = new loop.store.TextChatStore(dispatcher, { sdkDriver: mockSDK }); + /** + * Every view that uses an conversationStore needs its own; if they shared + * a conversation store, they'd interfere with each other. + * + * @param options + * @returns {loop.store.ConversationStore} + */ + function makeConversationStore() { + var roomDispatcher = new loop.Dispatcher(); + + var store = new loop.store.ConversationStore(dispatcher, { + client: {}, + mozLoop: navigator.mozLoop, + sdkDriver: mockSDK + }); + + store.forcedUpdate = function forcedUpdate(contentWindow) { + // Since this is called by setTimeout, we don't want to lose any + // exceptions if there's a problem and we need to debug, so... + try { + var newStoreState = { + // Override the matchMedia, this is so that the correct version is + // used for the frame. + // + // Currently, we use an icky hack, and the showcase conspires with + // react-frame-component to set iframe.contentWindow.matchMedia onto + // the store. Once React context matures a bit (somewhere between + // 0.14 and 1.0, apparently): + // + // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based + // + // we should be able to use those to clean this up. + matchMedia: contentWindow.matchMedia.bind(contentWindow) + }; + + store.setStoreState(newStoreState); + } catch (ex) { + console.error("exception in forcedUpdate:", ex); + } + }; + + return store; + } + + var conversationStores = []; + for (var index = 0; index < 5; index++) { + conversationStores[index] = makeConversationStore(); + } + // Update the text chat store with the room info. textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({ roomName: "A Very Long Conversation Name", roomOwner: "fake", roomUrl: "http://showcase", urls: [{ description: "A wonderful page!", location: "http://wonderful.invalid" @@ -336,17 +430,17 @@ dispatcher.dispatch(new sharedActions.SendTextChatMessage({ contentType: loop.store.CHAT_CONTENT_TYPES.TEXT, message: "Cool", sentTimestamp: "2015-06-23T22:27:45.590Z" })); loop.store.StoreMixin.register({ activeRoomStore: activeRoomStore, - conversationStore: conversationStore, + conversationStore: conversationStores[0], textChatStore: textChatStore }); // Local mocks var mockMozLoopRooms = _.extend({}, navigator.mozLoop); var mockContact = { @@ -355,24 +449,16 @@ value: "smith@invalid.com" }] }; var mockClient = { requestCallUrlInfo: noop }; - var mockConversationModel = new loop.shared.models.ConversationModel({ - callerId: "Mrs Jones", - urlCreationDate: (new Date() / 1000).toString() - }, { - sdk: mockSDK - }); - mockConversationModel.startSession = noop; - var mockWebSocket = new loop.CallConnectionWebSocket({ url: "fake", callId: "fakeId", websocketToken: "fakeToken" }); var notifications = new loop.shared.models.NotificationCollection(); var errNotifications = new loop.shared.models.NotificationCollection(); @@ -758,93 +844,128 @@ React.createElement(Section, {name: "CallFailedView"}, React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, summary: "Call Failed - Incoming"}, React.createElement("div", {className: "fx-embedded"}, React.createElement(CallFailedView, {dispatcher: dispatcher, outgoing: false, - store: conversationStore}) + store: conversationStores[0]}) ) ), React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, summary: "Call Failed - Outgoing"}, React.createElement("div", {className: "fx-embedded"}, React.createElement(CallFailedView, {dispatcher: dispatcher, outgoing: true, - store: conversationStore}) + store: conversationStores[1]}) ) ), React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"}, summary: "Call Failed — with call URL error"}, React.createElement("div", {className: "fx-embedded"}, React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true, outgoing: true, - store: conversationStore}) + store: conversationStores[0]}) ) ) ), React.createElement(Section, {name: "OngoingConversationView"}, - React.createElement(FramedExample, {height: 254, - summary: "Desktop ongoing conversation window", - width: 298}, + React.createElement(FramedExample, { + dashed: true, + height: 394, + onContentsRendered: conversationStores[0].forcedUpdate, + summary: "Desktop ongoing conversation window", + width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(OngoingConversationView, { audio: {enabled: true}, + conversationStore: conversationStores[0], dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mediaConnected: true, remotePosterUrl: "sample-img/video-screen-remote.png", remoteVideoEnabled: true, video: {enabled: true}}) ) ), - React.createElement(FramedExample, {height: 600, - summary: "Desktop ongoing conversation window large", - width: 800}, - React.createElement("div", {className: "fx-embedded"}, - React.createElement(OngoingConversationView, { - audio: {enabled: true}, - dispatcher: dispatcher, - localPosterUrl: "sample-img/video-screen-local.png", - mediaConnected: true, - remotePosterUrl: "sample-img/video-screen-remote.png", - remoteVideoEnabled: true, - video: {enabled: true}}) - ) + React.createElement(FramedExample, { + dashed: true, + height: 400, + onContentsRendered: conversationStores[1].forcedUpdate, + summary: "Desktop ongoing conversation window (medium)", + width: 600}, + React.createElement("div", {className: "fx-embedded"}, + React.createElement(OngoingConversationView, { + audio: {enabled: true}, + conversationStore: conversationStores[1], + dispatcher: dispatcher, + localPosterUrl: "sample-img/video-screen-local.png", + mediaConnected: true, + remotePosterUrl: "sample-img/video-screen-remote.png", + remoteVideoEnabled: true, + video: {enabled: true}}) + ) ), - React.createElement(FramedExample, {height: 254, + React.createElement(FramedExample, { + height: 600, + onContentsRendered: conversationStores[2].forcedUpdate, + summary: "Desktop ongoing conversation window (large)", + width: 800}, + React.createElement("div", {className: "fx-embedded"}, + React.createElement(OngoingConversationView, { + audio: {enabled: true}, + conversationStore: conversationStores[2], + dispatcher: dispatcher, + localPosterUrl: "sample-img/video-screen-local.png", + mediaConnected: true, + remotePosterUrl: "sample-img/video-screen-remote.png", + remoteVideoEnabled: true, + video: {enabled: true}}) + ) + ), + + React.createElement(FramedExample, { + dashed: true, + height: 394, + onContentsRendered: conversationStores[3].forcedUpdate, summary: "Desktop ongoing conversation window - local face mute", width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(OngoingConversationView, { audio: {enabled: true}, + conversationStore: conversationStores[3], dispatcher: dispatcher, + localPosterUrl: "sample-img/video-screen-local.png", mediaConnected: true, remotePosterUrl: "sample-img/video-screen-remote.png", remoteVideoEnabled: true, video: {enabled: false}}) ) ), - React.createElement(FramedExample, {height: 254, + React.createElement(FramedExample, { + dashed: true, height: 394, + onContentsRendered: conversationStores[4].forcedUpdate, summary: "Desktop ongoing conversation window - remote face mute", width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(OngoingConversationView, { audio: {enabled: true}, + conversationStore: conversationStores[4], dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mediaConnected: true, + remotePosterUrl: "sample-img/video-screen-remote.png", remoteVideoEnabled: false, video: {enabled: true}}) ) ) ), React.createElement(Section, {name: "FeedbackView"}, @@ -889,86 +1010,133 @@ React.createElement("div", {className: "standalone"}, React.createElement(UnsupportedDeviceView, {platform: "ios"}) ) ) ), React.createElement(Section, {name: "DesktopRoomConversationView"}, React.createElement(FramedExample, { - height: 254, + height: 398, + onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate, summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(DesktopRoomConversationView, { dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mozLoop: navigator.mozLoop, onCallTerminated: function(){}, roomState: ROOM_STATES.INIT, roomStore: invitationRoomStore}) ) ), React.createElement(FramedExample, { dashed: true, height: 394, + onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate, summary: "Desktop room conversation (loading)", width: 298}, /* Hide scrollbars here. Rotating loading div overflows and causes scrollbars to appear */ React.createElement("div", {className: "fx-embedded overflow-hidden"}, React.createElement(DesktopRoomConversationView, { dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mozLoop: navigator.mozLoop, onCallTerminated: function(){}, remotePosterUrl: "sample-img/video-screen-remote.png", roomState: ROOM_STATES.HAS_PARTICIPANTS, roomStore: desktopRoomStoreLoading}) ) ), - React.createElement(FramedExample, {height: 254, - summary: "Desktop room conversation"}, + React.createElement(FramedExample, { + dashed: true, + height: 394, + onContentsRendered: roomStore.activeRoomStore.forcedUpdate, + summary: "Desktop room conversation", + width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(DesktopRoomConversationView, { dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mozLoop: navigator.mozLoop, onCallTerminated: function(){}, remotePosterUrl: "sample-img/video-screen-remote.png", roomState: ROOM_STATES.HAS_PARTICIPANTS, roomStore: roomStore}) ) ), - React.createElement(FramedExample, {dashed: true, - height: 394, - summary: "Desktop room conversation local face-mute", - width: 298}, + React.createElement(FramedExample, { + dashed: true, + height: 482, + onContentsRendered: desktopRoomStoreMedium.activeRoomStore.forcedUpdate, + summary: "Desktop room conversation (medium)", + width: 602}, + React.createElement("div", {className: "fx-embedded"}, + React.createElement(DesktopRoomConversationView, { + dispatcher: dispatcher, + localPosterUrl: "sample-img/video-screen-local.png", + mozLoop: navigator.mozLoop, + onCallTerminated: function(){}, + remotePosterUrl: "sample-img/video-screen-remote.png", + roomState: ROOM_STATES.HAS_PARTICIPANTS, + roomStore: desktopRoomStoreMedium}) + ) + ), + + React.createElement(FramedExample, { + dashed: true, + height: 485, + onContentsRendered: desktopRoomStoreLarge.activeRoomStore.forcedUpdate, + summary: "Desktop room conversation (large)", + width: 646}, + React.createElement("div", {className: "fx-embedded"}, + React.createElement(DesktopRoomConversationView, { + dispatcher: dispatcher, + localPosterUrl: "sample-img/video-screen-local.png", + mozLoop: navigator.mozLoop, + onCallTerminated: function(){}, + remotePosterUrl: "sample-img/video-screen-remote.png", + roomState: ROOM_STATES.HAS_PARTICIPANTS, + roomStore: desktopRoomStoreLarge}) + ) + ), + + React.createElement(FramedExample, { + dashed: true, + height: 394, + onContentsRendered: desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate, + summary: "Desktop room conversation local face-mute", + width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(DesktopRoomConversationView, { dispatcher: dispatcher, mozLoop: navigator.mozLoop, onCallTerminated: function(){}, remotePosterUrl: "sample-img/video-screen-remote.png", roomStore: desktopLocalFaceMuteRoomStore}) ) ), - React.createElement(FramedExample, {dashed: true, height: 394, + React.createElement(FramedExample, {dashed: true, + height: 394, + onContentsRendered: desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate, summary: "Desktop room conversation remote face-mute", width: 298}, React.createElement("div", {className: "fx-embedded"}, React.createElement(DesktopRoomConversationView, { dispatcher: dispatcher, localPosterUrl: "sample-img/video-screen-local.png", mozLoop: navigator.mozLoop, onCallTerminated: function(){}, + remotePosterUrl: "sample-img/video-screen-remote.png", roomStore: desktopRemoteFaceMuteRoomStore}) ) ) ), React.createElement(Section, {name: "StandaloneRoomView"}, React.createElement(FramedExample, {cssClass: "standalone", dashed: true, @@ -1076,18 +1244,17 @@ scrollbars to appear */ React.createElement("div", {className: "standalone overflow-hidden"}, React.createElement(StandaloneRoomView, { activeRoomStore: loadingRemoteLoadingScreenStore, dispatcher: dispatcher, isFirefox: true, localPosterUrl: "sample-img/video-screen-local.png", remotePosterUrl: "sample-img/video-screen-remote.png", - roomState: ROOM_STATES.HAS_PARTICIPANTS, - screenSharePosterUrl: "sample-img/video-screen-baz.png"}) + roomState: ROOM_STATES.HAS_PARTICIPANTS}) ) ), React.createElement(FramedExample, { cssClass: "standalone", dashed: true, height: 660, onContentsRendered: loadingScreenSharingRoomStore.forcedUpdate, @@ -1097,18 +1264,17 @@ scrollbars to appear */ React.createElement("div", {className: "standalone overflow-hidden"}, React.createElement(StandaloneRoomView, { activeRoomStore: loadingScreenSharingRoomStore, dispatcher: dispatcher, isFirefox: true, localPosterUrl: "sample-img/video-screen-local.png", remotePosterUrl: "sample-img/video-screen-remote.png", - roomState: ROOM_STATES.HAS_PARTICIPANTS, - screenSharePosterUrl: "sample-img/video-screen-baz.png"}) + roomState: ROOM_STATES.HAS_PARTICIPANTS}) ) ), React.createElement(FramedExample, { cssClass: "standalone", dashed: true, height: 660, onContentsRendered: updatingSharingRoomStore.forcedUpdate, @@ -1166,40 +1332,40 @@ ) ), React.createElement(Section, {name: "StandaloneRoomView (Mobile)"}, React.createElement(FramedExample, { cssClass: "standalone", dashed: true, height: 480, - onContentsRendered: updatingActiveRoomStore.forcedUpdate, + onContentsRendered: updatingMobileActiveRoomStore.forcedUpdate, summary: "Standalone room conversation (has-participants, 600x480)", width: 600}, React.createElement("div", {className: "standalone"}, React.createElement(StandaloneRoomView, { - activeRoomStore: updatingActiveRoomStore, + activeRoomStore: updatingMobileActiveRoomStore, dispatcher: dispatcher, isFirefox: true, localPosterUrl: "sample-img/video-screen-local.png", remotePosterUrl: "sample-img/video-screen-remote.png", roomState: ROOM_STATES.HAS_PARTICIPANTS}) ) ), React.createElement(FramedExample, { cssClass: "standalone", dashed: true, height: 480, - onContentsRendered: updatingSharingRoomStore.forcedUpdate, + onContentsRendered: updatingSharingRoomMobileStore.forcedUpdate, summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)", width: 600}, React.createElement("div", {className: "standalone", cssClass: "standalone"}, React.createElement(StandaloneRoomView, { - activeRoomStore: updatingSharingRoomStore, + activeRoomStore: updatingSharingRoomMobileStore, dispatcher: dispatcher, isFirefox: true, localPosterUrl: "sample-img/video-screen-local.png", remotePosterUrl: "sample-img/video-screen-remote.png", roomState: ROOM_STATES.HAS_PARTICIPANTS, screenSharePosterUrl: "sample-img/video-screen-terminal.png"}) ) ) @@ -1277,17 +1443,17 @@ setTimeout(waitForQueuedFrames, 500); return; } // Put the title back, in case views changed it. document.title = "Loop UI Components Showcase"; // This simulates the mocha layout for errors which means we can run // this alongside our other unit tests but use the same harness. - var expectedWarningsCount = 23; + var expectedWarningsCount = 18; var warningsMismatch = caughtWarnings.length !== expectedWarningsCount; if (uncaughtError || warningsMismatch) { $("#results").append("<div class='failures'><em>" + ((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>"); if (warningsMismatch) { $("#results").append("<li class='test fail'>" + "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" + "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -70,23 +70,35 @@ } window.removeEventListener(eventName, func); }; loop.shared.mixins.setRootObject(rootObject); var dispatcher = new loop.Dispatcher(); - var mockSDK = _.extend({ + var MockSDK = function() { + dispatcher.register(this, [ + "setupStreamElements" + ]); + }; + + MockSDK.prototype = { + setupStreamElements: function() { + // Dummy function to stop warnings. + }, + sendTextChatMessage: function(message) { dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({ message: message.message })); } - }, Backbone.Events); + }; + + var mockSDK = new MockSDK(); /** * Every view that uses an activeRoomStore needs its own; if they shared * an active store, they'd interfere with each other. * * @param options * @returns {loop.store.ActiveRoomStore} */ @@ -111,17 +123,16 @@ remoteVideoEnabled: options.remoteVideoEnabled, roomName: "A Very Long Conversation Name", roomState: options.roomState, used: !!options.roomUsed, videoMuted: !!options.videoMuted }); store.forcedUpdate = function forcedUpdate(contentWindow) { - // Since this is called by setTimeout, we don't want to lose any // exceptions if there's a problem and we need to debug, so... try { // the dimensions here are taken from the poster images that we're // using, since they give the <video> elements their initial intrinsic // size. This ensures that the right aspect ratios are calculated. // These are forced to 640x480, because it makes it visually easy to // validate that the showcase looks like the real app on a chine @@ -131,16 +142,27 @@ camera: {height: 480, orientation: 0, width: 640} }, mediaConnected: options.mediaConnected, receivingScreenShare: !!options.receivingScreenShare, remoteVideoDimensions: { camera: {height: 480, orientation: 0, width: 640} }, remoteVideoEnabled: options.remoteVideoEnabled, + // Override the matchMedia, this is so that the correct version is + // used for the frame. + // + // Currently, we use an icky hack, and the showcase conspires with + // react-frame-component to set iframe.contentWindow.matchMedia onto + // the store. Once React context matures a bit (somewhere between + // 0.14 and 1.0, apparently): + // + // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based + // + // we should be able to use those to clean this up. matchMedia: contentWindow.matchMedia.bind(contentWindow), roomState: options.roomState, videoMuted: !!options.videoMuted }; if (options.receivingScreenShare) { // Note that the image we're using had to be scaled a bit, and // it still ended up a bit narrower than the live thing that @@ -180,38 +202,49 @@ mediaConnected: false, roomState: ROOM_STATES.READY }); var updatingActiveRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS }); + var updatingMobileActiveRoomStore = makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }); + var localFaceMuteRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, videoMuted: true }); var remoteFaceMuteRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, remoteVideoEnabled: false, mediaConnected: true }); var updatingSharingRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, receivingScreenShare: true }); + var updatingSharingRoomMobileStore = makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS, + receivingScreenShare: true + }); + var loadingRemoteLoadingScreenStore = makeActiveRoomStore({ mediaConnected: false, + receivingScreenShare: true, roomState: ROOM_STATES.HAS_PARTICIPANTS, remoteSrcVideoObject: false }); var loadingScreenSharingRoomStore = makeActiveRoomStore({ + receivingScreenShare: true, roomState: ROOM_STATES.HAS_PARTICIPANTS }); /* Set up the stores for pending screen sharing */ loadingScreenSharingRoomStore.receivingScreenShare({ receiving: true, srcVideoObject: false }); @@ -229,17 +262,20 @@ }); var endedRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.ENDED, roomUsed: true }); var invitationRoomStore = new loop.store.RoomStore(dispatcher, { - mozLoop: navigator.mozLoop + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.INIT + }) }); var roomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS }) }); @@ -248,16 +284,30 @@ mozLoop: navigator.mozLoop, activeRoomStore: makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, mediaConnected: false, remoteSrcVideoObject: false }) }); + var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, { + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }) + }); + + var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, { + mozLoop: navigator.mozLoop, + activeRoomStore: makeActiveRoomStore({ + roomState: ROOM_STATES.HAS_PARTICIPANTS + }) + }); + var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({ roomState: ROOM_STATES.HAS_PARTICIPANTS, videoMuted: true }); var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: desktopLocalFaceMuteActiveRoomStore }); @@ -267,25 +317,69 @@ remoteVideoEnabled: false, mediaConnected: true }); var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, activeRoomStore: desktopRemoteFaceMuteActiveRoomStore }); - var conversationStore = new loop.store.ConversationStore(dispatcher, { - client: {}, - mozLoop: navigator.mozLoop, - sdkDriver: mockSDK - }); var textChatStore = new loop.store.TextChatStore(dispatcher, { sdkDriver: mockSDK }); + /** + * Every view that uses an conversationStore needs its own; if they shared + * a conversation store, they'd interfere with each other. + * + * @param options + * @returns {loop.store.ConversationStore} + */ + function makeConversationStore() { + var roomDispatcher = new loop.Dispatcher(); + + var store = new loop.store.ConversationStore(dispatcher, { + client: {}, + mozLoop: navigator.mozLoop, + sdkDriver: mockSDK + }); + + store.forcedUpdate = function forcedUpdate(contentWindow) { + // Since this is called by setTimeout, we don't want to lose any + // exceptions if there's a problem and we need to debug, so... + try { + var newStoreState = { + // Override the matchMedia, this is so that the correct version is + // used for the frame. + // + // Currently, we use an icky hack, and the showcase conspires with + // react-frame-component to set iframe.contentWindow.matchMedia onto + // the store. Once React context matures a bit (somewhere between + // 0.14 and 1.0, apparently): + // + // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based + // + // we should be able to use those to clean this up. + matchMedia: contentWindow.matchMedia.bind(contentWindow) + }; + + store.setStoreState(newStoreState); + } catch (ex) { + console.error("exception in forcedUpdate:", ex); + } + }; + + return store; + } + + var conversationStores = []; + for (var index = 0; index < 5; index++) { + conversationStores[index] = makeConversationStore(); + } + // Update the text chat store with the room info. textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({ roomName: "A Very Long Conversation Name", roomOwner: "fake", roomUrl: "http://showcase", urls: [{ description: "A wonderful page!", location: "http://wonderful.invalid" @@ -336,17 +430,17 @@ dispatcher.dispatch(new sharedActions.SendTextChatMessage({ contentType: loop.store.CHAT_CONTENT_TYPES.TEXT, message: "Cool", sentTimestamp: "2015-06-23T22:27:45.590Z" })); loop.store.StoreMixin.register({ activeRoomStore: activeRoomStore, - conversationStore: conversationStore, + conversationStore: conversationStores[0], textChatStore: textChatStore }); // Local mocks var mockMozLoopRooms = _.extend({}, navigator.mozLoop); var mockContact = { @@ -355,24 +449,16 @@ value: "smith@invalid.com" }] }; var mockClient = { requestCallUrlInfo: noop }; - var mockConversationModel = new loop.shared.models.ConversationModel({ - callerId: "Mrs Jones", - urlCreationDate: (new Date() / 1000).toString() - }, { - sdk: mockSDK - }); - mockConversationModel.startSession = noop; - var mockWebSocket = new loop.CallConnectionWebSocket({ url: "fake", callId: "fakeId", websocketToken: "fakeToken" }); var notifications = new loop.shared.models.NotificationCollection(); var errNotifications = new loop.shared.models.NotificationCollection(); @@ -758,93 +844,128 @@ <Section name="CallFailedView"> <Example dashed={true} style={{width: "300px", height: "272px"}} summary="Call Failed - Incoming"> <div className="fx-embedded"> <CallFailedView dispatcher={dispatcher} outgoing={false} - store={conversationStore} /> + store={conversationStores[0]} /> </div> </Example> <Example dashed={true} style={{width: "300px", height: "272px"}} summary="Call Failed - Outgoing"> <div className="fx-embedded"> <CallFailedView dispatcher={dispatcher} outgoing={true} - store={conversationStore} /> + store={conversationStores[1]} /> </div> </Example> <Example dashed={true} style={{width: "300px", height: "272px"}} summary="Call Failed — with call URL error"> <div className="fx-embedded"> <CallFailedView dispatcher={dispatcher} emailLinkError={true} outgoing={true} - store={conversationStore} /> + store={conversationStores[0]} /> </div> </Example> </Section> <Section name="OngoingConversationView"> - <FramedExample height={254} - summary="Desktop ongoing conversation window" - width={298}> + <FramedExample + dashed={true} + height={394} + onContentsRendered={conversationStores[0].forcedUpdate} + summary="Desktop ongoing conversation window" + width={298}> <div className="fx-embedded"> <OngoingConversationView audio={{enabled: true}} + conversationStore={conversationStores[0]} dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mediaConnected={true} remotePosterUrl="sample-img/video-screen-remote.png" remoteVideoEnabled={true} video={{enabled: true}} /> </div> </FramedExample> - <FramedExample height={600} - summary="Desktop ongoing conversation window large" - width={800}> - <div className="fx-embedded"> - <OngoingConversationView - audio={{enabled: true}} - dispatcher={dispatcher} - localPosterUrl="sample-img/video-screen-local.png" - mediaConnected={true} - remotePosterUrl="sample-img/video-screen-remote.png" - remoteVideoEnabled={true} - video={{enabled: true}} /> - </div> + <FramedExample + dashed={true} + height={400} + onContentsRendered={conversationStores[1].forcedUpdate} + summary="Desktop ongoing conversation window (medium)" + width={600}> + <div className="fx-embedded"> + <OngoingConversationView + audio={{enabled: true}} + conversationStore={conversationStores[1]} + dispatcher={dispatcher} + localPosterUrl="sample-img/video-screen-local.png" + mediaConnected={true} + remotePosterUrl="sample-img/video-screen-remote.png" + remoteVideoEnabled={true} + video={{enabled: true}} /> + </div> </FramedExample> - <FramedExample height={254} + <FramedExample + height={600} + onContentsRendered={conversationStores[2].forcedUpdate} + summary="Desktop ongoing conversation window (large)" + width={800}> + <div className="fx-embedded"> + <OngoingConversationView + audio={{enabled: true}} + conversationStore={conversationStores[2]} + dispatcher={dispatcher} + localPosterUrl="sample-img/video-screen-local.png" + mediaConnected={true} + remotePosterUrl="sample-img/video-screen-remote.png" + remoteVideoEnabled={true} + video={{enabled: true}} /> + </div> + </FramedExample> + + <FramedExample + dashed={true} + height={394} + onContentsRendered={conversationStores[3].forcedUpdate} summary="Desktop ongoing conversation window - local face mute" width={298} > <div className="fx-embedded"> <OngoingConversationView audio={{enabled: true}} + conversationStore={conversationStores[3]} dispatcher={dispatcher} + localPosterUrl="sample-img/video-screen-local.png" mediaConnected={true} remotePosterUrl="sample-img/video-screen-remote.png" remoteVideoEnabled={true} video={{enabled: false}} /> </div> </FramedExample> - <FramedExample height={254} + <FramedExample + dashed={true} height={394} + onContentsRendered={conversationStores[4].forcedUpdate} summary="Desktop ongoing conversation window - remote face mute" width={298} > <div className="fx-embedded"> <OngoingConversationView audio={{enabled: true}} + conversationStore={conversationStores[4]} dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mediaConnected={true} + remotePosterUrl="sample-img/video-screen-remote.png" remoteVideoEnabled={false} video={{enabled: true}} /> </div> </FramedExample> </Section> <Section name="FeedbackView"> @@ -889,86 +1010,133 @@ <div className="standalone"> <UnsupportedDeviceView platform="ios"/> </div> </Example> </Section> <Section name="DesktopRoomConversationView"> <FramedExample - height={254} + height={398} + onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate} summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)" width={298}> <div className="fx-embedded"> <DesktopRoomConversationView dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mozLoop={navigator.mozLoop} onCallTerminated={function(){}} roomState={ROOM_STATES.INIT} roomStore={invitationRoomStore} /> </div> </FramedExample> <FramedExample dashed={true} height={394} + onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate} summary="Desktop room conversation (loading)" width={298}> {/* Hide scrollbars here. Rotating loading div overflows and causes scrollbars to appear */} <div className="fx-embedded overflow-hidden"> <DesktopRoomConversationView dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mozLoop={navigator.mozLoop} onCallTerminated={function(){}} remotePosterUrl="sample-img/video-screen-remote.png" roomState={ROOM_STATES.HAS_PARTICIPANTS} roomStore={desktopRoomStoreLoading} /> </div> </FramedExample> - <FramedExample height={254} - summary="Desktop room conversation"> + <FramedExample + dashed={true} + height={394} + onContentsRendered={roomStore.activeRoomStore.forcedUpdate} + summary="Desktop room conversation" + width={298}> <div className="fx-embedded"> <DesktopRoomConversationView dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mozLoop={navigator.mozLoop} onCallTerminated={function(){}} remotePosterUrl="sample-img/video-screen-remote.png" roomState={ROOM_STATES.HAS_PARTICIPANTS} roomStore={roomStore} /> </div> </FramedExample> - <FramedExample dashed={true} - height={394} - summary="Desktop room conversation local face-mute" - width={298}> + <FramedExample + dashed={true} + height={482} + onContentsRendered={desktopRoomStoreMedium.activeRoomStore.forcedUpdate} + summary="Desktop room conversation (medium)" + width={602}> + <div className="fx-embedded"> + <DesktopRoomConversationView + dispatcher={dispatcher} + localPosterUrl="sample-img/video-screen-local.png" + mozLoop={navigator.mozLoop} + onCallTerminated={function(){}} + remotePosterUrl="sample-img/video-screen-remote.png" + roomState={ROOM_STATES.HAS_PARTICIPANTS} + roomStore={desktopRoomStoreMedium} /> + </div> + </FramedExample> + + <FramedExample + dashed={true} + height={485} + onContentsRendered={desktopRoomStoreLarge.activeRoomStore.forcedUpdate} + summary="Desktop room conversation (large)" + width={646}> + <div className="fx-embedded"> + <DesktopRoomConversationView + dispatcher={dispatcher} + localPosterUrl="sample-img/video-screen-local.png" + mozLoop={navigator.mozLoop} + onCallTerminated={function(){}} + remotePosterUrl="sample-img/video-screen-remote.png" + roomState={ROOM_STATES.HAS_PARTICIPANTS} + roomStore={desktopRoomStoreLarge} /> + </div> + </FramedExample> + + <FramedExample + dashed={true} + height={394} + onContentsRendered={desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate} + summary="Desktop room conversation local face-mute" + width={298}> <div className="fx-embedded"> <DesktopRoomConversationView dispatcher={dispatcher} mozLoop={navigator.mozLoop} onCallTerminated={function(){}} remotePosterUrl="sample-img/video-screen-remote.png" roomStore={desktopLocalFaceMuteRoomStore} /> </div> </FramedExample> - <FramedExample dashed={true} height={394} + <FramedExample dashed={true} + height={394} + onContentsRendered={desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate} summary="Desktop room conversation remote face-mute" width={298} > <div className="fx-embedded"> <DesktopRoomConversationView dispatcher={dispatcher} localPosterUrl="sample-img/video-screen-local.png" mozLoop={navigator.mozLoop} onCallTerminated={function(){}} + remotePosterUrl="sample-img/video-screen-remote.png" roomStore={desktopRemoteFaceMuteRoomStore} /> </div> </FramedExample> </Section> <Section name="StandaloneRoomView"> <FramedExample cssClass="standalone" dashed={true} @@ -1076,18 +1244,17 @@ scrollbars to appear */} <div className="standalone overflow-hidden"> <StandaloneRoomView activeRoomStore={loadingRemoteLoadingScreenStore} dispatcher={dispatcher} isFirefox={true} localPosterUrl="sample-img/video-screen-local.png" remotePosterUrl="sample-img/video-screen-remote.png" - roomState={ROOM_STATES.HAS_PARTICIPANTS} - screenSharePosterUrl="sample-img/video-screen-baz.png" /> + roomState={ROOM_STATES.HAS_PARTICIPANTS} /> </div> </FramedExample> <FramedExample cssClass="standalone" dashed={true} height={660} onContentsRendered={loadingScreenSharingRoomStore.forcedUpdate} @@ -1097,18 +1264,17 @@ scrollbars to appear */} <div className="standalone overflow-hidden"> <StandaloneRoomView activeRoomStore={loadingScreenSharingRoomStore} dispatcher={dispatcher} isFirefox={true} localPosterUrl="sample-img/video-screen-local.png" remotePosterUrl="sample-img/video-screen-remote.png" - roomState={ROOM_STATES.HAS_PARTICIPANTS} - screenSharePosterUrl="sample-img/video-screen-baz.png" /> + roomState={ROOM_STATES.HAS_PARTICIPANTS} /> </div> </FramedExample> <FramedExample cssClass="standalone" dashed={true} height={660} onContentsRendered={updatingSharingRoomStore.forcedUpdate} @@ -1166,40 +1332,40 @@ </FramedExample> </Section> <Section name="StandaloneRoomView (Mobile)"> <FramedExample cssClass="standalone" dashed={true} height={480} - onContentsRendered={updatingActiveRoomStore.forcedUpdate} + onContentsRendered={updatingMobileActiveRoomStore.forcedUpdate} summary="Standalone room conversation (has-participants, 600x480)" width={600}> <div className="standalone"> <StandaloneRoomView - activeRoomStore={updatingActiveRoomStore} + activeRoomStore={updatingMobileActiveRoomStore} dispatcher={dispatcher} isFirefox={true} localPosterUrl="sample-img/video-screen-local.png" remotePosterUrl="sample-img/video-screen-remote.png" roomState={ROOM_STATES.HAS_PARTICIPANTS} /> </div> </FramedExample> <FramedExample cssClass="standalone" dashed={true} height={480} - onContentsRendered={updatingSharingRoomStore.forcedUpdate} + onContentsRendered={updatingSharingRoomMobileStore.forcedUpdate} summary="Standalone room convo (has-participants, receivingScreenShare, 600x480)" width={600} > <div className="standalone" cssClass="standalone"> <StandaloneRoomView - activeRoomStore={updatingSharingRoomStore} + activeRoomStore={updatingSharingRoomMobileStore} dispatcher={dispatcher} isFirefox={true} localPosterUrl="sample-img/video-screen-local.png" remotePosterUrl="sample-img/video-screen-remote.png" roomState={ROOM_STATES.HAS_PARTICIPANTS} screenSharePosterUrl="sample-img/video-screen-terminal.png" /> </div> </FramedExample> @@ -1277,17 +1443,17 @@ setTimeout(waitForQueuedFrames, 500); return; } // Put the title back, in case views changed it. document.title = "Loop UI Components Showcase"; // This simulates the mocha layout for errors which means we can run // this alongside our other unit tests but use the same harness. - var expectedWarningsCount = 23; + var expectedWarningsCount = 18; var warningsMismatch = caughtWarnings.length !== expectedWarningsCount; if (uncaughtError || warningsMismatch) { $("#results").append("<div class='failures'><em>" + ((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>"); if (warningsMismatch) { $("#results").append("<li class='test fail'>" + "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" + "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/devtools/styleinspector/computed-view.js +++ b/browser/devtools/styleinspector/computed-view.js @@ -1,15 +1,16 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* globals overlays, StyleInspectorMenu */ +/* globals overlays, StyleInspectorMenu, loader, clipboardHelper, + _Iterator, StopIteration */ "use strict"; const {Cc, Ci, Cu} = require("chrome"); const ToolDefinitions = require("main").Tools; const {CssLogic} = require("devtools/styleinspector/css-logic"); const {ELEMENT_STYLE} = require("devtools/server/actors/styles"); @@ -44,90 +45,86 @@ const HTML_NS = "http://www.w3.org/1999/ * onBatch {function} Will be called after each batch of iterations, * before yielding to the main loop. * onDone {function} Will be called when iteration is complete. * onCancel {function} Will be called if the process is canceled. * threshold {int} How long to process before yielding, in ms. * * @constructor */ -function UpdateProcess(aWin, aGenerator, aOptions) -{ +function UpdateProcess(aWin, aGenerator, aOptions) { this.win = aWin; this.iter = _Iterator(aGenerator); this.onItem = aOptions.onItem || function() {}; this.onBatch = aOptions.onBatch || function() {}; this.onDone = aOptions.onDone || function() {}; this.onCancel = aOptions.onCancel || function() {}; this.threshold = aOptions.threshold || 45; this.canceled = false; } UpdateProcess.prototype = { /** * Schedule a new batch on the main loop. */ - schedule: function UP_schedule() - { + schedule: function() { if (this.canceled) { return; } this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0); }, /** * Cancel the running process. onItem will not be called again, * and onCancel will be called. */ - cancel: function UP_cancel() - { + cancel: function() { if (this._timeout) { this.win.clearTimeout(this._timeout); this._timeout = 0; } this.canceled = true; this.onCancel(); }, - _timeoutHandler: function UP_timeoutHandler() { + _timeoutHandler: function() { this._timeout = null; try { this._runBatch(); this.schedule(); } catch(e) { if (e instanceof StopIteration) { this.onBatch(); this.onDone(); return; } console.error(e); throw e; } }, - _runBatch: function Y_runBatch() - { + _runBatch: function() { let time = Date.now(); - while(!this.canceled) { + while (!this.canceled) { // Continue until iter.next() throws... let next = this.iter.next(); this.onItem(next[1]); if ((Date.now() - time) > this.threshold) { this.onBatch(); return; } } } }; /** - * CssComputedView is a panel that manages the display of a table sorted by style. - * There should be one instance of CssComputedView per style display (of which there - * will generally only be one). + * CssComputedView is a panel that manages the display of a table + * sorted by style. There should be one instance of CssComputedView + * per style display (of which there will generally only be one). * * @param {Inspector} inspector toolbox panel * @param {Document} document The document that will contain the computed view. * @param {PageStyleFront} pageStyle * Front for the page style actor that will be providing * the style information. * * @constructor @@ -137,18 +134,18 @@ function CssComputedView(inspector, docu this.styleDocument = document; this.styleWindow = this.styleDocument.defaultView; this.pageStyle = pageStyle; this.propertyViews = []; this._outputParser = new OutputParser(); - let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. - getService(Ci.nsIXULChromeRegistry); + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry); this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr"; // Create bound methods. this.focusWindow = this.focusWindow.bind(this); this._onKeypress = this._onKeypress.bind(this); this._onContextMenu = this._onContextMenu.bind(this); this._onClick = this._onClick.bind(this); this._onCopy = this._onCopy.bind(this); @@ -206,18 +203,17 @@ function CssComputedView(inspector, docu this.highlighters.addToView(); } /** * Memoized lookup of a l10n string from a string bundle. * @param {string} aName The key to lookup. * @returns A localized version of the given key. */ -CssComputedView.l10n = function CssComputedView_l10n(aName) -{ +CssComputedView.l10n = function(aName) { try { return CssComputedView._strings.GetStringFromName(aName); } catch (ex) { Services.console.logStringMessage("Error reading '" + aName + "'"); throw new Error("l10n error with " + aName); } }; @@ -246,31 +242,31 @@ CssComputedView.prototype = { // Number of visible properties numVisibleProperties: 0, setPageStyle: function(pageStyle) { this.pageStyle = pageStyle; }, - get includeBrowserStyles() - { + get includeBrowserStyles() { return this.includeBrowserStylesCheckbox.checked; }, _handlePrefChange: function(event, data) { if (this._computed && (data.pref == "devtools.defaultColorUnit" || data.pref == PREF_ORIG_SOURCES)) { this.refreshPanel(); } }, /** - * Update the view with a new selected element. - * The CssComputedView panel will show the style information for the given element. + * Update the view with a new selected element. The CssComputedView panel + * will show the style information for the given element. + * * @param {NodeFront} aElement The highlighted node to get styles for. * @returns a promise that will be resolved when highlighting is complete. */ selectElement: function(aElement) { if (!aElement) { this.viewedElement = null; this.noResults.hidden = false; @@ -378,30 +374,30 @@ CssComputedView.prototype = { value.url = node.href; } else { return null; } return {type, value}; }, - _createPropertyViews: function() - { + _createPropertyViews: function() { if (this._createViewsPromise) { return this._createViewsPromise; } let deferred = promise.defer(); this._createViewsPromise = deferred.promise; this.refreshSourceFilter(); this.numVisibleProperties = 0; let fragment = this.styleDocument.createDocumentFragment(); - this._createViewsProcess = new UpdateProcess(this.styleWindow, CssComputedView.propertyNames, { + this._createViewsProcess = new UpdateProcess( + this.styleWindow, CssComputedView.propertyNames, { onItem: (aPropertyName) => { // Per-item callback. let propView = new PropertyView(this, aPropertyName); fragment.appendChild(propView.buildMain()); fragment.appendChild(propView.buildSelectorContainer()); if (propView.visible) { this.numVisibleProperties++; @@ -421,39 +417,38 @@ CssComputedView.prototype = { this._createViewsProcess.schedule(); return deferred.promise; }, /** * Refresh the panel content. */ - refreshPanel: function CssComputedView_refreshPanel() - { + refreshPanel: function() { if (!this.viewedElement) { return promise.resolve(); } // Capture the current viewed element to return from the promise handler // early if it changed let viewedElement = this.viewedElement; return promise.all([ this._createPropertyViews(), this.pageStyle.getComputed(this.viewedElement, { filter: this._sourceFilter, onlyMatched: !this.includeBrowserStyles, markMatched: true }) - ]).then(([createViews, computed]) => { + ]).then(([, computed]) => { if (viewedElement !== this.viewedElement) { - return; + return promise.resolve(); } - this._matchedProperties = new Set; + this._matchedProperties = new Set(); for (let name in computed) { if (computed[name].matched) { this._matchedProperties.add(name); } } this._computed = computed; if (this._refreshProcess) { @@ -464,17 +459,18 @@ CssComputedView.prototype = { // Reset visible property count this.numVisibleProperties = 0; // Reset zebra striping. this._darkStripe = true; let deferred = promise.defer(); - this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, { + this._refreshProcess = new UpdateProcess( + this.styleWindow, this.propertyViews, { onItem: (aPropView) => { aPropView.refresh(); }, onDone: () => { this._refreshProcess = null; this.noResults.hidden = this.numVisibleProperties > 0; if (this.searchField.value.length > 0 && !this.numVisibleProperties) { @@ -506,18 +502,17 @@ CssComputedView.prototype = { } }, /** * Called when the user enters a search term in the filter style search box. * * @param {Event} aEvent the DOM Event object. */ - _onFilterStyles: function(aEvent) - { + _onFilterStyles: function(aEvent) { let win = this.styleWindow; if (this._filterChangedTimeout) { win.clearTimeout(this._filterChangedTimeout); } let filterTimeout = (this.searchField.value.length > 0) ? FILTER_CHANGED_TIMEOUT : 0; @@ -575,49 +570,45 @@ CssComputedView.prototype = { return false; }, /** * The change event handler for the includeBrowserStyles checkbox. * * @param {Event} aEvent the DOM Event object. */ - _onIncludeBrowserStyles: function(aEvent) - { + _onIncludeBrowserStyles: function(aEvent) { this.refreshSourceFilter(); this.refreshPanel(); }, /** * When includeBrowserStylesCheckbox.checked is false we only display * properties that have matched selectors and have been included by the * document or one of thedocument's stylesheets. If .checked is false we * display all properties including those that come from UA stylesheets. */ - refreshSourceFilter: function CssComputedView_setSourceFilter() - { + refreshSourceFilter: function() { this._matchedProperties = null; this._sourceFilter = this.includeBrowserStyles ? CssLogic.FILTER.UA : CssLogic.FILTER.USER; }, - _onSourcePrefChanged: function CssComputedView__onSourcePrefChanged() - { + _onSourcePrefChanged: function() { for (let propView of this.propertyViews) { propView.updateSourceLinks(); } this.inspector.emit("computed-view-sourcelinks-updated"); }, /** * The CSS as displayed by the UI. */ - createStyleViews: function CssComputedView_createStyleViews() - { + createStyleViews: function() { if (CssComputedView.propertyNames) { return; } CssComputedView.propertyNames = []; // Here we build and cache a list of css properties supported by the browser // We could use any element but let's use the main document's root element @@ -636,41 +627,39 @@ CssComputedView.prototype = { } CssComputedView.propertyNames.sort(); CssComputedView.propertyNames.push.apply(CssComputedView.propertyNames, mozProps.sort()); this._createPropertyViews().then(null, e => { if (!this._isDestroyed) { - console.warn("The creation of property views was cancelled because the " + - "computed-view was destroyed before it was done creating views"); + console.warn("The creation of property views was cancelled because " + + "the computed-view was destroyed before it was done creating views"); } else { console.error(e); } }); }, /** * Get a set of properties that have matched selectors. * * @return {Set} If a property name is in the set, it has matching selectors. */ - get matchedProperties() - { - return this._matchedProperties || new Set; + get matchedProperties() { + return this._matchedProperties || new Set(); }, /** * Focus the window on mousedown. * - * @param aEvent The event object + * @param event The event object */ - focusWindow: function(aEvent) - { + focusWindow: function(event) { let win = this.styleDocument.defaultView; win.focus(); }, /** * Context menu handler. */ _onContextMenu: function(event) { @@ -702,18 +691,18 @@ CssComputedView.prototype = { /** * Copy the current selection to the clipboard */ copySelection: function() { try { let win = this.styleDocument.defaultView; let text = win.getSelection().toString().trim(); - // Tidy up block headings by moving CSS property names and their values onto - // the same line and inserting a colon between them. + // Tidy up block headings by moving CSS property names and their + // values onto the same line and inserting a colon between them. let textArray = text.split(/[\r\n]+/); let result = ""; // Parse text array to output string. if (textArray.length > 1) { for (let prop of textArray) { if (CssComputedView.propertyNames.indexOf(prop) !== -1) { // Property name @@ -732,18 +721,17 @@ CssComputedView.prototype = { } catch(e) { console.error(e); } }, /** * Destructor for CssComputedView. */ - destroy: function CssComputedView_destroy() - { + destroy: function() { this.viewedElement = null; this._outputParser = null; gDevTools.off("pref-changed", this._handlePrefChange); this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); this._prefObserver.destroy(); @@ -814,18 +802,17 @@ PropertyInfo.prototype = { /** * A container to give easy access to property data from the template engine. * * @constructor * @param {CssComputedView} aTree the CssComputedView instance we are working with. * @param {string} aName the CSS property name for which this PropertyView * instance will render the rules. */ -function PropertyView(aTree, aName) -{ +function PropertyView(aTree, aName) { this.tree = aTree; this.name = aName; this.getRTLAttr = aTree.getRTLAttr; this.link = "https://developer.mozilla.org/CSS/" + aName; this._propertyInfo = new PropertyInfo(aTree, aName); } @@ -859,42 +846,38 @@ PropertyView.prototype = { prevViewedElement: null, /** * Get the computed style for the current property. * * @return {string} the computed style for the current property of the * currently highlighted element. */ - get value() - { + get value() { return this.propertyInfo.value; }, /** * An easy way to access the CssPropertyInfo behind this PropertyView. */ - get propertyInfo() - { + get propertyInfo() { return this._propertyInfo; }, /** * Does the property have any matched selectors? */ - get hasMatchedSelectors() - { + get hasMatchedSelectors() { return this.tree.matchedProperties.has(this.name); }, /** * Should this property be visible? */ - get visible() - { + get visible() { if (!this.tree.viewedElement) { return false; } if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { return false; } @@ -908,45 +891,42 @@ PropertyView.prototype = { return true; }, /** * Returns the className that should be assigned to the propertyView. * @return string */ - get propertyHeaderClassName() - { + get propertyHeaderClassName() { if (this.visible) { let isDark = this.tree._darkStripe = !this.tree._darkStripe; return isDark ? "property-view row-striped" : "property-view"; } return "property-view-hidden"; }, /** * Returns the className that should be assigned to the propertyView content * container. * @return string */ - get propertyContentClassName() - { + get propertyContentClassName() { if (this.visible) { let isDark = this.tree._darkStripe; return isDark ? "property-content row-striped" : "property-content"; } return "property-content-hidden"; }, /** * Build the markup for on computed style * @return Element */ - buildMain: function PropertyView_buildMain() - { + buildMain: function() { let doc = this.tree.styleDocument; // Build the container element this.onMatchedToggle = this.onMatchedToggle.bind(this); this.element = doc.createElementNS(HTML_NS, "div"); this.element.setAttribute("class", this.propertyHeaderClassName); this.element.addEventListener("dblclick", this.onMatchedToggle, false); @@ -993,33 +973,31 @@ PropertyView.prototype = { this.valueNode.setAttribute("dir", "ltr"); // Make it hand over the focus to the container this.valueNode.addEventListener("click", this.onFocus, false); this.element.appendChild(this.valueNode); return this.element; }, - buildSelectorContainer: function PropertyView_buildSelectorContainer() - { + buildSelectorContainer: function() { let doc = this.tree.styleDocument; let element = doc.createElementNS(HTML_NS, "div"); element.setAttribute("class", this.propertyContentClassName); this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div"); this.matchedSelectorsContainer.setAttribute("class", "matchedselectors"); element.appendChild(this.matchedSelectorsContainer); return element; }, /** * Refresh the panel's CSS property value. */ - refresh: function PropertyView_refresh() - { + refresh: function() { this.element.className = this.propertyHeaderClassName; this.element.nextElementSibling.className = this.propertyContentClassName; if (this.prevViewedElement != this.tree.viewedElement) { this._matchedSelectorViews = null; this.prevViewedElement = this.tree.viewedElement; } @@ -1046,50 +1024,50 @@ PropertyView.prototype = { this.valueNode.appendChild(frag); this.refreshMatchedSelectors(); }, /** * Refresh the panel matched rules. */ - refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors() - { + refreshMatchedSelectors: function() { let hasMatchedSelectors = this.hasMatchedSelectors; this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; if (hasMatchedSelectors) { this.matchedExpander.classList.add("expandable"); } else { this.matchedExpander.classList.remove("expandable"); } if (this.matchedExpanded && hasMatchedSelectors) { - return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => { - if (!this.matchedExpanded) { - return; - } + return this.tree.pageStyle + .getMatchedSelectors(this.tree.viewedElement, this.name) + .then(matched => { + if (!this.matchedExpanded) { + return promise.resolve(undefined); + } - this._matchedSelectorResponse = matched; + this._matchedSelectorResponse = matched; - return this._buildMatchedSelectors().then(() => { - this.matchedExpander.setAttribute("open", ""); - this.tree.inspector.emit("computed-view-property-expanded"); - }); - }).then(null, console.error); - } else { - this.matchedSelectorsContainer.innerHTML = ""; - this.matchedExpander.removeAttribute("open"); - this.tree.inspector.emit("computed-view-property-collapsed"); - return promise.resolve(undefined); + return this._buildMatchedSelectors().then(() => { + this.matchedExpander.setAttribute("open", ""); + this.tree.inspector.emit("computed-view-property-expanded"); + }); + }).then(null, console.error); } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.tree.inspector.emit("computed-view-property-collapsed"); + return promise.resolve(undefined); }, - get matchedSelectors() - { + get matchedSelectors() { return this._matchedSelectorResponse; }, _buildMatchedSelectors: function() { let promises = []; let frag = this.element.ownerDocument.createDocumentFragment(); for (let selector of this.matchedSelectorViews) { @@ -1125,76 +1103,73 @@ PropertyView.prototype = { this.matchedSelectorsContainer.appendChild(frag); return promise.all(promises); }, /** * Provide access to the matched SelectorViews that we are currently * displaying. */ - get matchedSelectorViews() - { + get matchedSelectorViews() { if (!this._matchedSelectorViews) { this._matchedSelectorViews = []; this._matchedSelectorResponse.forEach( - function matchedSelectorViews_convert(aSelectorInfo) { - this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo)); + function(aSelectorInfo) { + let selectorView = new SelectorView(this.tree, aSelectorInfo); + this._matchedSelectorViews.push(selectorView); }, this); } return this._matchedSelectorViews; }, /** * Update all the selector source links to reflect whether we're linking to * original sources (e.g. Sass files). */ - updateSourceLinks: function PropertyView_updateSourceLinks() - { + updateSourceLinks: function() { if (!this._matchedSelectorViews) { return; } for (let view of this._matchedSelectorViews) { view.updateSourceLink(); } }, /** * The action when a user expands matched selectors. * * @param {Event} aEvent Used to determine the class name of the targets click * event. */ - onMatchedToggle: function PropertyView_onMatchedToggle(aEvent) - { + onMatchedToggle: function(aEvent) { if (aEvent.shiftKey) { return; } this.matchedExpanded = !this.matchedExpanded; this.refreshMatchedSelectors(); aEvent.preventDefault(); }, /** * The action when a user clicks on the MDN help link for a property. */ - mdnLinkClick: function PropertyView_mdnLinkClick(aEvent) - { + mdnLinkClick: function(aEvent) { let inspector = this.tree.inspector; if (inspector.target.tab) { let browserWin = inspector.target.tab.ownerDocument.defaultView; browserWin.openUILinkIn(this.link, "tab"); } aEvent.preventDefault(); }, /** * Destroy this property view, removing event listeners */ - destroy: function PropertyView_destroy() { + destroy: function() { this.element.removeEventListener("dblclick", this.onMatchedToggle, false); this.element.removeEventListener("keydown", this.onKeyDown, false); this.element = null; this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false); this.matchedExpander = null; this.nameNode.removeEventListener("click", this.onFocus, false); @@ -1205,18 +1180,17 @@ PropertyView.prototype = { } }; /** * A container to give us easy access to display data from a CssRule * @param CssComputedView aTree, the owning CssComputedView * @param aSelectorInfo */ -function SelectorView(aTree, aSelectorInfo) -{ +function SelectorView(aTree, aSelectorInfo) { this.tree = aTree; this.selectorInfo = aSelectorInfo; this._cacheStatusNames(); this.openStyleEditor = this.openStyleEditor.bind(this); this.maybeOpenStyleEditor = this.maybeOpenStyleEditor.bind(this); this.ready = this.updateSourceLink(); @@ -1240,71 +1214,63 @@ SelectorView.prototype = { * Cache localized status names. * * These statuses are localized inside the styleinspector.properties string * bundle. * @see css-logic.js - the CssLogic.STATUS array. * * @return {void} */ - _cacheStatusNames: function SelectorView_cacheStatusNames() - { + _cacheStatusNames: function() { if (SelectorView.STATUS_NAMES.length) { return; } for (let status in CssLogic.STATUS) { let i = CssLogic.STATUS[status]; if (i > CssLogic.STATUS.UNMATCHED) { let value = CssComputedView.l10n("rule.status." + status); // Replace normal spaces with non-breaking spaces - SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0'); + SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0"); } } }, /** * A localized version of cssRule.status */ - get statusText() - { + get statusText() { return SelectorView.STATUS_NAMES[this.selectorInfo.status]; }, /** * Get class name for selector depending on status */ - get statusClass() - { + get statusClass() { return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; }, - get href() - { + get href() { if (this._href) { return this._href; } let sheet = this.selectorInfo.rule.parentStyleSheet; this._href = sheet ? sheet.href : "#"; return this._href; }, - get sourceText() - { + get sourceText() { return this.selectorInfo.sourceText; }, - - get value() - { + get value() { return this.selectorInfo.value; }, - get outputFragment() - { + get outputFragment() { // Sadly, because this fragment is added to the template by DOM Templater // we lose any events that are attached. This means that URLs will open in a // new window. At some point we should fix this by stopping using the // templater. let outputParser = this.tree._outputParser; let frag = outputParser.parseCssProperty( this.selectorInfo.name, this.selectorInfo.value, { @@ -1315,35 +1281,33 @@ SelectorView.prototype = { }); return frag; }, /** * Update the text of the source link to reflect whether we're showing * original sources or not. */ - updateSourceLink: function() - { + updateSourceLink: function() { return this.updateSource().then((oldSource) => { if (oldSource != this.source && this.tree.element) { let selector = '[sourcelocation="' + oldSource + '"]'; let link = this.tree.element.querySelector(selector); if (link) { link.textContent = this.source; link.setAttribute("sourcelocation", this.source); } } }); }, /** * Update the 'source' store based on our original sources preference. */ - updateSource: function() - { + updateSource: function() { let rule = this.selectorInfo.rule; this.sheet = rule.parentStyleSheet; if (!rule || !this.sheet) { let oldSource = this.source; this.source = CssLogic.l10n("rule.sourceElement"); return promise.resolve(oldSource); } @@ -1368,53 +1332,44 @@ SelectorView.prototype = { let oldSource = this.source; this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; return promise.resolve(oldSource); }, /** * Open the style editor if the RETURN key was pressed. */ - maybeOpenStyleEditor: function(aEvent) - { + maybeOpenStyleEditor: function(aEvent) { let keyEvent = Ci.nsIDOMKeyEvent; if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) { this.openStyleEditor(); } }, /** * When a css link is clicked this method is called in order to either: * 1. Open the link in view source (for chrome stylesheets). * 2. Open the link in the style editor. * * We can only view stylesheets contained in document.styleSheets inside the * style editor. * * @param aEvent The click event */ - openStyleEditor: function(aEvent) - { + openStyleEditor: function(aEvent) { let inspector = this.tree.inspector; let rule = this.selectorInfo.rule; // The style editor can only display stylesheets coming from content because // chrome stylesheets are not listed in the editor's stylesheet selector. // // If the stylesheet is a content stylesheet we send it to the style // editor else we display it in the view source window. - let sheet = rule.parentStyleSheet; - if (!sheet || sheet.isSystem) { - let contentDoc = null; - if (this.tree.viewedElement.isLocal_toBeDeprecated()) { - let rawNode = this.tree.viewedElement.rawNode(); - if (rawNode) { - contentDoc = rawNode.ownerDocument; - } - } + let parentStyleSheet = rule.parentStyleSheet; + if (!parentStyleSheet || parentStyleSheet.isSystem) { let toolbox = gDevTools.getToolbox(inspector.target); toolbox.viewSource(rule.href, rule.line); return; } let location = promise.resolve(rule.location); if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { location = rule.getOriginalLocation(); @@ -1442,17 +1397,17 @@ SelectorView.prototype = { * A set of attributes to set on the node. */ function createChild(aParent, aTag, aAttributes={}) { let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag); for (let attr in aAttributes) { if (aAttributes.hasOwnProperty(attr)) { if (attr === "textContent") { elt.textContent = aAttributes[attr]; - } else if(attr === "child") { + } else if (attr === "child") { elt.appendChild(aAttributes[attr]); } else { elt.setAttribute(attr, aAttributes[attr]); } } } aParent.appendChild(elt); return elt;
--- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -1,16 +1,17 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals overlays, Services, EventEmitter, StyleInspectorMenu, - clipboardHelper, _strings, domUtils, AutocompletePopup */ + clipboardHelper, _strings, domUtils, AutocompletePopup, loader, + osString */ "use strict"; const {Cc, Ci, Cu} = require("chrome"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {CssLogic} = require("devtools/styleinspector/css-logic"); const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); @@ -204,17 +205,17 @@ ElementStyle.prototype = { */ populate: function() { let populated = this.pageStyle.getApplied(this.element, { inherited: true, matchedSelectors: true, filter: this.showUserAgentStyles ? "ua" : undefined, }).then(entries => { if (this.destroyed) { - return; + return promise.resolve(undefined); } // Make sure the dummy element has been created before continuing... return this.dummyElementPromise.then(() => { if (this.populated != populated) { // Don't care anymore. return; } @@ -231,24 +232,22 @@ ElementStyle.prototype = { // Mark overridden computed styles. this.markOverriddenAll(); this._sortRulesForPseudoElement(); // We're done with the previous list of rules. delete this._refreshRules; - - return null; }); }).then(null, e => { // populate is often called after a setTimeout, // the connection may already be closed. if (this.destroyed) { - return; + return promise.resolve(undefined); } return promiseWarn(e); }); this.populated = populated; return this.populated; }, /** @@ -631,17 +630,17 @@ Rule.prototype = { // Store disabled properties in the disabled store. let disabled = this.elementStyle.store.disabled; if (disabledProps.length > 0) { disabled.set(this.style, disabledProps); } else { disabled.delete(this.style); } - let promise = aModifications.apply().then(() => { + let modificationsPromise = aModifications.apply().then(() => { let cssProps = {}; for (let cssProp of parseDeclarations(this.style.cssText)) { cssProps[cssProp.name] = cssProp; } for (let textProp of this.textProps) { if (!textProp.enabled) { continue; @@ -663,18 +662,18 @@ Rule.prototype = { if (promise === this._applyingModifications) { this._applyingModifications = null; } this.elementStyle._changed(); }).then(null, promiseWarn); - this._applyingModifications = promise; - return promise; + this._applyingModifications = modificationsPromise; + return modificationsPromise; }, /** * Renames a property. * * @param {TextProperty} aProperty * The property to rename. * @param {string} aName @@ -1106,17 +1105,18 @@ TextProperty.prototype = { this.rule.removeProperty(this); }, /** * Return a string representation of the rule property. */ stringifyProperty: function() { // Get the displayed property value - let declaration = this.name + ": " + this.editor.committed.value + ";"; + let declaration = this.name + ": " + this.editor.valueSpan.textContent + + ";"; // Comment out property declarations that are not enabled if (!this.enabled) { declaration = "/* " + declaration + " */"; } return declaration; } @@ -1736,17 +1736,17 @@ CssRuleView.prototype = { }, /** * Update the rules for the currently highlighted element. */ refreshPanel: function() { // Ignore refreshes during editing or when no element is selected. if (this.isEditing || !this._elementStyle) { - return; + return promise.resolve(undefined); } // Repopulate the element style once the current modifications are done. let promises = []; for (let rule of this._elementStyle.rules) { if (rule._applyingModifications) { promises.push(rule._applyingModifications); } @@ -1888,19 +1888,20 @@ CssRuleView.prototype = { this._showPseudoElements = Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); } return this._showPseudoElements; }, /** * Creates an expandable container in the rule view - * @param {String} aLabel The label for the container header - * @param {Boolean} isPseudo Whether or not the container will hold - * pseudo element rules + * @param {String} aLabel + * The label for the container header + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules * @return {DOMNode} The container element */ createExpandableContainer: function(aLabel, isPseudo = false) { let header = this.styleDocument.createElementNS(HTML_NS, "div"); header.className = this._getRuleViewHeaderClassName(true); header.classList.add("show-expandable-container"); header.textContent = aLabel; @@ -1910,54 +1911,69 @@ CssRuleView.prototype = { header.insertBefore(twisty, header.firstChild); this.element.appendChild(header); let container = this.styleDocument.createElementNS(HTML_NS, "div"); container.classList.add("ruleview-expandable-container"); this.element.appendChild(container); - let toggleContainerVisibility = (isPseudo, showPseudo) => { - let isOpen = twisty.getAttribute("open"); - - if (isPseudo) { - this._showPseudoElements = !!showPseudo; - - Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", - this.showPseudoElements); - - header.classList.toggle("show-expandable-container", - this.showPseudoElements); - - isOpen = !this.showPseudoElements; - } else { - header.classList.toggle("show-expandable-container"); - } - - if (isOpen) { - twisty.removeAttribute("open"); - } else { - twisty.setAttribute("open", "true"); - } - }; - header.addEventListener("dblclick", () => { - toggleContainerVisibility(isPseudo, !this.showPseudoElements); + this._toggleContainerVisibility(twisty, header, isPseudo, + !this.showPseudoElements); }, false); + twisty.addEventListener("click", () => { - toggleContainerVisibility(isPseudo, !this.showPseudoElements); + this._toggleContainerVisibility(twisty, header, isPseudo, + !this.showPseudoElements); }, false); if (isPseudo) { - toggleContainerVisibility(isPseudo, this.showPseudoElements); + this._toggleContainerVisibility(twisty, header, isPseudo, + this.showPseudoElements); } return container; }, + /** + * Toggle the visibility of an expandable container + * @param {DOMNode} twisty + * clickable toggle DOM Node + * @param {DOMNode} header + * expandable container header DOM Node + * @param {Boolean} isPseudo + * whether or not the container will hold pseudo element rules + * @param {Boolean} showPseudo + * whether or not pseudo element rules should be displayed + */ + _toggleContainerVisibility: function(twisty, header, isPseudo, showPseudo) { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + header.classList.toggle("show-expandable-container", + this.showPseudoElements); + + isOpen = !this.showPseudoElements; + } else { + header.classList.toggle("show-expandable-container"); + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }, + _getRuleViewHeaderClassName: function(isPseudo) { let baseClassName = "theme-gutter ruleview-header"; return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName; }, /** * Creates editor UI for each of the rules in _elementStyle.
--- a/browser/devtools/styleinspector/style-inspector-menu.js +++ b/browser/devtools/styleinspector/style-inspector-menu.js @@ -226,16 +226,17 @@ StyleInspectorMenu.prototype = { /** * Display the necessary copy context menu items depending on the clicked * node and selection in the rule view. */ _updateCopyMenuItems: function() { this.menuitemCopy.hidden = !this._hasTextSelected(); this.menuitemCopyColor.hidden = !this._isColorPopup(); this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl(); + this.menuitemCopyUrl.hidden = !this._isImageUrl(); this.menuitemCopyRule.hidden = true; this.menuitemCopyLocation.hidden = true; this.menuitemCopyPropertyDeclaration.hidden = true; this.menuitemCopyPropertyName.hidden = true; this.menuitemCopyPropertyValue.hidden = true; this.menuitemCopySelector.hidden = true; @@ -373,16 +374,20 @@ StyleInspectorMenu.prototype = { _onCopyColor: function() { clipboardHelper.copyString(this._colorToCopy); }, /* * Retrieve the url for the selected image and copy it to the clipboard */ _onCopyUrl: function() { + if (!this._clickedNodeInfo) { + return; + } + clipboardHelper.copyString(this._clickedNodeInfo.value.url); }, /** * Retrieve the image data for the selected image url and copy it to the clipboard */ _onCopyImageDataUrl: Task.async(function*() { if (!this._clickedNodeInfo) {
--- a/browser/devtools/styleinspector/test/browser_ruleview_copy_styles.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_copy_styles.js @@ -48,37 +48,66 @@ add_task(function*() { copyPropertyDeclaration: false, copyPropertyName: true, copyPropertyValue: false, copySelector: true, copyRule: false } }, { + desc: "Test Copy Property Value with Priority", + node: ruleEditor.rule.textProps[3].editor.valueSpan, + menuItem: contextmenu.menuitemCopyPropertyValue, + expectedPattern: "#00F !important", + hidden: { + copyLocation: true, + copyPropertyDeclaration: false, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: true, + copyRule: false + } + }, + { desc: "Test Copy Property Declaration", node: ruleEditor.rule.textProps[2].editor.nameSpan, menuItem: contextmenu.menuitemCopyPropertyDeclaration, expectedPattern: "font-size: 12px;", hidden: { copyLocation: true, copyPropertyDeclaration: false, copyPropertyName: false, copyPropertyValue: true, copySelector: true, copyRule: false } }, { + desc: "Test Copy Property Declaration with Priority", + node: ruleEditor.rule.textProps[3].editor.nameSpan, + menuItem: contextmenu.menuitemCopyPropertyDeclaration, + expectedPattern: "border-color: #00F !important;", + hidden: { + copyLocation: true, + copyPropertyDeclaration: false, + copyPropertyName: false, + copyPropertyValue: true, + copySelector: true, + copyRule: false + } + }, + { desc: "Test Copy Rule", node: ruleEditor.rule.textProps[2].editor.nameSpan, menuItem: contextmenu.menuitemCopyRule, expectedPattern: "#testid {[\\r\\n]+" + "\tcolor: #F00;[\\r\\n]+" + "\tbackground-color: #00F;[\\r\\n]+" + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + "}", hidden: { copyLocation: true, copyPropertyDeclaration: false, copyPropertyName: false, copyPropertyValue: true, copySelector: true, copyRule: false @@ -119,16 +148,17 @@ add_task(function*() { }, desc: "Test Copy Rule with Disabled Property", node: ruleEditor.rule.textProps[2].editor.nameSpan, menuItem: contextmenu.menuitemCopyRule, expectedPattern: "#testid {[\\r\\n]+" + "\t\/\\* color: #F00; \\*\/[\\r\\n]+" + "\tbackground-color: #00F;[\\r\\n]+" + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + "}", hidden: { copyLocation: true, copyPropertyDeclaration: false, copyPropertyName: false, copyPropertyValue: true, copySelector: true, copyRule: false
--- a/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-urls.js +++ b/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-urls.js @@ -75,16 +75,17 @@ function* testCopyUrlToClipboard({view, let rect = imageLink.getClientRects()[0]; let x = rect.left + 2; let y = rect.top + 2; EventUtils.synthesizeMouseAtPoint(x, y, {button: 2, type: "contextmenu"}, getViewWindow(view)); yield popup; info("Context menu is displayed"); + ok(!view._contextmenu.menuitemCopyUrl.hidden, "\"Copy URL\" menu entry is displayed"); ok(!view._contextmenu.menuitemCopyImageDataUrl.hidden, "\"Copy Image Data-URL\" menu entry is displayed"); if (type == "data-uri") { info("Click Copy Data URI and wait for clipboard"); yield waitForClipboard(() => view._contextmenu.menuitemCopyImageDataUrl.click(), expected); } else { info("Click Copy URL and wait for clipboard"); yield waitForClipboard(() => view._contextmenu.menuitemCopyUrl.click(), expected);
--- a/browser/devtools/styleinspector/test/doc_copystyles.css +++ b/browser/devtools/styleinspector/test/doc_copystyles.css @@ -1,9 +1,10 @@ /* 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/. */ html, body, #testid { color: #F00; background-color: #00F; font-size: 12px; + border-color: #00F !important; }
--- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -633,16 +633,17 @@ @RESPATH@/modules/* ; Safe Browsing #ifdef MOZ_URL_CLASSIFIER @RESPATH@/components/nsURLClassifier.manifest @RESPATH@/components/nsUrlClassifierHashCompleter.js @RESPATH@/components/nsUrlClassifierListManager.js @RESPATH@/components/nsUrlClassifierLib.js +@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js @RESPATH@/components/url-classifier.xpt #endif ; ANGLE GLES-on-D3D rendering library #ifdef MOZ_ANGLE_RENDERER @BINPATH@/libEGL.dll @BINPATH@/libGLESv2.dll
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties +++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties @@ -92,16 +92,19 @@ quit-button.tooltiptext.linux2 = Quit %1 # LOCALIZATION NOTE(quit-button.tooltiptext.mac): %1$S is the brand name (e.g. Firefox), # %2$S is the keyboard shortcut quit-button.tooltiptext.mac = Quit %1$S (%2$S) # LOCALIZATION NOTE(loop-call-button3.label): This is a brand name, request # approval before you change it. loop-call-button3.label = Hello loop-call-button3.tooltiptext = Start a conversation +# LOCALIZATION NOTE(loop-call-button3-pb.tooltiptext): Shown when the button is +# placed inside a Private Browsing window. %S is the value of loop-call-button3.label. +loop-call-button3-pb.tooltiptext = %S is not available in Private Browsing social-share-button.label = Share This Page social-share-button.tooltiptext = Share this page panic-button.label = Forget panic-button.tooltiptext = Forget about some browsing history web-apps-button.label = Apps
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties +++ b/browser/locales/en-US/chrome/browser/loop/loop.properties @@ -201,16 +201,18 @@ hangup_button_caption2=Exit mute_local_audio_button_title=Mute your audio unmute_local_audio_button_title=Unmute your audio mute_local_video_button_title=Mute your video unmute_local_video_button_title=Unmute your video active_screenshare_button_title=Stop sharing inactive_screenshare_button_title=Share your screen share_tabs_button_title2=Share your Tabs share_windows_button_title=Share other Windows +self_view_hidden_message=Self-view hidden but still being sent; resize window to show + ## LOCALIZATION NOTE (call_with_contact_title): The title displayed ## when calling a contact. Don't translate the part between {{..}} because ## this will be replaced by the contact's name. call_with_contact_title=Conversation with {{contactName}} # Outgoing conversation
--- a/browser/themes/windows/browser-aero.css +++ b/browser/themes/windows/browser-aero.css @@ -110,17 +110,17 @@ #titlebar-buttonbox, .titlebar-button { -moz-appearance: none !important; } .titlebar-button { border: none; margin: 0 !important; - padding: 12px 17px; + padding: 10px 17px; } #main-window[sizemode=maximized] .titlebar-button { padding-top: 8px; padding-bottom: 8px; } .titlebar-button > .toolbarbutton-icon { @@ -142,16 +142,30 @@ #titlebar-close { list-style-image: url(chrome://browser/skin/caption-buttons.svg#close); } #titlebar-close:hover { list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white); } + #titlebar-min:-moz-lwtheme { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-themes); + } + #titlebar-max:-moz-lwtheme { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-themes); + } + #main-window[sizemode="maximized"] #titlebar-max:-moz-lwtheme { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-themes); + } + #titlebar-close:-moz-lwtheme { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-themes); + } + + /* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which * rounds up to 13px, which makes the icon one pixel too big on 1.25dppx. Fix: */ @media (min-resolution: 1.20dppx) and (max-resolution: 1.45dppx) { .titlebar-button > .toolbarbutton-icon { width: 11.5px; height: 11.5px; } } @@ -217,30 +231,42 @@ @media not all and (-moz-windows-default-theme) { .titlebar-button { background-color: -moz-field; } .titlebar-button:hover { background-color: Highlight; } + #titlebar-min { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast); + } #titlebar-min:hover { - list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highlight); + list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast-hover); } + #titlebar-max { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast); + } #titlebar-max:hover { - list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highlight); + list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast-hover); } + #main-window[sizemode="maximized"] #titlebar-max { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast); + } #main-window[sizemode="maximized"] #titlebar-max:hover { - list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highlight); + list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast-hover); } + #titlebar-close { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast); + } #titlebar-close:hover { - list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight); + list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast-hover); } } } } } @media (-moz-os-version: windows-vista), (-moz-os-version: windows-win7),
--- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -654,17 +654,17 @@ toolbar[brighttext] .toolbarbutton-1 > . -moz-box-pack: center; } #nav-bar #PanelUI-menu-button { -moz-padding-start: 5px; -moz-padding-end: 5px; } -#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button), +#nav-bar .toolbarbutton-1[type=panel], #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) { padding-left: 5px; padding-right: 5px; } #nav-bar .toolbarbutton-1 > menupopup { margin-top: -3px; } @@ -766,18 +766,18 @@ toolbarbutton[constrain-size="true"][cui width: 16px; } #nav-bar toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-icon { /* XXXgijs box models strike again: this is 16px + 2 * 7px padding + 2 * 1px border (from the rules above) */ width: 32px; } -#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon, -#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container, +#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-icon, +#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-badge-container, #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon, #nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container, #nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ { -moz-padding-end: 17px; } #nav-bar .toolbarbutton-1 > .toolbarbutton-menu-dropmarker { -moz-margin-start: -15px; @@ -1198,16 +1198,21 @@ toolbarbutton[constrain-size="true"][cui background-clip: padding-box; border: 1px solid ThreeDShadow; } #urlbar { -moz-padding-end: 2px; } +/* overlap the urlbar's border */ +#PopupAutoCompleteRichResult { + margin-top: -1px; +} + @media (-moz-os-version: windows-xp), (-moz-os-version: windows-vista), (-moz-os-version: windows-win7) { #urlbar, .searchbar-textbox { border-radius: 2px; } } @@ -1241,16 +1246,21 @@ toolbarbutton[constrain-size="true"][cui .searchbar-textbox:not(:-moz-lwtheme):hover { border-color: hsl(0,0%,80%); } #urlbar:not(:-moz-lwtheme)[focused], .searchbar-textbox:not(:-moz-lwtheme)[focused] { box-shadow: 0 0 0 1px Highlight inset; } + + /* overlap the urlbar's border and inset box-shadow */ + #PopupAutoCompleteRichResult:not(:-moz-lwtheme) { + margin-top: -2px; + } } @media not all and (-moz-os-version: windows-xp) { #urlbar:not(:-moz-lwtheme)[focused], .searchbar-textbox:not(:-moz-lwtheme)[focused] { border-color: Highlight; } }
--- a/browser/themes/windows/caption-buttons.svg +++ b/browser/themes/windows/caption-buttons.svg @@ -4,52 +4,104 @@ <svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <style> g { stroke: ButtonText; stroke-width: 0.9px; fill: none; } - g:not(#close) { + g:not([id|="close"]) { shape-rendering: crispEdges; } g:not(:target) { display: none; } use:target > g { display: initial; } - [id$="-highlight"] > g { + g.highlight { + stroke-width: 1.9px; + } + + g.themes { + stroke: #fff; + stroke-width: 1.9px; + } + + .outer-stroke { + stroke: #000; + stroke-width: 3.6; + opacity: .75; + } + + .restore-background-window { + stroke-width: .9; + } + + [id$="-highcontrast-hover"] > g { stroke: HighlightText; } [id$="-white"] > g { stroke: #fff; } + </style> <g id="close"> - <line x1="1" y1="1" x2="11" y2="11"/> - <line x1="11" y1="1" x2="1" y2="11"/> + <path d="M1,1 l 10,10 M1,11 l 10,-10"/> </g> <g id="maximize"> <rect x="1.5" y="1.5" width="9" height="9"/> </g> <g id="minimize"> <line x1="1" y1="5.5" x2="11" y2="5.5"/> </g> <g id="restore"> <rect x="1.5" y="3.5" width="7" height="7"/> <polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/> </g> - <use id="close-highlight" xlink:href="#close"/> - <use id="maximize-highlight" xlink:href="#maximize"/> - <use id="minimize-highlight" xlink:href="#minimize"/> - <use id="restore-highlight" xlink:href="#restore"/> <use id="close-white" xlink:href="#close"/> <use id="maximize-white" xlink:href="#maximize"/> <use id="minimize-white" xlink:href="#minimize"/> <use id="restore-white" xlink:href="#restore"/> + + <g id="close-highcontrast" class="highlight"> + <path d="M1,1 l 10,10 M1,11 l 10,-10"/> + </g> + <g id="maximize-highcontrast" class="highlight"> + <rect x="2" y="2" width="8" height="8"/> + </g> + <g id="minimize-highcontrast" class="highlight"> + <line x1="1" y1="6" x2="11" y2="6"/> + </g> + <g id="restore-highcontrast" class="highlight"> + <rect x="2" y="4" width="6" height="6"/> + <polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/> + </g> + + <use id="close-highcontrast-hover" xlink:href="#close-highcontrast"/> + <use id="maximize-highcontrast-hover" xlink:href="#maximize-highcontrast"/> + <use id="minimize-highcontrast-hover" xlink:href="#minimize-highcontrast"/> + <use id="restore-highcontrast-hover" xlink:href="#restore-highcontrast"/> + + <g id="close-themes" class="themes"> + <path d="M1,1 l 10,10 M1,11 l 10,-10" class="outer-stroke" /> + <path d="M1.75,1.75 l 8.5,8.5 M1.75,10.25 l 8.5,-8.5"/> + </g> + <g id="maximize-themes" class="themes"> + <rect x="2" y="2" width="8" height="8" class="outer-stroke"/> + <rect x="2" y="2" width="8" height="8"/> + </g> + <g id="minimize-themes" class="themes"> + <line x1="0" y1="6" x2="12" y2="6" class="outer-stroke"/> + <line x1="1" y1="6" x2="11" y2="6"/> + </g> + <g id="restore-themes" class="themes"> + <path d="M2,4 l 6,0 l 0,6 l -6,0z M2.5,1.5 l 8,0 l 0,8" class="outer-stroke"/> + <rect x="2" y="4" width="6" height="6"/> + <polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/> + </g> </svg>
--- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -2950,139 +2950,26 @@ nsDocShell::GetRecordProfileTimelineMark { *aValue = IsObserved(); return NS_OK; } nsresult nsDocShell::PopProfileTimelineMarkers( JSContext* aCx, - JS::MutableHandle<JS::Value> aProfileTimelineMarkers) -{ - // Looping over all markers gathered so far at the docShell level, whenever a - // START marker is found, look for the corresponding END marker and build a - // {name,start,end} JS object. - // Paint markers are different because paint is handled at root docShell level - // in the information that a paint was done is then stored at each sub - // docShell level but we can only be sure that a paint did happen in a - // docShell if an Layer marker type was recorded too. - - nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers; - SequenceRooter<mozilla::dom::ProfileTimelineMarker> rooter( - aCx, &profileTimelineMarkers); - - if (!IsObserved()) { - if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) { - JS_ClearPendingException(aCx); - return NS_ERROR_UNEXPECTED; - } - return NS_OK; - } - - nsTArray<UniquePtr<TimelineMarker>>& markersStore = mObserved.get()->mTimelineMarkers; - - // If we see an unpaired START, we keep it around for the next call - // to PopProfileTimelineMarkers. We store the kept START objects in - // this array. - nsTArray<UniquePtr<TimelineMarker>> keptMarkers; - - for (uint32_t i = 0; i < markersStore.Length(); ++i) { - UniquePtr<TimelineMarker>& startPayload = markersStore[i]; - const char* startMarkerName = startPayload->GetName(); - - bool hasSeenPaintedLayer = false; - bool isPaint = strcmp(startMarkerName, "Paint") == 0; - - // If we are processing a Paint marker, we append information from - // all the embedded Layer markers to this array. - dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles; - - // If this is a TRACING_TIMESTAMP marker, there's no corresponding "end" - // marker, as it's a single unit of time, not a duration, create the final - // marker here. - if (startPayload->GetMetaData() == TRACING_TIMESTAMP) { - mozilla::dom::ProfileTimelineMarker* marker = - profileTimelineMarkers.AppendElement(); - - marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); - marker->mStart = startPayload->GetTime(); - marker->mEnd = startPayload->GetTime(); - marker->mStack = startPayload->GetStack(); - startPayload->AddDetails(aCx, *marker); - continue; - } - - if (startPayload->GetMetaData() == TRACING_INTERVAL_START) { - bool hasSeenEnd = false; - - // DOM events can be nested, so we must take care when searching - // for the matching end. It doesn't hurt to apply this logic to - // all event types. - uint32_t markerDepth = 0; - - // The assumption is that the devtools timeline flushes markers frequently - // enough for the amount of markers to always be small enough that the - // nested for loop isn't going to be a performance problem. - for (uint32_t j = i + 1; j < markersStore.Length(); ++j) { - UniquePtr<TimelineMarker>& endPayload = markersStore[j]; - const char* endMarkerName = endPayload->GetName(); - - // Look for Layer markers to stream out paint markers. - if (isPaint && strcmp(endMarkerName, "Layer") == 0) { - hasSeenPaintedLayer = true; - endPayload->AddLayerRectangles(layerRectangles); - } - - if (!startPayload->Equals(*endPayload)) { - continue; - } - - // Pair start and end markers. - if (endPayload->GetMetaData() == TRACING_INTERVAL_START) { - ++markerDepth; - } else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) { - if (markerDepth > 0) { - --markerDepth; - } else { - // But ignore paint start/end if no layer has been painted. - if (!isPaint || (isPaint && hasSeenPaintedLayer)) { - mozilla::dom::ProfileTimelineMarker* marker = - profileTimelineMarkers.AppendElement(); - - marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); - marker->mStart = startPayload->GetTime(); - marker->mEnd = endPayload->GetTime(); - marker->mStack = startPayload->GetStack(); - if (isPaint) { - marker->mRectangles.Construct(layerRectangles); - } - startPayload->AddDetails(aCx, *marker); - endPayload->AddDetails(aCx, *marker); - } - - // We want the start to be dropped either way. - hasSeenEnd = true; - - break; - } - } - } - - // If we did not see the corresponding END, keep the START. - if (!hasSeenEnd) { - keptMarkers.AppendElement(Move(markersStore[i])); - markersStore.RemoveElementAt(i); - --i; - } - } - } - - markersStore.SwapElements(keptMarkers); - - if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) { + JS::MutableHandle<JS::Value> aOut) +{ + nsTArray<dom::ProfileTimelineMarker> store; + SequenceRooter<dom::ProfileTimelineMarker> rooter(aCx, &store); + + if (IsObserved()) { + mObserved->PopMarkers(aCx, store); + } + + if (!ToJSValue(aCx, store, aOut)) { JS_ClearPendingException(aCx); return NS_ERROR_UNEXPECTED; } return NS_OK; } nsresult
--- a/docshell/base/timeline/ObservedDocShell.cpp +++ b/docshell/base/timeline/ObservedDocShell.cpp @@ -29,9 +29,106 @@ ObservedDocShell::AddMarker(UniquePtr<Ti } void ObservedDocShell::ClearMarkers() { mTimelineMarkers.Clear(); } +void +ObservedDocShell::PopMarkers(JSContext* aCx, + nsTArray<dom::ProfileTimelineMarker>& aStore) +{ + // If we see an unpaired START, we keep it around for the next call + // to ObservedDocShell::PopMarkers. We store the kept START objects here. + nsTArray<UniquePtr<TimelineMarker>> keptStartMarkers; + + for (uint32_t i = 0; i < mTimelineMarkers.Length(); ++i) { + UniquePtr<TimelineMarker>& startPayload = mTimelineMarkers[i]; + + // If this is a TRACING_TIMESTAMP marker, there's no corresponding END + // as it's a single unit of time, not a duration. + if (startPayload->GetMetaData() == TRACING_TIMESTAMP) { + dom::ProfileTimelineMarker* marker = aStore.AppendElement(); + marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); + marker->mStart = startPayload->GetTime(); + marker->mEnd = startPayload->GetTime(); + marker->mStack = startPayload->GetStack(); + startPayload->AddDetails(aCx, *marker); + continue; + } + + // Whenever a START marker is found, look for the corresponding END + // and build a {name,start,end} JS object. + if (startPayload->GetMetaData() == TRACING_INTERVAL_START) { + bool hasSeenEnd = false; + + // "Paint" markers are different because painting is handled at root + // docshell level. The information that a paint was done is stored at + // sub-docshell level, but we can only be sure that a paint did actually + // happen in if a "Layer" marker was recorded too. + bool startIsPaintType = strcmp(startPayload->GetName(), "Paint") == 0; + bool hasSeenLayerType = false; + + // If we are processing a "Paint" marker, we append information from + // all the embedded "Layer" markers to this array. + dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles; + + // DOM events can be nested, so we must take care when searching + // for the matching end. It doesn't hurt to apply this logic to + // all event types. + uint32_t markerDepth = 0; + + // The assumption is that the devtools timeline flushes markers frequently + // enough for the amount of markers to always be small enough that the + // nested for loop isn't going to be a performance problem. + for (uint32_t j = i + 1; j < mTimelineMarkers.Length(); ++j) { + UniquePtr<TimelineMarker>& endPayload = mTimelineMarkers[j]; + bool endIsLayerType = strcmp(endPayload->GetName(), "Layer") == 0; + + // Look for "Layer" markers to stream out "Paint" markers. + if (startIsPaintType && endIsLayerType) { + hasSeenLayerType = true; + endPayload->AddLayerRectangles(layerRectangles); + } + if (!startPayload->Equals(*endPayload)) { + continue; + } + if (endPayload->GetMetaData() == TRACING_INTERVAL_START) { + ++markerDepth; + continue; + } + if (endPayload->GetMetaData() == TRACING_INTERVAL_END) { + if (markerDepth > 0) { + --markerDepth; + continue; + } + if (!startIsPaintType || (startIsPaintType && hasSeenLayerType)) { + dom::ProfileTimelineMarker* marker = aStore.AppendElement(); + marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); + marker->mStart = startPayload->GetTime(); + marker->mEnd = endPayload->GetTime(); + marker->mStack = startPayload->GetStack(); + if (hasSeenLayerType) { + marker->mRectangles.Construct(layerRectangles); + } + startPayload->AddDetails(aCx, *marker); + endPayload->AddDetails(aCx, *marker); + } + hasSeenEnd = true; + break; + } + } + + // If we did not see the corresponding END, keep the START. + if (!hasSeenEnd) { + keptStartMarkers.AppendElement(Move(mTimelineMarkers[i])); + mTimelineMarkers.RemoveElementAt(i); + --i; + } + } + } + + mTimelineMarkers.SwapElements(keptStartMarkers); +} + } // namespace mozilla
--- a/docshell/base/timeline/ObservedDocShell.h +++ b/docshell/base/timeline/ObservedDocShell.h @@ -10,34 +10,35 @@ #include "GeckoProfiler.h" #include "nsTArray.h" #include "nsRefPtr.h" class nsDocShell; class TimelineMarker; namespace mozilla { +namespace dom { +struct ProfileTimelineMarker; +} // # ObservedDocShell // // A wrapper around a docshell for which docshell-specific markers are // allowed to exist. See TimelineConsumers for register/unregister logic. class ObservedDocShell : public LinkedListElement<ObservedDocShell> { private: nsRefPtr<nsDocShell> mDocShell; + nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers; public: - // FIXME: make this private once all marker-specific logic has been - // moved out of nsDocShell. - nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers; - explicit ObservedDocShell(nsDocShell* aDocShell); nsDocShell* operator*() const { return mDocShell.get(); } void AddMarker(const char* aName, TracingMetadata aMetaData); void AddMarker(UniquePtr<TimelineMarker>&& aMarker); void ClearMarkers(); + void PopMarkers(JSContext* aCx, nsTArray<dom::ProfileTimelineMarker>& aStore); }; } // namespace mozilla #endif /* ObservedDocShell_h_ */
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp +++ b/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp @@ -893,17 +893,17 @@ MediaPipelineFactory::EnsureExternalCode bool enabled = mozilla::Preferences::GetBool("media.navigator.hardware.vp8_encode.acceleration_enabled", false); #else bool enabled = false; #endif if (enabled) { nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1"); if (gfxInfo) { int32_t status; - if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) { + if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, &status))) { if (status != nsIGfxInfo::FEATURE_STATUS_OK) { NS_WARNING("VP8 encoder hardware is not whitelisted: disabling.\n"); } else { VideoEncoder* encoder = nullptr; encoder = MediaCodecVideoCodec::CreateEncoder(MediaCodecVideoCodec::CodecType::CODEC_VP8); if (encoder) { return aConduit.SetExternalSendCodec(aConfig, encoder); } else { @@ -918,21 +918,20 @@ MediaPipelineFactory::EnsureExternalCode bool enabled = mozilla::Preferences::GetBool("media.navigator.hardware.vp8_decode.acceleration_enabled", false); #else bool enabled = false; #endif if (enabled) { nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1"); if (gfxInfo) { int32_t status; - if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) { + if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE, &status))) { if (status != nsIGfxInfo::FEATURE_STATUS_OK) { NS_WARNING("VP8 decoder hardware is not whitelisted: disabling.\n"); } else { - VideoDecoder* decoder; decoder = MediaCodecVideoCodec::CreateDecoder(MediaCodecVideoCodec::CodecType::CODEC_VP8); if (decoder) { return aConduit.SetExternalRecvCodec(aConfig, decoder); } else { return kMediaConduitNoError; } }
--- a/mobile/android/base/AppConstants.java.in +++ b/mobile/android/base/AppConstants.java.in @@ -59,16 +59,18 @@ public class AppConstants { public static final boolean feature20Plus = MIN_SDK_VERSION >= 20 || (MAX_SDK_VERSION >= 20 && Build.VERSION.SDK_INT >= 20); public static final boolean feature21Plus = MIN_SDK_VERSION >= 21 || (MAX_SDK_VERSION >= 21 && Build.VERSION.SDK_INT >= 21); /* * If our MIN_SDK_VERSION is 14 or higher, we must be an ICS device. * If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device. * Otherwise, we need a range check. */ + public static final boolean preM = MAX_SDK_VERSION < 23 || + (MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23 && !Build.VERSION.RELEASE.equals("M")); public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21); public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18); public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17); public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16); public static final boolean preICS = MAX_SDK_VERSION < 14 || (MIN_SDK_VERSION < 14 && Build.VERSION.SDK_INT < 14); public static final boolean preHCMR2 = MAX_SDK_VERSION < 13 || (MIN_SDK_VERSION < 13 && Build.VERSION.SDK_INT < 13); public static final boolean preHCMR1 = MAX_SDK_VERSION < 12 || (MIN_SDK_VERSION < 12 && Build.VERSION.SDK_INT < 12); public static final boolean preHC = MAX_SDK_VERSION < 11 || (MIN_SDK_VERSION < 11 && Build.VERSION.SDK_INT < 11);
--- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -42,16 +42,17 @@ import org.mozilla.gecko.mozglue.GeckoLo import org.mozilla.gecko.mozglue.JNITarget; import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.mozglue.generatorannotations.OptionalGeneratedParameter; import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; import org.mozilla.gecko.overlays.ui.ShareDialog; import org.mozilla.gecko.prompts.PromptService; import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.GeckoRequest; +import org.mozilla.gecko.util.HardwareCodecCapabilityUtils; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeJSContainer; import org.mozilla.gecko.util.NativeJSObject; import org.mozilla.gecko.util.ProxySelector; import org.mozilla.gecko.util.ThreadUtils; import android.annotation.TargetApi; @@ -87,16 +88,17 @@ import android.hardware.SensorEventListe import android.hardware.SensorManager; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; import android.os.Vibrator; @@ -956,16 +958,26 @@ public class GeckoAppShell Uri uri = aURL.indexOf(':') >= 0 ? Uri.parse(aURL) : new Uri.Builder().scheme(aURL).build(); Intent intent = getOpenURIIntent(getContext(), uri.toString(), "", TextUtils.isEmpty(aAction) ? Intent.ACTION_VIEW : aAction, ""); return getHandlersForIntent(intent); } + @WrapElementForJNI(stubName = "GetHWEncoderCapability") + static boolean getHWEncoderCapability() { + return HardwareCodecCapabilityUtils.getHWEncoderCapability(); + } + + @WrapElementForJNI(stubName = "GetHWDecoderCapability") + static boolean getHWDecoderCapability() { + return HardwareCodecCapabilityUtils.getHWDecoderCapability(); + } + static List<ResolveInfo> queryIntentActivities(Intent intent) { final PackageManager pm = getContext().getPackageManager(); // Exclude any non-exported activities: we can't open them even if we want to! // Bug 1031569 has some details. final ArrayList<ResolveInfo> list = new ArrayList<>(); for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) { if (ri.activityInfo.exported) {
--- a/mobile/android/base/home/BrowserSearch.java +++ b/mobile/android/base/home/BrowserSearch.java @@ -232,16 +232,18 @@ public class BrowserSearch extends HomeF @Override public void onResume() { super.onResume(); // Fetch engines if we need to. if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) { GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); + } else { + updateSearchEngineBar(); } Telemetry.startUISession(TelemetryContract.Session.FRECENCY); } @Override public void onPause() { super.onPause(); @@ -337,20 +339,16 @@ public class BrowserSearch extends HomeF return false; } }); registerForContextMenu(mList); EventDispatcher.getInstance().registerGeckoThreadListener(this, "SearchEngines:Data"); - // If the view backed by this Fragment is being recreated, we will not receive - // a new search engine data event so refresh the new search engine bar's data - // & Views with the data we have. - updateSearchEngineBar(); mSearchEngineBar.setOnSearchBarClickListener(this); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Initialize the search adapter
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -78,16 +78,17 @@ gujar.sources += [ 'util/EventCallback.java', 'util/FileUtils.java', 'util/FloatUtils.java', 'util/GamepadUtils.java', 'util/GeckoBackgroundThread.java', 'util/GeckoEventListener.java', 'util/GeckoJarReader.java', 'util/GeckoRequest.java', + 'util/HardwareCodecCapabilityUtils.java', 'util/HardwareUtils.java', 'util/INIParser.java', 'util/INISection.java', 'util/InputOptionsUtils.java', 'util/IOUtils.java', 'util/JSONUtils.java', 'util/MenuUtils.java', 'util/NativeEventListener.java',
--- a/mobile/android/base/preferences/AndroidImportPreference.java +++ b/mobile/android/base/preferences/AndroidImportPreference.java @@ -1,15 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.preferences; +import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.R; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.RestrictedProfiles; import org.mozilla.gecko.RestrictedProfiles.Restriction; import java.util.Set; import android.app.ProgressDialog; @@ -21,17 +22,18 @@ import android.util.Log; class AndroidImportPreference extends MultiPrefMultiChoicePreference { private static final String LOGTAG = "AndroidImport"; public static final String PREF_KEY = "android.not_a_preference.import_android"; private static final String PREF_KEY_PREFIX = "import_android.data."; private final Context mContext; public static class Handler implements GeckoPreferences.PrefHandler { public boolean setupPref(Context context, Preference pref) { - return RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS); + // Feature disabled on devices running Android M+ (Bug 1183559) + return Versions.preM && RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS); } public void onChange(Context context, Preference pref, Object newValue) { } } public AndroidImportPreference(Context context, AttributeSet attrs) { super(context, attrs); mContext = context;
--- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -826,17 +826,17 @@ OnSharedPreferenceChangeListener } else if (PREFS_VOICE_INPUT_ENABLED.equals(key)) { if (!InputOptionsUtils.supportsVoiceRecognizer(getApplicationContext(), getResources().getString(R.string.voicesearch_prompt))) { // Remove UI for voice input on non nightly builds. preferences.removePreference(pref); i--; continue; } } else if (PREFS_QRCODE_ENABLED.equals(key)) { - if (!AppConstants.NIGHTLY_BUILD || !InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) { + if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) { // Remove UI for qr code input on non nightly builds preferences.removePreference(pref); i--; continue; } } // Some Preference UI elements are not actually preferences,
--- a/mobile/android/base/toolbar/ToolbarEditLayout.java +++ b/mobile/android/base/toolbar/ToolbarEditLayout.java @@ -248,20 +248,16 @@ public class ToolbarEditLayout extends T final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT); } }); } private boolean qrCodeIsEnabled(Context context) { - // QR code is enabled for nightly only - if(!AppConstants.NIGHTLY_BUILD) { - return false; - } final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context); if (!qrCodeIsSupported) { return false; } return GeckoSharedPrefs.forApp(context) .getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true); }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/util/HardwareCodecCapabilityUtils.java @@ -0,0 +1,143 @@ +/* -*- 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.util; + +import org.mozilla.gecko.AppConstants.Versions; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecList; +import android.util.Log; + +public final class HardwareCodecCapabilityUtils { + private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils"; + + // List of supported HW VP8 encoders. + private static final String[] supportedVp8HwEncCodecPrefixes = + {"OMX.qcom.", "OMX.Intel." }; + // List of supported HW VP8 decoders. + private static final String[] supportedVp8HwDecCodecPrefixes = + {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." }; + private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8"; + // NV12 color format supported by QCOM codec, but not declared in MediaCodec - + // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h + private static final int + COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04; + // Allowable color formats supported by codec - in order of preference. + private static final int[] supportedColorList = { + CodecCapabilities.COLOR_FormatYUV420Planar, + CodecCapabilities.COLOR_FormatYUV420SemiPlanar, + CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, + COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m + }; + + + public static boolean getHWEncoderCapability() { + if (Versions.feature20Plus) { + for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { + MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); + if (!info.isEncoder()) { + continue; + } + String name = null; + for (String mimeType : info.getSupportedTypes()) { + if (mimeType.equals(VP8_MIME_TYPE)) { + name = info.getName(); + break; + } + } + if (name == null) { + continue; // No HW support in this codec; try the next one. + } + Log.e(LOGTAG, "Found candidate encoder " + name); + + // Check if this is supported encoder. + boolean supportedCodec = false; + for (String codecPrefix : supportedVp8HwEncCodecPrefixes) { + if (name.startsWith(codecPrefix)) { + supportedCodec = true; + break; + } + } + if (!supportedCodec) { + continue; + } + + // Check if codec supports either yuv420 or nv12. + CodecCapabilities capabilities = + info.getCapabilitiesForType(VP8_MIME_TYPE); + for (int colorFormat : capabilities.colorFormats) { + Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat)); + } + for (int supportedColorFormat : supportedColorList) { + for (int codecColorFormat : capabilities.colorFormats) { + if (codecColorFormat == supportedColorFormat) { + // Found supported HW Encoder. + Log.e(LOGTAG, "Found target encoder " + name + + ". Color: 0x" + Integer.toHexString(codecColorFormat)); + return true; + } + } + } + } + } + // No HW encoder. + return false; + } + + public static boolean getHWDecoderCapability() { + if (Versions.feature20Plus) { + for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { + MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); + if (info.isEncoder()) { + continue; + } + String name = null; + for (String mimeType : info.getSupportedTypes()) { + if (mimeType.equals(VP8_MIME_TYPE)) { + name = info.getName(); + break; + } + } + if (name == null) { + continue; // No HW support in this codec; try the next one. + } + Log.e(LOGTAG, "Found candidate decoder " + name); + + // Check if this is supported decoder. + boolean supportedCodec = false; + for (String codecPrefix : supportedVp8HwDecCodecPrefixes) { + if (name.startsWith(codecPrefix)) { + supportedCodec = true; + break; + } + } + if (!supportedCodec) { + continue; + } + + // Check if codec supports either yuv420 or nv12. + CodecCapabilities capabilities = + info.getCapabilitiesForType(VP8_MIME_TYPE); + for (int colorFormat : capabilities.colorFormats) { + Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat)); + } + for (int supportedColorFormat : supportedColorList) { + for (int codecColorFormat : capabilities.colorFormats) { + if (codecColorFormat == supportedColorFormat) { + // Found supported HW decoder. + Log.e(LOGTAG, "Found target decoder " + name + + ". Color: 0x" + Integer.toHexString(codecColorFormat)); + return true; + } + } + } + } + } + return false; // No HW decoder. + } +}
--- a/mobile/android/base/util/InputOptionsUtils.java +++ b/mobile/android/base/util/InputOptionsUtils.java @@ -33,14 +33,17 @@ public class InputOptionsUtils { public static boolean supportsQrCodeReader(Context context) { final Intent intent = createQRCodeReaderIntent(); return supportsIntent(intent, context); } public static Intent createQRCodeReaderIntent() { // Bug 602818 enables QR code input if you have the particular app below installed in your device - Intent intent = new Intent("com.google.zxing.client.android.SCAN"); + final String appPackage = "com.google.zxing.client.android"; + + Intent intent = new Intent(appPackage + ".SCAN"); + intent.setPackage(appPackage); intent.putExtra("SCAN_MODE", "QR_CODE_MODE"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); return intent; } }
--- a/mobile/android/chrome/content/aboutLogins.js +++ b/mobile/android/chrome/content/aboutLogins.js @@ -213,17 +213,17 @@ let Logins = { let origUsername = this._selectedLogin.username; let origPassword = this._selectedLogin.password; let origDomain = this._selectedLogin.hostname; try { if ((newUsername === origUsername) && (newPassword === origPassword) && (newDomain === origDomain) ) { - gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short"); + gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short"); this._showList(); return; } let logins = Services.logins.findLogins({}, origDomain, origDomain, null); for (let i = 0; i < logins.length; i++) { if (logins[i].username == origUsername) { @@ -235,17 +235,17 @@ let Logins = { Services.logins.addLogin(clone); break; } } } catch (e) { gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), "short"); return; } - gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short"); + gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short"); this._showList(); }, _onPasswordBtn: function () { this._updatePasswordBtn(this._isPasswordBtnInHideMode()); }, _updatePasswordBtn: function (aShouldShow) {
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -1754,23 +1754,31 @@ var BrowserApp = { // Convert document URI into the format used by // nsChannelClassifier::ShouldEnableTrackingProtection // (any scheme turned into https is correct) let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null); if (data.allowContent) { // Add the current host in the 'trackingprotection' consumer of // the permission manager using a normalized URI. This effectively // places this host on the tracking protection white list. - Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION); + if (PrivateBrowsingUtils.isBrowserPrivate(browser)) { + PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl); + } else { + Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION); + } Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1); } else { // Remove the current host from the 'trackingprotection' consumer // of the permission manager. This effectively removes this host // from the tracking protection white list (any list actually). - Services.perms.remove(normalizedUrl, "trackingprotection"); + if (PrivateBrowsingUtils.isBrowserPrivate(browser)) { + PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl); + } else { + Services.perms.remove(normalizedUrl, "trackingprotection"); + } Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2); } } } // Try to use the session history to reload so that framesets are // handled properly. If the window has no session history, fall back // to using the web navigation's reload method.
--- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -475,16 +475,17 @@ @BINPATH@/modules/* #ifdef MOZ_SAFE_BROWSING ; Safe Browsing @BINPATH@/components/nsURLClassifier.manifest @BINPATH@/components/nsUrlClassifierHashCompleter.js @BINPATH@/components/nsUrlClassifierListManager.js @BINPATH@/components/nsUrlClassifierLib.js +@BINPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js @BINPATH@/components/url-classifier.xpt #endif ; GNOME hooks #ifdef MOZ_ENABLE_GNOME_COMPONENT @BINPATH@/components/@DLL_PREFIX@mozgnome@DLL_SUFFIX@ #endif
--- a/mobile/android/locales/en-US/chrome/aboutLogins.properties +++ b/mobile/android/locales/en-US/chrome/aboutLogins.properties @@ -9,17 +9,17 @@ loginsMenu.editLogin=Edit login loginsMenu.delete=Delete loginsDialog.confirmDelete=Delete this login? loginsDialog.copy=Copy loginsDialog.confirm=OK loginsDialog.cancel=Cancel editLogin.fallbackTitle=Edit Login -editLogin.saved=Saved login +editLogin.saved1=Saved login editLogin.couldNotSave=Changes could not be saved loginsDetails.age=Age: %S days loginsDetails.copyFailed=Copy failed loginsDetails.passwordCopied=Password copied loginsDetails.usernameCopied=Username copied
--- a/mobile/android/tests/browser/robocop/StringHelper.java +++ b/mobile/android/tests/browser/robocop/StringHelper.java @@ -129,16 +129,18 @@ public class StringHelper { public final String TABS_LABEL; // Display public final String TEXT_SIZE_LABEL; public final String TITLE_BAR_LABEL = "Title bar"; public final String SCROLL_TITLE_BAR_LABEL; public final String VOICE_INPUT_TITLE_LABEL; public final String VOICE_INPUT_SUMMARY_LABEL; + public final String QRCODE_INPUT_TITLE_LABEL; + public final String QRCODE_INPUT_SUMMARY_LABEL; public final String TEXT_REFLOW_LABEL; public final String CHARACTER_ENCODING_LABEL; public final String PLUGINS_LABEL; // Title bar public final String SHOW_PAGE_TITLE_LABEL = "Show page title"; public final String SHOW_PAGE_ADDRESS_LABEL = "Show page address"; @@ -318,16 +320,18 @@ public class StringHelper { IMPORT_FROM_ANDROID_LABEL = res.getString(R.string.pref_import_android); TABS_LABEL = res.getString(R.string.pref_restore); // Display TEXT_SIZE_LABEL = res.getString(R.string.pref_text_size); SCROLL_TITLE_BAR_LABEL = res.getString(R.string.pref_scroll_title_bar2); VOICE_INPUT_TITLE_LABEL = res.getString(R.string.pref_voice_input); VOICE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_voice_input_summary); + QRCODE_INPUT_TITLE_LABEL = res.getString(R.string.pref_qrcode_enabled); + QRCODE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_qrcode_enabled_summary); TEXT_REFLOW_LABEL = res.getString(R.string.pref_reflow_on_zoom); CHARACTER_ENCODING_LABEL = res.getString(R.string.pref_char_encoding); PLUGINS_LABEL = res.getString(R.string.pref_plugins); // Privacy TRACKING_PROTECTION_LABEL = res.getString(R.string.pref_tracking_protection_title); TRACKING_PROTECTION_PROMPT_TITLE = res.getString(R.string.tracking_protection_prompt_title); TRACKING_PROTECTION_PROMPT_BUTTON = res.getString(R.string.tracking_protection_prompt_action_button);
--- a/mobile/android/tests/browser/robocop/testSettingsMenuItems.java +++ b/mobile/android/tests/browser/robocop/testSettingsMenuItems.java @@ -228,16 +228,22 @@ public class testSettingsMenuItems exten settingsMap.get(PATH_DISPLAY).remove(TITLE_BAR_LABEL_ARR); } // Voice input if (InputOptionsUtils.supportsVoiceRecognizer(this.getActivity().getApplicationContext(), this.getActivity().getResources().getString(R.string.voicesearch_prompt))) { String[] voiceInputUi = { mStringHelper.VOICE_INPUT_TITLE_LABEL, mStringHelper.VOICE_INPUT_SUMMARY_LABEL }; settingsMap.get(PATH_DISPLAY).add(voiceInputUi); } + + // QR Code input + if (InputOptionsUtils.supportsQrCodeReader(this.getActivity().getApplicationContext())) { + String[] qrCodeInputUi = { mStringHelper.QRCODE_INPUT_TITLE_LABEL, mStringHelper.QRCODE_INPUT_SUMMARY_LABEL }; + settingsMap.get(PATH_DISPLAY).add(qrCodeInputUi); + } } public void checkMenuHierarchy(Map<String[], List<String[]>> settingsMap) { // Check the items within each category. String section = null; for (Entry<String[], List<String[]>> e : settingsMap.entrySet()) { final String[] menuPath = e.getKey();
--- a/netwerk/base/nsChannelClassifier.cpp +++ b/netwerk/base/nsChannelClassifier.cpp @@ -14,16 +14,17 @@ #include "nsIDocShell.h" #include "nsIDocument.h" #include "nsIDOMDocument.h" #include "nsIDOMWindow.h" #include "nsIHttpChannelInternal.h" #include "nsIIOService.h" #include "nsIParentChannel.h" #include "nsIPermissionManager.h" +#include "nsIPrivateBrowsingTrackingProtectionWhitelist.h" #include "nsIProtocolHandler.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsISecureBrowserUI.h" #include "nsISecurityEventSink.h" #include "nsIURL.h" #include "nsIWebProgressListener.h" #include "nsPIDOMWindow.h" @@ -158,16 +159,35 @@ nsChannelClassifier::ShouldEnableTrackin if (permissions == nsIPermissionManager::ALLOW_ACTION) { mIsAllowListed = true; *result = false; } else { *result = true; } + // In Private Browsing Mode we also check against an in-memory list. + if (NS_UsePrivateBrowsing(aChannel)) { + nsCOMPtr<nsIPrivateBrowsingTrackingProtectionWhitelist> pbmtpWhitelist = + do_GetService(NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists = false; + rv = pbmtpWhitelist->ExistsInAllowList(topWinURI, &exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + mIsAllowListed = true; + LOG(("nsChannelClassifier[%p]: Allowlisting channel[%p] in PBM for %s", + this, aChannel, escaped.get())); + } + + *result = !exists; + } + // Tracking protection will be enabled so return without updating // the security state. If any channels are subsequently cancelled // (page elements blocked) the state will be then updated. if (*result) { #ifdef DEBUG nsCString topspec; nsCString spec; topWinURI->GetSpec(topspec);
--- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -67,35 +67,33 @@ let publicProperties = [ // somePromiseBasedFunction: function() { // let currentState = this.currentAccountState; // return someOtherPromiseFunction().then( // data => currentState.resolve(data) // ); // } // If the state has changed between the function being called and the promise // being resolved, the .resolve() call will actually be rejected. -let AccountState = this.AccountState = function(fxaInternal, storageManager) { - this.fxaInternal = fxaInternal; +let AccountState = this.AccountState = function(storageManager) { this.storageManager = storageManager; this.promiseInitialized = this.storageManager.getAccountData().then(data => { this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {}; }).catch(err => { log.error("Failed to initialize the storage manager", err); // Things are going to fall apart, but not much we can do about it here. }); }; AccountState.prototype = { - cert: null, - keyPair: null, oauthTokens: null, whenVerifiedDeferred: null, whenKeysReadyDeferred: null, - get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, + // If the storage manager has been nuked then we are no longer current. + get isCurrent() this.storageManager != null, abort() { if (this.whenVerifiedDeferred) { this.whenVerifiedDeferred.reject( new Error("Verification aborted; Another user signing in")); this.whenVerifiedDeferred = null; } @@ -103,17 +101,16 @@ AccountState.prototype = { this.whenKeysReadyDeferred.reject( new Error("Verification aborted; Another user signing in")); this.whenKeysReadyDeferred = null; } this.cert = null; this.keyPair = null; this.oauthTokens = null; - this.fxaInternal = null; // Avoid finalizing the storageManager multiple times (ie, .signOut() // followed by .abort()) if (!this.storageManager) { return Promise.resolve(); } let storageManager = this.storageManager; this.storageManager = null; return storageManager.finalize(); @@ -126,92 +123,35 @@ AccountState.prototype = { this.oauthTokens = null; let storageManager = this.storageManager; this.storageManager = null; return storageManager.deleteAccountData().then(() => { return storageManager.finalize(); }); }, - getUserAccountData() { + // Get user account data. Optionally specify explcit field names to fetch + // (and note that if you require an in-memory field you *must* specify the + // field name(s).) + getUserAccountData(fieldNames = null) { if (!this.isCurrent) { return Promise.reject(new Error("Another user has signed in")); } - return this.storageManager.getAccountData().then(result => { + return this.storageManager.getAccountData(fieldNames).then(result => { return this.resolve(result); }); }, updateUserAccountData(updatedFields) { if (!this.isCurrent) { return Promise.reject(new Error("Another user has signed in")); } return this.storageManager.updateAccountData(updatedFields); }, - getCertificate: function(data, keyPair, mustBeValidUntil) { - // TODO: get the lifetime from the cert's .exp field - if (this.cert && this.cert.validUntil > mustBeValidUntil) { - log.debug(" getCertificate already had one"); - return this.resolve(this.cert.cert); - } - - if (Services.io.offline) { - return this.reject(new Error(ERROR_OFFLINE)); - } - - let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME; - return this.fxaInternal.getCertificateSigned(data.sessionToken, - keyPair.serializedPublicKey, - CERT_LIFETIME).then( - cert => { - log.debug("getCertificate got a new one: " + !!cert); - this.cert = { - cert: cert, - validUntil: willBeValidUntil - }; - return cert; - } - ).then(result => this.resolve(result)); - }, - - getKeyPair: function(mustBeValidUntil) { - // If the debugging pref to ignore cached authentication credentials is set for Sync, - // then don't use any cached key pair, i.e., generate a new one and get it signed. - // The purpose of this pref is to expedite any auth errors as the result of a - // expired or revoked FxA session token, e.g., from resetting or changing the FxA - // password. - let ignoreCachedAuthCredentials = false; - try { - ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials"); - } catch(e) { - // Pref doesn't exist - } - if (!ignoreCachedAuthCredentials && this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) { - log.debug("getKeyPair: already have a keyPair"); - return this.resolve(this.keyPair.keyPair); - } - // Otherwse, create a keypair and set validity limit. - let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME; - let d = Promise.defer(); - jwcrypto.generateKeyPair("DS160", (err, kp) => { - if (err) { - return this.reject(err); - } - this.keyPair = { - keyPair: kp, - validUntil: willBeValidUntil - }; - log.debug("got keyPair"); - delete this.cert; - d.resolve(this.keyPair.keyPair); - }); - return d.promise.then(result => this.resolve(result)); - }, - resolve: function(result) { if (!this.isCurrent) { log.info("An accountState promise was resolved, but was actually rejected" + " due to a different user being signed in. Originally resolved" + " with", result); return Promise.reject(new Error("A different user signed in")); } return Promise.resolve(result); @@ -422,17 +362,17 @@ FxAccountsInternal.prototype = { } return this._profile; }, // A hook-point for tests who may want a mocked AccountState or mocked storage. newAccountState(credentials) { let storage = new FxAccountsStorageManager(); storage.initialize(credentials); - return new AccountState(this, storage); + return new AccountState(storage); }, /** * Return the current time in milliseconds as an integer. Allows tests to * manipulate the date to simulate certificate expiration. */ now: function() { return this.fxAccountsClient.now(); @@ -555,34 +495,80 @@ FxAccountsInternal.prototype = { } }).then(() => { return currentAccountState.resolve(); }); }) }, /** + * returns a promise that fires with the keypair. + */ + getKeyPair: Task.async(function* (mustBeValidUntil) { + // If the debugging pref to ignore cached authentication credentials is set for Sync, + // then don't use any cached key pair, i.e., generate a new one and get it signed. + // The purpose of this pref is to expedite any auth errors as the result of a + // expired or revoked FxA session token, e.g., from resetting or changing the FxA + // password. + let ignoreCachedAuthCredentials = false; + try { + ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials"); + } catch(e) { + // Pref doesn't exist + } + let currentState = this.currentAccountState; + let accountData = yield currentState.getUserAccountData("keyPair"); + if (!ignoreCachedAuthCredentials && accountData.keyPair && (accountData.keyPair.validUntil > mustBeValidUntil)) { + log.debug("getKeyPair: already have a keyPair"); + return accountData.keyPair.keyPair; + } + // Otherwse, create a keypair and set validity limit. + let willBeValidUntil = this.now() + KEY_LIFETIME; + let kp = yield new Promise((resolve, reject) => { + jwcrypto.generateKeyPair("DS160", (err, kp) => { + if (err) { + return reject(err); + } + log.debug("got keyPair"); + let toUpdate = { + keyPair: { + keyPair: kp, + validUntil: willBeValidUntil + }, + cert: null + }; + currentState.updateUserAccountData(toUpdate).then(() => { + resolve(kp); + }).catch(err => { + log.error("Failed to update account data with keypair and cert"); + }); + }); + }); + return kp; + }), + + /** * returns a promise that fires with the assertion. If there is no verified * signed-in user, fires with null. */ getAssertion: function getAssertion(audience) { log.debug("enter getAssertion()"); let currentState = this.currentAccountState; let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; return currentState.getUserAccountData().then(data => { if (!data) { // No signed-in user return null; } if (!this.isUserEmailVerified(data)) { // Signed-in user has not verified email return null; } - return currentState.getKeyPair(mustBeValidUntil).then(keyPair => { - return currentState.getCertificate(data, keyPair, mustBeValidUntil) + return this.getKeyPair(mustBeValidUntil).then(keyPair => { + return this.getCertificate(data, keyPair, mustBeValidUntil) .then(cert => { return this.getAssertionFromCert(data, keyPair, cert, audience); }); }); }).then(result => currentState.resolve(result)); }, /** @@ -840,16 +826,47 @@ FxAccountsInternal.prototype = { } return this.fxAccountsClient.signCertificate( sessionToken, JSON.parse(serializedPublicKey), lifetime ); }, + /** + * returns a promise that fires with a certificate. + */ + getCertificate: Task.async(function* (data, keyPair, mustBeValidUntil) { + // TODO: get the lifetime from the cert's .exp field + let currentState = this.currentAccountState; + let accountData = yield currentState.getUserAccountData("cert"); + if (accountData.cert && accountData.cert.validUntil > mustBeValidUntil) { + log.debug(" getCertificate already had one"); + return accountData.cert.cert; + } + if (Services.io.offline) { + throw new Error(ERROR_OFFLINE); + } + let willBeValidUntil = this.now() + CERT_LIFETIME; + let cert = yield this.getCertificateSigned(data.sessionToken, + keyPair.serializedPublicKey, + CERT_LIFETIME); + log.debug("getCertificate got a new one: " + !!cert); + if (cert) { + let toUpdate = { + cert: { + cert: cert, + validUntil: willBeValidUntil + } + }; + yield currentState.updateUserAccountData(toUpdate); + } + return cert; + }), + getUserAccountData: function() { return this.currentAccountState.getUserAccountData(); }, isUserEmailVerified: function isUserEmailVerified(data) { return !!(data && data.verified); },
--- a/services/fxaccounts/FxAccountsCommon.js +++ b/services/fxaccounts/FxAccountsCommon.js @@ -207,23 +207,32 @@ exports.ERROR_AUTH_ERROR exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER"; // Status code errors exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405; exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED"; // FxAccounts has the ability to "split" the credentials between a plain-text // JSON file in the profile dir and in the login manager. -// These constants relate to that. +// In order to prevent new fields accidentally ending up in the "wrong" place, +// all fields stored are listed here. // The fields we save in the plaintext JSON. // See bug 1013064 comments 23-25 for why the sessionToken is "safe" -exports.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt", - "sessionToken", "uid", "oauthTokens", - "profile"]; +exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set( + ["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile"]); + +// Fields we store in secure storage if it exists. +exports.FXA_PWDMGR_SECURE_FIELDS = new Set( + ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]); + +// Fields we keep in memory and don't persist anywhere. +exports.FXA_PWDMGR_MEMORY_FIELDS = new Set( + ["cert", "keyPair"]); + // The pseudo-host we use in the login manager exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts"; // The realm we use in the login manager. exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials"; // Error matching. exports.SERVER_ERRNO_TO_ERROR = {};
--- a/services/fxaccounts/FxAccountsStorage.jsm +++ b/services/fxaccounts/FxAccountsStorage.jsm @@ -57,20 +57,28 @@ this.FxAccountsStorageManager.prototype _initialize: Task.async(function* (accountData) { log.trace("initializing new storage manager"); try { if (accountData) { // If accountData is passed we don't need to read any storage. this._needToReadSecure = false; // split it into the 2 parts, write it and we are done. for (let [name, val] of Iterator(accountData)) { - if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) { + if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { this.cachedPlain[name] = val; + } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { + this.cachedSecure[name] = val; } else { - this.cachedSecure[name] = val; + // Hopefully it's an "in memory" field. If it's not we log a warning + // but still treat it as such (so it will still be available in this + // session but isn't persisted anywhere.) + if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) { + log.warn("Unknown FxA field name in user data, treating as in-memory", name); + } + this.cachedMemory[name] = val; } } // write it out and we are done. yield this._write(); return; } // So we were initialized without account data - that means we need to // read the state from storage. We try and read plain storage first and @@ -116,40 +124,77 @@ this.FxAccountsStorageManager.prototype // be in a rejected state) this._promiseStorageComplete = result.catch(err => { log.error("${func} failed: ${err}", {func, err}); }); return result; }, // Get the account data by combining the plain and secure storage. - getAccountData: Task.async(function* () { + // If fieldNames is specified, it may be a string or an array of strings, + // and only those fields are returned. If not specified the entire account + // data is returned except for "in memory" fields. Note that not specifying + // field names will soon be deprecated/removed - we want all callers to + // specify the fields they care about. + getAccountData: Task.async(function* (fieldNames = null) { yield this._promiseInitialized; // We know we are initialized - this means our .cachedPlain is accurate // and doesn't need to be read (it was read if necessary by initialize). // So if there's no uid, there's no user signed in. if (!('uid' in this.cachedPlain)) { return null; } let result = {}; - for (let [name, value] of Iterator(this.cachedPlain)) { - result[name] = value; + if (fieldNames === null) { + // The "old" deprecated way of fetching a logged in user. + for (let [name, value] of Iterator(this.cachedPlain)) { + result[name] = value; + } + // But the secure data may not have been read, so try that now. + yield this._maybeReadAndUpdateSecure(); + // .cachedSecure now has as much as it possibly can (which is possibly + // nothing if (a) secure storage remains locked and (b) we've never updated + // a field to be stored in secure storage.) + for (let [name, value] of Iterator(this.cachedSecure)) { + result[name] = value; + } + // Note we don't return cachedMemory fields here - they must be explicitly + // requested. + return result; + } + // The new explicit way of getting attributes. + if (!Array.isArray(fieldNames)) { + fieldNames = [fieldNames]; } - // But the secure data may not have been read, so try that now. - yield this._maybeReadAndUpdateSecure(); - // .cachedSecure now has as much as it possibly can (which is possibly - // nothing if (a) secure storage remains locked and (b) we've never updated - // a field to be stored in secure storage.) - for (let [name, value] of Iterator(this.cachedSecure)) { - result[name] = value; + let checkedSecure = false; + for (let fieldName of fieldNames) { + if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) { + if (this.cachedMemory[fieldName] !== undefined) { + result[fieldName] = this.cachedMemory[fieldName]; + } + } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) { + if (this.cachedPlain[fieldName] !== undefined) { + result[fieldName] = this.cachedPlain[fieldName]; + } + } else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) { + // We may not have read secure storage yet. + if (!checkedSecure) { + yield this._maybeReadAndUpdateSecure(); + checkedSecure = true; + } + if (this.cachedSecure[fieldName] !== undefined) { + result[fieldName] = this.cachedSecure[fieldName]; + } + } else { + throw new Error("unexpected field '" + name + "'"); + } } return result; }), - // Update just the specified fields. This DOES NOT allow you to change to // a different user, nor to set the user as signed-out. updateAccountData: Task.async(function* (newFields) { yield this._promiseInitialized; if (!('uid' in this.cachedPlain)) { // If this storage instance shows no logged in user, then you can't // update fields. throw new Error("No user is logged in"); @@ -158,38 +203,50 @@ this.FxAccountsStorageManager.prototype // Once we support // user changing email address this may need to change, but it's not // clear how we would be told of such a change anyway... throw new Error("Can't change uid or email address"); } log.debug("_updateAccountData with items", Object.keys(newFields)); // work out what bucket. for (let [name, value] of Iterator(newFields)) { - if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) { + if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) { + if (value == null) { + delete this.cachedMemory[name]; + } else { + this.cachedMemory[name] = value; + } + } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { if (value == null) { delete this.cachedPlain[name]; } else { this.cachedPlain[name] = value; } - } else { + } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { // don't do the "delete on null" thing here - we need to keep it until // we have managed to read so we can nuke it on write. this.cachedSecure[name] = value; + } else { + // Throwing seems reasonable here as some client code has explicitly + // specified the field name, so it's either confused or needs to update + // how this field is to be treated. + throw new Error("unexpected field '" + name + "'"); } } // If we haven't yet read the secure data, do so now, else we may write // out partial data. yield this._maybeReadAndUpdateSecure(); // Now save it - but don't wait on the _write promise - it's queued up as // a storage operation, so .finalize() will wait for completion, but no need // for us to. this._write(); }), _clearCachedData() { + this.cachedMemory = {}; this.cachedPlain = {}; // If we don't have secure storage available we have cachedPlain and // cachedSecure be the same object. this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {}; }, /* Reads the plain storage and caches the read values in this.cachedPlain. Only ever called once and unlike the "secure" storage, is expected to never
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -150,17 +150,17 @@ function MockFxAccounts() { _now_is: new Date(), now: function () { return this._now_is; }, newAccountState(credentials) { // we use a real accountState but mocked storage. let storage = new MockStorageManager(); storage.initialize(credentials); - return new AccountState(this, storage); + return new AccountState(storage); }, getCertificateSigned: function (sessionToken, serializedPublicKey) { _("mock getCertificateSigned\n"); this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]); return this._d_signCertificate.promise; }, fxAccountsClient: new MockFxAccountsClient() }); @@ -197,17 +197,17 @@ add_test(function test_non_https_remote_ add_task(function test_get_signed_in_user_initially_unset() { // This test, unlike many of the the rest, uses a (largely) un-mocked // FxAccounts instance. let account = new FxAccounts({ newAccountState(credentials) { // we use a real accountState but mocked storage. let storage = new MockStorageManager(); storage.initialize(credentials); - return new AccountState(this, storage); + return new AccountState(storage); }, }); let credentials = { email: "foo@example.com", uid: "1234@lcip.org", assertion: "foobar", sessionToken: "dead", kA: "beef", @@ -246,17 +246,17 @@ add_task(function* test_getCertificate() // This test, unlike many of the the rest, uses a (largely) un-mocked // FxAccounts instance. // We do mock the storage to keep the test fast on b2g. let fxa = new FxAccounts({ newAccountState(credentials) { // we use a real accountState but mocked storage. let storage = new MockStorageManager(); storage.initialize(credentials); - return new AccountState(this, storage); + return new AccountState(storage); }, }); let credentials = { email: "foo@example.com", uid: "1234@lcip.org", assertion: "foobar", sessionToken: "dead", kA: "beef", @@ -267,17 +267,17 @@ add_task(function* test_getCertificate() // Test that an expired cert throws if we're offline. fxa.internal.currentAccountState.cert = { validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT") }; let offline = Services.io.offline; Services.io.offline = true; // This call would break from missing parameters ... - yield fxa.internal.currentAccountState.getCertificate().then( + yield fxa.internal.getCertificate().then( result => { Services.io.offline = offline; do_throw("Unexpected success"); }, err => { Services.io.offline = offline; // ... so we have to check the error string. do_check_eq(err, "Error: OFFLINE"); @@ -500,18 +500,19 @@ add_task(function test_getAssertion() { fxa.internal._d_signCertificate.resolve("cert1"); let assertion = yield d; do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken"); do_check_neq(assertion, null); _("ASSERTION: " + assertion + "\n"); let pieces = assertion.split("~"); do_check_eq(pieces[0], "cert1"); - let keyPair = fxa.internal.currentAccountState.keyPair; - let cert = fxa.internal.currentAccountState.cert; + let userData = yield fxa.getSignedInUser(); + let keyPair = userData.keyPair; + let cert = userData.cert; do_check_neq(keyPair, undefined); _(keyPair.validUntil + "\n"); let p2 = pieces[1].split("."); let header = JSON.parse(atob(p2[0])); _("HEADER: " + JSON.stringify(header) + "\n"); do_check_eq(header.alg, "DS128"); let payload = JSON.parse(atob(p2[1])); _("PAYLOAD: " + JSON.stringify(payload) + "\n"); @@ -548,19 +549,20 @@ add_task(function test_getAssertion() { header = JSON.parse(atob(p2[0])); payload = JSON.parse(atob(p2[1])); do_check_eq(payload.aud, "third.example.com"); // The keypair and cert should have the same validity as before, but the // expiration time of the assertion should be different. We compare this to // the initial start time, to which they are relative, not the current value // of "now". + userData = yield fxa.getSignedInUser(); - keyPair = fxa.internal.currentAccountState.keyPair; - cert = fxa.internal.currentAccountState.cert; + keyPair = userData.keyPair; + cert = userData.cert; do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); do_check_eq(cert.validUntil, start + CERT_LIFETIME); exp = Number(payload.exp); do_check_eq(exp, now + ASSERTION_LIFETIME); // Now we wait even longer, and expect both assertion and cert to expire. So // we will have to get a new keypair and cert. now += ONE_DAY_MS; @@ -571,18 +573,19 @@ add_task(function test_getAssertion() { do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2); do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken"); pieces = assertion.split("~"); do_check_eq(pieces[0], "cert2"); p2 = pieces[1].split("."); header = JSON.parse(atob(p2[0])); payload = JSON.parse(atob(p2[1])); do_check_eq(payload.aud, "fourth.example.com"); - keyPair = fxa.internal.currentAccountState.keyPair; - cert = fxa.internal.currentAccountState.cert; + userData = yield fxa.getSignedInUser(); + keyPair = userData.keyPair; + cert = userData.cert; do_check_eq(keyPair.validUntil, now + KEY_LIFETIME); do_check_eq(cert.validUntil, now + CERT_LIFETIME); exp = Number(payload.exp); do_check_eq(exp, now + ASSERTION_LIFETIME); _("----- DONE ----\n"); });
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js +++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js @@ -80,17 +80,17 @@ MockFxAccountsClient.prototype = { function MockFxAccounts() { return new FxAccounts({ fxAccountsClient: new MockFxAccountsClient(), newAccountState(credentials) { // we use a real accountState but mocked storage. let storage = new MockStorageManager(); storage.initialize(credentials); - return new AccountState(this, storage); + return new AccountState(storage); }, }); } function* createMockFxA() { let fxa = new MockFxAccounts(); let credentials = { email: "foo@example.com",
--- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js +++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js @@ -46,19 +46,21 @@ function MockedSecureStorage(accountData accountData: accountData, } } this.data = data; this.numReads = 0; } MockedSecureStorage.prototype = { + fetchCount: 0, locked: false, STORAGE_LOCKED: function() {}, get: Task.async(function* (uid, email) { + this.fetchCount++; if (this.locked) { throw new this.STORAGE_LOCKED(); } this.numReads++; Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data"); return this.data; }), @@ -80,17 +82,17 @@ function add_storage_task(testFunction) // initialized without account data and there's nothing to read. Not logged in. add_storage_task(function* checkInitializedEmpty(sm) { if (sm.secureStorage) { sm.secureStorage = new MockedSecureStorage(null); } yield sm.initialize(); Assert.strictEqual((yield sm.getAccountData()), null); - Assert.rejects(sm.updateAccountData({foo: "bar"}), "No user is logged in") + Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in") }); // Initialized with account data (ie, simulating a new user being logged in). // Should reflect the initial data and be written to storage. add_storage_task(function* checkNewUser(sm) { let initialAccountData = { uid: "uid", email: "someone@somewhere.com", @@ -125,31 +127,31 @@ add_storage_task(function* checkEverythi } yield sm.initialize(); let accountData = yield sm.getAccountData(); Assert.ok(accountData, "read account data"); Assert.equal(accountData.uid, "uid"); Assert.equal(accountData.email, "someone@somewhere.com"); // Update the data - we should be able to fetch it back and it should appear // in our storage. - yield sm.updateAccountData({verified: true, foo: "bar", kA: "kA"}); + yield sm.updateAccountData({verified: true, kA: "kA", kB: "kB"}); accountData = yield sm.getAccountData(); - Assert.equal(accountData.foo, "bar"); + Assert.equal(accountData.kB, "kB"); Assert.equal(accountData.kA, "kA"); // Check the new value was written to storage. yield sm._promiseStorageComplete; // storage is written in the background. // "verified" is a plain-text field. Assert.equal(sm.plainStorage.data.accountData.verified, true); // "kA" and "foo" are secure if (sm.secureStorage) { Assert.equal(sm.secureStorage.data.accountData.kA, "kA"); - Assert.equal(sm.secureStorage.data.accountData.foo, "bar"); + Assert.equal(sm.secureStorage.data.accountData.kB, "kB"); } else { Assert.equal(sm.plainStorage.data.accountData.kA, "kA"); - Assert.equal(sm.plainStorage.data.accountData.foo, "bar"); + Assert.equal(sm.plainStorage.data.accountData.kB, "kB"); } }); add_storage_task(function* checkInvalidUpdates(sm) { sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) if (sm.secureStorage) { sm.secureStorage = new MockedSecureStorage(null); } @@ -226,16 +228,63 @@ add_task(function* checkEverythingReadSe let accountData = yield sm.getAccountData(); Assert.ok(accountData, "read account data"); Assert.equal(accountData.uid, "uid"); Assert.equal(accountData.email, "someone@somewhere.com"); Assert.equal(accountData.kA, "kA"); }); +add_task(function* checkMemoryFieldsNotReturnedByDefault() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + // keyPair is a memory field. + yield sm.updateAccountData({keyPair: "the keypair value"}); + let accountData = yield sm.getAccountData(); + + // Requesting everything should *not* return in memory fields. + Assert.strictEqual(accountData.keyPair, undefined); + // But requesting them specifically does get them. + accountData = yield sm.getAccountData("keyPair"); + Assert.strictEqual(accountData.keyPair, "the keypair value"); +}); + +add_task(function* checkExplicitGet() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + let accountData = yield sm.getAccountData(["uid", "kA"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.kA, "kA"); + // We didn't ask for email so shouldn't have got it. + Assert.strictEqual(accountData.email, undefined); +}); + +add_task(function* checkExplicitGetNoSecureRead() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) + sm.secureStorage = new MockedSecureStorage({kA: "kA"}); + yield sm.initialize(); + + Assert.equal(sm.secureStorage.fetchCount, 0); + // request 2 fields in secure storage - it should have caused a single fetch. + let accountData = yield sm.getAccountData(["email", "uid"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.strictEqual(accountData.kA, undefined); + Assert.equal(sm.secureStorage.fetchCount, 1); +}); + add_task(function* checkLockedUpdates() { let sm = new FxAccountsStorageManager(); sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"}); sm.secureStorage.locked = true; yield sm.initialize(); let accountData = yield sm.getAccountData();
--- a/services/sync/modules-testing/utils.js +++ b/services/sync/modules-testing/utils.js @@ -176,27 +176,27 @@ this.configureFxAccountIdentity = functi newAccountState(credentials) { // We only expect this to be called with null indicating the (mock) // storage should be read. if (credentials) { throw new Error("Not expecting to have credentials passed"); } let storageManager = new MockFxaStorageManager(); storageManager.initialize(config.fxaccount.user); - let accountState = new AccountState(this, storageManager); - // mock getCertificate - accountState.getCertificate = function(data, keyPair, mustBeValidUntil) { - accountState.cert = { - validUntil: fxa.internal.now() + CERT_LIFETIME, - cert: "certificate", - }; - return Promise.resolve(this.cert.cert); - } + let accountState = new AccountState(storageManager); return accountState; - } + }, + getCertificate(data, keyPair, mustBeValidUntil) { + let cert = { + validUntil: this.now() + CERT_LIFETIME, + cert: "certificate", + }; + this.currentAccountState.updateUserAccountData({cert: cert}); + return Promise.resolve(cert.cert); + }, }; fxa = new FxAccounts(MockInternal); let mockTSC = { // TokenServerClient getTokenFromBrowserIDAssertion: function(uri, assertion, cb) { config.fxaccount.token.uid = config.username; cb(null, config.fxaccount.token); },
--- a/services/sync/tests/unit/test_browserid_identity.js +++ b/services/sync/tests/unit/test_browserid_identity.js @@ -590,17 +590,17 @@ add_task(function test_getKeysMissing() newAccountState(credentials) { // We only expect this to be called with null indicating the (mock) // storage should be read. if (credentials) { throw new Error("Not expecting to have credentials passed"); } let storageManager = new MockFxaStorageManager(); storageManager.initialize(identityConfig.fxaccount.user); - return new AccountState(this, storageManager); + return new AccountState(storageManager); }, }); // Add a mock to the currentAccountState object. fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { this.cert = { validUntil: fxa.internal.now() + CERT_LIFETIME, cert: "certificate", @@ -669,17 +669,17 @@ function* initializeIdentityWithHAWKResp newAccountState(credentials) { // We only expect this to be called with null indicating the (mock) // storage should be read. if (credentials) { throw new Error("Not expecting to have credentials passed"); } let storageManager = new MockFxaStorageManager(); storageManager.initialize(config.fxaccount.user); - return new AccountState(this, storageManager); + return new AccountState(storageManager); }, } let fxa = new FxAccounts(internal); browseridManager._fxaService = fxa; browseridManager._signedInUser = null; yield browseridManager.initializeWithCurrentIdentity(); yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
--- a/toolkit/components/places/AsyncFaviconHelpers.cpp +++ b/toolkit/components/places/AsyncFaviconHelpers.cpp @@ -343,17 +343,17 @@ OptimizeIconSize(IconData& aIcon, } // namespace //////////////////////////////////////////////////////////////////////////////// //// AsyncFaviconHelperBase AsyncFaviconHelperBase::AsyncFaviconHelperBase( nsCOMPtr<nsIFaviconDataCallback>& aCallback -) : mDB(Database::GetDatabase()) +) { // Don't AddRef or Release in runnables for thread-safety. mCallback.swap(aCallback); } AsyncFaviconHelperBase::~AsyncFaviconHelperBase() { nsCOMPtr<nsIThread> thread; @@ -446,30 +446,32 @@ AsyncFetchAndSetIconForPage::~AsyncFetch NS_IMETHODIMP AsyncFetchAndSetIconForPage::Run() { NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread"); // Try to fetch the icon from the database. - nsresult rv = FetchIconInfo(mDB, mIcon); + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); + nsresult rv = FetchIconInfo(DB, mIcon); NS_ENSURE_SUCCESS(rv, rv); bool isInvalidIcon = mIcon.data.IsEmpty() || (mIcon.expiration && PR_Now() > mIcon.expiration); bool fetchIconFromNetwork = mIcon.fetchMode == FETCH_ALWAYS || (mIcon.fetchMode == FETCH_IF_MISSING && isInvalidIcon); if (!fetchIconFromNetwork) { // There is already a valid icon or we don't want to fetch a new one, // directly proceed with association. nsRefPtr<AsyncAssociateIconToPage> event = new AsyncAssociateIconToPage(mIcon, mPage, mCallback); - mDB->DispatchToAsyncThread(event); + DB->DispatchToAsyncThread(event); return NS_OK; } else { // Fetch the icon from network. When done this will associate the // icon to the page and notify. nsRefPtr<AsyncFetchAndSetIconFromNetwork> event = new AsyncFetchAndSetIconFromNetwork(mIcon, mPage, mFaviconLoadPrivate, mCallback); @@ -690,19 +692,21 @@ AsyncFetchAndSetIconFromNetwork::OnStopR // If over the maximum size allowed, don't save data to the database to // avoid bloating it. if (mIcon.data.Length() > MAX_FAVICON_SIZE) { return NS_OK; } mIcon.status = ICON_STATUS_CHANGED; + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); nsRefPtr<AsyncAssociateIconToPage> event = new AsyncAssociateIconToPage(mIcon, mPage, mCallback); - mDB->DispatchToAsyncThread(event); + DB->DispatchToAsyncThread(event); return NS_OK; } //////////////////////////////////////////////////////////////////////////////// //// AsyncAssociateIconToPage AsyncAssociateIconToPage::AsyncAssociateIconToPage( @@ -720,64 +724,66 @@ AsyncAssociateIconToPage::~AsyncAssociat } NS_IMETHODIMP AsyncAssociateIconToPage::Run() { NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread"); - nsresult rv = FetchPageInfo(mDB, mPage); + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); + nsresult rv = FetchPageInfo(DB, mPage); if (rv == NS_ERROR_NOT_AVAILABLE){ // We have never seen this page. If we can add the page to history, // we will try to do it later, otherwise just bail out. if (!mPage.canAddToHistory) { return NS_OK; } } else { NS_ENSURE_SUCCESS(rv, rv); } - mozStorageTransaction transaction(mDB->MainConn(), false, + mozStorageTransaction transaction(DB->MainConn(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); // If there is no entry for this icon, or the entry is obsolete, replace it. if (mIcon.id == 0 || (mIcon.status & ICON_STATUS_CHANGED)) { - rv = SetIconInfo(mDB, mIcon); + rv = SetIconInfo(DB, mIcon); NS_ENSURE_SUCCESS(rv, rv); // Get the new icon id. Do this regardless mIcon.id, since other code // could have added a entry before us. Indeed we interrupted the thread // after the previous call to FetchIconInfo. mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED; - rv = FetchIconInfo(mDB, mIcon); + rv = FetchIconInfo(DB, mIcon); NS_ENSURE_SUCCESS(rv, rv); } // If the page does not have an id, don't try to insert a new one, cause we // don't know where the page comes from. Not doing so we may end adding // a page that otherwise we'd explicitly ignore, like a POST or an error page. if (mPage.id == 0) { return NS_OK; } // Otherwise just associate the icon to the page, if needed. if (mPage.iconId != mIcon.id) { nsCOMPtr<mozIStorageStatement> stmt; if (mPage.id) { - stmt = mDB->GetStatement( + stmt = DB->GetStatement( "UPDATE moz_places SET favicon_id = :icon_id WHERE id = :page_id" ); NS_ENSURE_STATE(stmt); rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPage.id); NS_ENSURE_SUCCESS(rv, rv); } else { - stmt = mDB->GetStatement( + stmt = DB->GetStatement( "UPDATE moz_places SET favicon_id = :icon_id WHERE url = :page_url" ); NS_ENSURE_STATE(stmt); rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPage.spec); NS_ENSURE_SUCCESS(rv, rv); } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("icon_id"), mIcon.id); NS_ENSURE_SUCCESS(rv, rv); @@ -841,18 +847,20 @@ AsyncGetFaviconURLForPage::~AsyncGetFavi } NS_IMETHODIMP AsyncGetFaviconURLForPage::Run() { NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread."); + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); nsAutoCString iconSpec; - nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec); + nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec); NS_ENSURE_SUCCESS(rv, rv); // Now notify our callback of the icon spec we retrieved, even if empty. IconData iconData; iconData.spec.Assign(iconSpec); PageData pageData; pageData.spec.Assign(mPageSpec); @@ -906,28 +914,30 @@ AsyncGetFaviconDataForPage::~AsyncGetFav } NS_IMETHODIMP AsyncGetFaviconDataForPage::Run() { NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread."); + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); nsAutoCString iconSpec; - nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec); + nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec); NS_ENSURE_SUCCESS(rv, rv); IconData iconData; iconData.spec.Assign(iconSpec); PageData pageData; pageData.spec.Assign(mPageSpec); if (!iconSpec.IsEmpty()) { - rv = FetchIconInfo(mDB, iconData); + rv = FetchIconInfo(DB, iconData); if (NS_FAILED(rv)) { iconData.spec.Truncate(); } } nsCOMPtr<nsIRunnable> event = new NotifyIconObservers(iconData, pageData, mCallback); rv = NS_DispatchToMainThread(event); @@ -970,26 +980,28 @@ AsyncReplaceFaviconData::~AsyncReplaceFa } NS_IMETHODIMP AsyncReplaceFaviconData::Run() { NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread"); + nsRefPtr<Database> DB = Database::GetDatabase(); + NS_ENSURE_STATE(DB); IconData dbIcon; dbIcon.spec.Assign(mIcon.spec); - nsresult rv = FetchIconInfo(mDB, dbIcon); + nsresult rv = FetchIconInfo(DB, dbIcon); NS_ENSURE_SUCCESS(rv, rv); if (!dbIcon.id) { return NS_OK; } - rv = SetIconInfo(mDB, mIcon); + rv = SetIconInfo(DB, mIcon); NS_ENSURE_SUCCESS(rv, rv); // We can invalidate the cache version since we now persist the icon. nsCOMPtr<nsIRunnable> event = new RemoveIconDataCacheEntry(mIcon, mCallback); rv = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; @@ -1091,18 +1103,21 @@ NotifyIconObservers::SendGlobalNotificat // If the page is bookmarked and the bookmarked url is different from the // updated one, start a new task to update its icon as well. if (!mPage.bookmarkedSpec.IsEmpty() && !mPage.bookmarkedSpec.Equals(mPage.spec)) { // Create a new page struct to avoid polluting it with old data. PageData bookmarkedPage; bookmarkedPage.spec = mPage.bookmarkedSpec; + nsRefPtr<Database> DB = Database::GetDatabase(); + if (!DB) + return; // This will be silent, so be sure to not pass in the current callback. nsCOMPtr<nsIFaviconDataCallback> nullCallback; nsRefPtr<AsyncAssociateIconToPage> event = new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback); - mDB->DispatchToAsyncThread(event); + DB->DispatchToAsyncThread(event); } } } // namespace places } // namespace mozilla
--- a/toolkit/components/places/AsyncFaviconHelpers.h +++ b/toolkit/components/places/AsyncFaviconHelpers.h @@ -97,17 +97,16 @@ struct PageData */ class AsyncFaviconHelperBase : public nsRunnable { protected: explicit AsyncFaviconHelperBase(nsCOMPtr<nsIFaviconDataCallback>& aCallback); virtual ~AsyncFaviconHelperBase(); - nsRefPtr<Database> mDB; // Strong reference since we are responsible for its existence. nsCOMPtr<nsIFaviconDataCallback> mCallback; }; /** * Async fetches icon from database or network, associates it with the required * page and finally notifies the change. */
--- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -310,18 +310,18 @@ public: explicit DatabaseShutdown(Database* aDatabase); already_AddRefed<nsIAsyncShutdownClient> GetClient(); /** * `true` if we have not started shutdown, i.e. if * `BlockShutdown()` hasn't been called yet, false otherwise. */ - bool IsStarted() const { - return mIsStarted; + static bool IsStarted() { + return sIsStarted; } private: nsCOMPtr<nsIAsyncShutdownBarrier> mBarrier; nsCOMPtr<nsIAsyncShutdownClient> mParentClient; // The owning database. // The cycle is broken in method Complete(), once the connection @@ -349,31 +349,32 @@ private: // Execution of `Complete` in progress // a. `Complete` is starting. RECEIVED_STORAGESHUTDOWN_COMPLETE, // b. We have notified observers that Places as closed connection. NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED, }; State mState; - bool mIsStarted; // As tests may resurrect a dead `Database`, we use a counter to // give the instances of `DatabaseShutdown` unique names. uint16_t mCounter; static uint16_t sCounter; + static Atomic<bool> sIsStarted; + ~DatabaseShutdown() {} }; uint16_t DatabaseShutdown::sCounter = 0; +Atomic<bool> DatabaseShutdown::sIsStarted(false); DatabaseShutdown::DatabaseShutdown(Database* aDatabase) : mDatabase(aDatabase) , mState(NOT_STARTED) - , mIsStarted(false) , mCounter(sCounter++) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown(); MOZ_ASSERT(asyncShutdownSvc); if (asyncShutdownSvc) { DebugOnly<nsresult> rv = asyncShutdownSvc->MakeBarrier( @@ -460,17 +461,17 @@ NS_IMETHODIMP DatabaseShutdown::GetState // of `this` barrier have completed their own shutdown. // // See `Done()` for step 2. NS_IMETHODIMP DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient) { mParentClient = aParentClient; mState = RECEIVED_BLOCK_SHUTDOWN; - mIsStarted = true; + sIsStarted = true; if (NS_WARN_IF(!mBarrier)) { return NS_ERROR_NOT_AVAILABLE; } // Wait until all clients have removed their blockers, then proceed // with own shutdown. DebugOnly<nsresult> rv = mBarrier->Wait(this); @@ -657,16 +658,25 @@ Database::GetStatement(const nsACString& already_AddRefed<nsIAsyncShutdownClient> Database::GetConnectionShutdown() { MOZ_ASSERT(mConnectionShutdown); return mConnectionShutdown->GetClient(); } +// static +already_AddRefed<Database> +Database::GetDatabase() +{ + if (DatabaseShutdown::IsStarted()) + return nullptr; + return GetSingleton(); +} + nsresult Database::Init() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr<mozIStorageService> storage = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); NS_ENSURE_STATE(storage);
--- a/toolkit/components/places/Database.h +++ b/toolkit/components/places/Database.h @@ -89,20 +89,17 @@ public: */ already_AddRefed<nsIAsyncShutdownClient> GetConnectionShutdown(); /** * Getter to use when instantiating the class. * * @return Singleton instance of this class. */ - static already_AddRefed<Database> GetDatabase() - { - return GetSingleton(); - } + static already_AddRefed<Database> GetDatabase(); /** * Returns last known database status. * * @return one of the nsINavHistoryService::DATABASE_STATUS_* constants. */ uint16_t GetDatabaseStatus() const {
--- a/toolkit/components/telemetry/TelemetrySend.jsm +++ b/toolkit/components/telemetry/TelemetrySend.jsm @@ -141,17 +141,16 @@ function gzipCompressString(string) { .createInstance(Ci.nsIStringInputStream); stringStream.data = string; converter.onStartRequest(null, null); converter.onDataAvailable(null, null, stringStream, 0, string.length); converter.onStopRequest(null, null, null); return observer.buffer; } - this.TelemetrySend = { /** * Age in ms of a pending ping to be considered overdue. */ get OVERDUE_PING_FILE_AGE() { return OVERDUE_PING_FILE_AGE; }, @@ -386,18 +385,22 @@ let SendScheduler = { } // Get a list of pending pings, sorted by last modified, descending. // Filter out all the pings we can't send now. This addresses scenarios like "deletion" pings // which can be send even when upload is disabled. let pending = TelemetryStorage.getPendingPingList(); let current = TelemetrySendImpl.getUnpersistedPings(); this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length); - pending = pending.filter(p => TelemetrySendImpl.sendingEnabled(p)); - current = current.filter(p => TelemetrySendImpl.sendingEnabled(p)); + // Note that the two lists contain different kind of data. |pending| only holds ping + // info, while |current| holds actual ping data. + if (!TelemetrySendImpl.sendingEnabled()) { + pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id)); + current = current.filter(p => isDeletionPing(p)); + } this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length); // Bail out if there is nothing to send. if ((pending.length == 0) && (current.length == 0)) { this._log.trace("_doSendTask - no pending pings, bailing out"); this._sendTaskState = "bail out - no pings to send"; return; } @@ -707,17 +710,22 @@ let TelemetrySendImpl = { for (let current of currentPings) { let ping = current; let p = Task.spawn(function*() { try { yield this._doPing(ping, ping.id, false); } catch (ex) { this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex); - yield TelemetryStorage.savePendingPing(ping); + // Deletion pings must be saved to a special location. + if (isDeletionPing(ping)) { + yield TelemetryStorage.saveDeletionPing(ping); + } else { + yield TelemetryStorage.savePendingPing(ping); + } } finally { this._currentPings.delete(ping.id); } }.bind(this)); this._trackPendingPingTask(p); pingSends.push(p); } @@ -779,16 +787,19 @@ let TelemetrySendImpl = { hping.add(new Date() - startTime); if (!success) { // Let the scheduler know about send failures for triggering backoff timeouts. SendScheduler.notifySendsFailed(); } if (success && isPersisted) { + if (TelemetryStorage.isDeletionPing(id)) { + return TelemetryStorage.removeDeletionPing(); + } return TelemetryStorage.removePendingPing(id); } else { return Promise.resolve(); } }, _getSubmissionPath: function(ping) { // The new ping format contains an "application" section, the old one doesn't.
--- a/toolkit/components/telemetry/TelemetryStorage.jsm +++ b/toolkit/components/telemetry/TelemetryStorage.jsm @@ -29,26 +29,30 @@ const LOGGER_PREFIX = "TelemetryStorage: const Telemetry = Services.telemetry; const Utils = TelemetryUtils; // Compute the path of the pings archive on the first use. const DATAREPORTING_DIR = "datareporting"; const PINGS_ARCHIVE_DIR = "archived"; const ABORTED_SESSION_FILE_NAME = "aborted-session-ping"; +const DELETION_PING_FILE_NAME = "pending-deletion-ping"; XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() { return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR); }); XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() { return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR); }); XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() { return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME); }); +XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() { + return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME); +}); // Maxmimum time, in milliseconds, archive pings should be retained. const MAX_ARCHIVED_PINGS_RETENTION_MS = 180 * 24 * 60 * 60 * 1000; // 180 days // Maximum space the archive can take on disk (in Bytes). const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB // Maximum space the outgoing pings can take on disk, for Desktop (in Bytes). const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB @@ -258,16 +262,40 @@ this.TelemetryStorage = { * @return {promise<object>} Promise that is resolved with the ping data if found. * Otherwise returns null. */ loadAbortedSessionPing: function() { return TelemetryStorageImpl.loadAbortedSessionPing(); }, /** + * Save the deletion ping. + * @param ping The deletion ping. + * @return {Promise} A promise resolved when the ping is saved. + */ + saveDeletionPing: function(ping) { + return TelemetryStorageImpl.saveDeletionPing(ping); + }, + + /** + * Remove the deletion ping. + * @return {Promise} Resolved when the ping is deleted from the disk. + */ + removeDeletionPing: function() { + return TelemetryStorageImpl.removeDeletionPing(); + }, + + /** + * Check if the ping id identifies a deletion ping. + */ + isDeletionPing: function(aPingId) { + return TelemetryStorageImpl.isDeletionPing(aPingId); + }, + + /** * Remove the aborted-session ping if present. * * @return {promise} Promise that is resolved once the ping is removed. */ removeAbortedSessionPing: function() { return TelemetryStorageImpl.removeAbortedSessionPing(); }, @@ -477,16 +505,18 @@ SaveSerializer.prototype = { }); }, }; let TelemetryStorageImpl = { _logger: null, // Used to serialize aborted session ping writes to disk. _abortedSessionSerializer: new SaveSerializer(), + // Used to serialize deletion ping writes to disk. + _deletionPingSerializer: new SaveSerializer(), // Tracks the archived pings in a Map of (id -> {timestampCreated, type}). // We use this to cache info on archived pings to avoid scanning the disk more than once. _archivedPings: new Map(), // A set of promises for pings currently being archived _activelyArchiving: new Set(), // Track the archive loading task to prevent multiple tasks from being executed. _scanArchiveTask: null, @@ -516,16 +546,17 @@ let TelemetryStorageImpl = { /** * Shutdown & block on any outstanding async activity in this module. * * @return {Promise} Promise that is resolved when shutdown is complete. */ shutdown: Task.async(function*() { this._shutdown = true; yield this._abortedSessionSerializer.flushTasks(); + yield this._deletionPingSerializer.flushTasks(); // If the tasks for archive cleaning or pending ping quota are still running, block on // them. They will bail out as soon as possible. yield this._cleanArchiveTask; yield this._enforcePendingPingsQuotaTask; }), /** * Save an archived ping to disk. @@ -1220,16 +1251,28 @@ let TelemetryStorageImpl = { this._pendingPings.set(id, { path: file.path, lastModificationDate: info.lastModificationDate.getTime(), }); } yield iter.close(); + + // Explicitly load the deletion ping from its known path, if it's there. + if (yield OS.File.exists(gDeletionPingFilePath)) { + this._log.trace("_scanPendingPings - Adding pending deletion ping."); + // We can't get the ping id or the last modification date without hitting the disk. + // Since deletion has a special handling, we don't really need those. + this._pendingPings.set(Utils.generateUUID(), { + path: gDeletionPingFilePath, + lastModificationDate: Date.now(), + }); + } + this._scannedPendingDirectory = true; return this._buildPingList(); }), _buildPingList: function() { const list = [for (p of this._pendingPings) { id: p[0], lastModificationDate: p[1].lastModificationDate, @@ -1374,16 +1417,60 @@ let TelemetryStorageImpl = { this._log.trace("removeAbortedSessionPing - success"); } catch (ex if ex.becauseNoSuchFile) { this._log.trace("removeAbortedSessionPing - no such file"); } catch (ex) { this._log.error("removeAbortedSessionPing - error removing ping", ex) } }.bind(this))); }, + + /** + * Save the deletion ping. + * @param ping The deletion ping. + * @return {Promise} Resolved when the ping is saved. + */ + saveDeletionPing: Task.async(function*(ping) { + this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath); + yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true }); + + return this._deletionPingSerializer.enqueueTask(() => + this.savePingToFile(ping, gDeletionPingFilePath, true)); + }), + + /** + * Remove the deletion ping. + * @return {Promise} Resolved when the ping is deleted from the disk. + */ + removeDeletionPing: Task.async(function*() { + return this._deletionPingSerializer.enqueueTask(Task.async(function*() { + try { + yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false }); + this._log.trace("removeDeletionPing - success"); + } catch (ex if ex.becauseNoSuchFile) { + this._log.trace("removeDeletionPing - no such file"); + } catch (ex) { + this._log.error("removeDeletionPing - error removing ping", ex) + } + }.bind(this))); + }), + + isDeletionPing: function(aPingId) { + this._log.trace("isDeletionPing - id: " + aPingId); + let pingInfo = this._pendingPings.get(aPingId); + if (!pingInfo) { + return false; + } + + if (pingInfo.path != gDeletionPingFilePath) { + return false; + } + + return true; + }, }; ///// Utility functions function pingFilePath(ping) { // Support legacy ping formats, who don't have an "id" field, but a "slug" field. let pingIdentifier = (ping.slug) ? ping.slug : ping.id; return OS.Path.join(TelemetryStorage.pingDirectoryPath, pingIdentifier);
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js @@ -147,21 +147,47 @@ add_task(function* test_simplePing() { add_task(function* test_deletionPing() { const isUnified = Preferences.get(PREF_UNIFIED, false); if (!isUnified) { // Skipping the test if unified telemetry is off, as no deletion ping will // be generated. return; } + const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server"; + // Disable FHR upload: this should trigger a deletion ping. Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); let ping = yield PingServer.promiseNextPing(); checkPingFormat(ping, DELETION_PING_TYPE, true, false); + // Wait on ping activity to settle. + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Restore FHR Upload. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); + + // Simulate a failure in sending the deletion ping by disabling the HTTP server. + yield PingServer.stop(); + // Disable FHR upload to send a deletion ping again. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); + // Wait for the send task to terminate, flagging it to do so at the next opportunity and + // cancelling any timeouts. + yield TelemetryController.reset(); + + // Enable the ping server again. + PingServer.start(); + // We set the new server using the pref, otherwise it would get reset with + // |TelemetryController.reset|. + Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port); + + // Reset the controller to spin the ping sending task. + yield TelemetryController.reset(); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); // Restore FHR Upload. Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); }); add_task(function* test_pingHasClientId() { // Send a ping with a clientId. yield sendPing(true, false); @@ -257,17 +283,17 @@ add_task(function* test_archivePings() { "TelemetryController should still archive pings if ping upload is enabled."); }); // Test that we fuzz the submission time around midnight properly // to avoid overloading the telemetry servers. add_task(function* test_midnightPingSendFuzzing() { const fuzzingDelay = 60 * 60 * 1000; fakeMidnightPingFuzzingDelay(fuzzingDelay); - let now = new Date(2030, 5, 1, 11, 00, 0); + let now = new Date(2030, 5, 1, 11, 0, 0); fakeNow(now); let waitForTimer = () => new Promise(resolve => { fakePingSendTimer((callback, timeout) => { resolve([callback, timeout]); }, () => {}); });
new file mode 100644 --- /dev/null +++ b/toolkit/components/url-classifier/PrivateBrowsingTrackingProtectionWhitelist.js @@ -0,0 +1,68 @@ +/* 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/. */ + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function PrivateBrowsingTrackingProtectionWhitelist() { + // The list of URIs explicitly excluded from tracking protection. + this._allowlist = []; + + Services.obs.addObserver(this, "last-pb-context-exited", true); +} + +PrivateBrowsingTrackingProtectionWhitelist.prototype = { + classID: Components.ID("{a319b616-c45d-4037-8d86-01c592b5a9af}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingTrackingProtectionWhitelist, + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), + _xpcom_factory: XPCOMUtils.generateSingletonFactory(PrivateBrowsingTrackingProtectionWhitelist), + + /** + * Add the provided URI to the list of allowed tracking sites. + * + * @param uri nsIURI + * The URI to add to the list. + */ + addToAllowList(uri) { + if (this._allowlist.indexOf(uri.spec) === -1) { + this._allowlist.push(uri.spec); + } + }, + + /** + * Remove the provided URI from the list of allowed tracking sites. + * + * @param uri nsIURI + * The URI to add to the list. + */ + removeFromAllowList(uri) { + let index = this._allowlist.indexOf(uri.spec); + if (index !== -1) { + this._allowlist.splice(index, 1); + } + }, + + /** + * Check if the provided URI exists in the list of allowed tracking sites. + * + * @param uri nsIURI + * The URI to add to the list. + */ + existsInAllowList(uri) { + return this._allowlist.indexOf(uri.spec) !== -1; + }, + + observe: function (subject, topic, data) { + if (topic == "last-pb-context-exited") { + this._allowlist = []; + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PrivateBrowsingTrackingProtectionWhitelist]);
--- a/toolkit/components/url-classifier/moz.build +++ b/toolkit/components/url-classifier/moz.build @@ -2,16 +2,17 @@ # 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 += ['tests'] XPIDL_SOURCES += [ + 'nsIPrivateBrowsingTrackingProtectionWhitelist.idl', 'nsIUrlClassifierDBService.idl', 'nsIUrlClassifierHashCompleter.idl', 'nsIUrlClassifierPrefixSet.idl', 'nsIUrlClassifierStreamUpdater.idl', 'nsIUrlClassifierUtils.idl', 'nsIUrlListManager.idl', ] @@ -37,16 +38,17 @@ SOURCES += [ # contains variables that conflict with LookupCache.cpp SOURCES += [ 'HashStore.cpp', ] EXTRA_COMPONENTS += [ 'nsURLClassifier.manifest', 'nsUrlClassifierHashCompleter.js', + 'PrivateBrowsingTrackingProtectionWhitelist.js', ] # Same as JS components that are run through the pre-processor. EXTRA_PP_COMPONENTS += [ 'nsUrlClassifierLib.js', 'nsUrlClassifierListManager.js', ]
new file mode 100644 --- /dev/null +++ b/toolkit/components/url-classifier/nsIPrivateBrowsingTrackingProtectionWhitelist.idl @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; + +/** + * The Private Browsing Tracking Protection service checks a URI against an + * in-memory list of tracking sites. + */ +[scriptable, uuid(c77ddfac-6cd6-43a9-84e8-91682a1a7b18)] +interface nsIPrivateBrowsingTrackingProtectionWhitelist : nsISupports +{ + /** + * Add a URI to the list of allowed tracking sites in Private Browsing mode + * (essentially a tracking whitelist). This operation will cause the URI to + * be registered if it does not currently exist. If it already exists, then + * the operation is essentially a no-op. + * + * @param uri the uri to add to the list + */ + void addToAllowList(in nsIURI uri); + + /** + * Remove a URI from the list of allowed tracking sites in Private Browsing + * mode (the tracking whitelist). If the URI is not already in the list, + * then the operation is essentially a no-op. + * + * @param uri the uri to remove from the list + */ + void removeFromAllowList(in nsIURI uri); + + /** + * Check if a URI exists in the list of allowed tracking sites in Private + * Browsing mode (the tracking whitelist). + * + * @param uri the uri to look for in the list + */ + bool existsInAllowList(in nsIURI uri); +}; + +%{ C++ +#define NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID "@mozilla.org/url-classifier/pbm-tp-whitelist;1" +%}
--- a/toolkit/components/url-classifier/nsURLClassifier.manifest +++ b/toolkit/components/url-classifier/nsURLClassifier.manifest @@ -1,6 +1,8 @@ component {26a4a019-2827-4a89-a85c-5931a678823a} nsUrlClassifierLib.js contract @mozilla.org/url-classifier/jslib;1 {26a4a019-2827-4a89-a85c-5931a678823a} component {ca168834-cc00-48f9-b83c-fd018e58cae3} nsUrlClassifierListManager.js contract @mozilla.org/url-classifier/listmanager;1 {ca168834-cc00-48f9-b83c-fd018e58cae3} component {9111de73-9322-4bfc-8b65-2b727f3e6ec8} nsUrlClassifierHashCompleter.js contract @mozilla.org/url-classifier/hashcompleter;1 {9111de73-9322-4bfc-8b65-2b727f3e6ec8} +component {a319b616-c45d-4037-8d86-01c592b5a9af} PrivateBrowsingTrackingProtectionWhitelist.js +contract @mozilla.org/url-classifier/pbm-tp-whitelist;1 {a319b616-c45d-4037-8d86-01c592b5a9af}
--- a/toolkit/devtools/server/actors/utils/make-debugger.js +++ b/toolkit/devtools/server/actors/utils/make-debugger.js @@ -63,25 +63,25 @@ const { reportException } = require("dev * |findDebuggees|) to the |Debugger| instance. */ module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebuggee }) { const dbg = new Debugger(); EventEmitter.decorate(dbg); dbg.uncaughtExceptionHook = reportDebuggerHookException; - dbg.onNewGlobalObject = global => { + dbg.onNewGlobalObject = function(global) { if (shouldAddNewGlobalAsDebuggee(global)) { - safeAddDebuggee(dbg, global); + safeAddDebuggee(this, global); } }; - dbg.addDebuggees = () => { - for (let global of findDebuggees(dbg)) { - safeAddDebuggee(dbg, global); + dbg.addDebuggees = function() { + for (let global of findDebuggees(this)) { + safeAddDebuggee(this, global); } }; return dbg; }; const reportDebuggerHookException = e => reportException("Debugger Hook", e);
--- a/toolkit/modules/PrivateBrowsingUtils.jsm +++ b/toolkit/modules/PrivateBrowsingUtils.jsm @@ -46,16 +46,28 @@ this.PrivateBrowsingUtils = { }, privacyContextFromWindow: function pbu_privacyContextFromWindow(aWindow) { return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsILoadContext); }, + addToTrackingAllowlist(aURI) { + let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"] + .getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist); + pbmtpWhitelist.addToAllowList(aURI); + }, + + removeFromTrackingAllowlist(aURI) { + let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"] + .getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist); + pbmtpWhitelist.removeFromAllowList(aURI); + }, + get permanentPrivateBrowsing() { try { return gTemporaryAutoStartMode || Services.prefs.getBoolPref(kAutoStartPref); } catch (e) { // The pref does not exist return false; }
--- a/toolkit/mozapps/extensions/amWebInstallListener.js +++ b/toolkit/mozapps/extensions/amWebInstallListener.js @@ -114,30 +114,30 @@ Installer.prototype = { if (install.addon.appDisabled) failed.push(install); else installs.push(install); if (install.linkedInstalls) { install.linkedInstalls.forEach(function(aInstall) { aInstall.addListener(this); - // App disabled items are not compatible and so fail to install - if (aInstall.addon.appDisabled) + // Corrupt or incompatible items fail to install + if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED || aInstall.addon.appDisabled) failed.push(aInstall); else installs.push(aInstall); }, this); } break; case AddonManager.STATE_CANCELLED: // Just ignore cancelled downloads break; default: logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " + - install.state); + install.state); } } this.isDownloading = false; this.downloads = installs; if (failed.length > 0) { // Stop listening and cancel any installs that are failed because of
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -5121,17 +5121,17 @@ AddonInstall.prototype = { this.state = AddonManager.STATE_DOWNLOAD_FAILED; this.error = AddonManager.ERROR_INCORRECT_HASH; aCallback(this); return; } } let self = this; - this.loadManifest().then(() => { + this.loadManifest(this.file).then(() => { XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) { self.existingAddon = aAddon; if (aAddon) applyBlocklistChanges(aAddon, self.addon); self.addon.updateDate = Date.now(); self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate; if (!self.addon.isCompatible) { @@ -5157,16 +5157,20 @@ AddonInstall.prototype = { aCallback(self); } }); }, ([error, message]) => { logger.warn("Invalid XPI", message); this.state = AddonManager.STATE_DOWNLOAD_FAILED; this.error = error; + AddonManagerPrivate.callInstallListeners("onNewInstall", + self.listeners, + self.wrapper); + aCallback(this); }); }, /** * Initialises this install to be a download from a remote url. * * @param aCallback @@ -5333,132 +5337,132 @@ AddonInstall.prototype = { */ updateAddonURIs: function AI_updateAddonURIs() { this.addon.sourceURI = this.sourceURI.spec; if (this.releaseNotesURI) this.addon.releaseNotesURI = this.releaseNotesURI.spec; }, /** + * Fills out linkedInstalls with AddonInstall instances for the other files + * in a multi-package XPI. + * + * @param aFiles + * An array of { entryName, file } for each remaining file from the + * multi-package XPI. + */ + _createLinkedInstalls: Task.async(function* AI_createLinkedInstalls(aFiles) { + if (aFiles.length == 0) + return; + + // Create new AddonInstall instances for every remaining file + if (!this.linkedInstalls) + this.linkedInstalls = []; + + for (let { entryName, file } of aFiles) { + logger.debug("Creating linked install from " + entryName); + let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file)); + + // Make the new install own its temporary file + install.ownsTempFile = true; + + this.linkedInstalls.push(install); + + // If one of the internal XPIs was multipackage then move its linked + // installs to the outer install + if (install.linkedInstalls) { + this.linkedInstalls.push(...install.linkedInstalls); + install.linkedInstalls = null; + } + + install.sourceURI = this.sourceURI; + install.releaseNotesURI = this.releaseNotesURI; + if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) + install.updateAddonURIs(); + } + }), + + /** * Loads add-on manifests from a multi-package XPI file. Each of the * XPI and JAR files contained in the XPI will be extracted. Any that * do not contain valid add-ons will be ignored. The first valid add-on will * be installed by this AddonInstall instance, the rest will have new * AddonInstall instances created for them. * * @param aZipReader * An open nsIZipReader for the multi-package XPI's files. This will * be closed before this method returns. - * @param aCallback - * A function to call when all of the add-on manifests have been - * loaded. Because this loadMultipackageManifests is an internal API - * we don't exception-wrap this callback */ _loadMultipackageManifests: Task.async(function* AI_loadMultipackageManifests(aZipReader) { let files = []; let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); while (entries.hasMore()) { let entryName = entries.getNext(); - var target = getTemporaryFile(); + let file = getTemporaryFile(); try { - aZipReader.extract(entryName, target); - files.push(target); + aZipReader.extract(entryName, file); + files.push({ entryName, file }); } catch (e) { logger.warn("Failed to extract " + entryName + " from multi-package " + "XPI", e); - target.remove(false); + file.remove(false); } } aZipReader.close(); if (files.length == 0) { - throw new Error("Multi-package XPI does not contain any packages " + - "to install"); + return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, + "Multi-package XPI does not contain any packages to install"]); } let addon = null; - // Find the first file that has a valid install manifest and use it for + // Find the first file that is a valid install and use it for // the add-on that this AddonInstall instance will install. - while (files.length > 0) { + for (let { entryName, file } of files) { this.removeTemporaryFile(); - this.file = files.shift(); - this.ownsTempFile = true; try { - addon = yield loadManifestFromZipFile(this.file); - break; + yield this.loadManifest(file); + logger.debug("Base multi-package XPI install came from " + entryName); + this.file = file; + this.ownsTempFile = true; + + yield this._createLinkedInstalls(files.filter(f => f.file != file)); + return; } catch (e) { - logger.warn(this.file.leafName + " cannot be installed from multi-package " + - "XPI", e); - } - } - - if (!addon) { - // No valid add-on was found - return; - } - - this.addon = addon; - - this.updateAddonURIs(); - - this.addon._install = this; - this.name = this.addon.selectedLocale.name; - this.type = this.addon.type; - this.version = this.addon.version; - - // Setting the iconURL to something inside the XPI locks the XPI and - // makes it impossible to delete on Windows. - //let newIcon = createWrapper(this.addon).iconURL; - //if (newIcon) - // this.iconURL = newIcon; - - // Create new AddonInstall instances for every remaining file - if (files.length > 0) { - this.linkedInstalls = []; - let self = this; - for (let file of files) { - let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file)); - - // Ignore bad add-ons (createInstall will have logged the error) - if (install.state == AddonManager.STATE_DOWNLOAD_FAILED) { - // Manually remove the temporary file - file.remove(true); - } - else { - // Make the new install own its temporary file - install.ownsTempFile = true; - - self.linkedInstalls.push(install) - - install.sourceURI = self.sourceURI; - install.releaseNotesURI = self.releaseNotesURI; - install.updateAddonURIs(); - } - } - } + // _createLinkedInstalls will log errors when it tries to process this + // file + } + } + + // No valid add-on was found, delete all the temporary files + for (let { file } of files) + file.remove(true); + + return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, + "Multi-package XPI does not contain any valid packages to install"]); }), /** * Called after the add-on is a local file and the signature and install * manifest can be read. * * @param aCallback * A function to call when the manifest has been loaded * @throws if the add-on does not contain a valid install manifest or the * XPI is incorrectly signed */ - loadManifest: Task.async(function* AI_loadManifest() { + loadManifest: Task.async(function* AI_loadManifest(file) { let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. createInstance(Ci.nsIZipReader); try { - zipreader.open(this.file); + zipreader.open(file); } catch (e) { zipreader.close(); return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]); } try { // loadManifestFromZipReader performs the certificate verification for us @@ -5766,17 +5770,17 @@ AddonInstall.prototype = { if (this.hash && calculatedHash != this.hash.data) { this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, "Downloaded file hash (" + calculatedHash + ") did not match provided hash (" + this.hash.data + ")"); return; } let self = this; - this.loadManifest().then(() => { + this.loadManifest(this.file).then(() => { if (self.addon.isCompatible) { self.downloadCompleted(); } else { // TODO Should we send some event here (bug 557716)? self.state = AddonManager.STATE_CHECKING; new UpdateChecker(self.addon, { onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) { @@ -5855,19 +5859,20 @@ AddonInstall.prototype = { self.wrapper)) { // If a listener changed our state then do not proceed with the install if (self.state != AddonManager.STATE_DOWNLOADED) return; self.install(); if (self.linkedInstalls) { - self.linkedInstalls.forEach(function(aInstall) { - aInstall.install(); - }); + for (let install of self.linkedInstalls) { + if (install.state == AddonManager.STATE_DOWNLOADED) + install.install(); + } } } }); }, // TODO This relies on the assumption that we are always installing into the // highest priority install location so the resulting add-on will be visible // overriding any existing copy in another install location (bug 557710).
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi @@ -0,0 +1,1 @@ +This isn't a valid zip file. \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi @@ -0,0 +1,1 @@ +This isn't a valid zip file. \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_install7/install.rdf @@ -0,0 +1,10 @@ +<?xml version="1.0"?> + +<!-- A multi-package XPI --> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:type>32</em:type> + </Description> +</RDF>
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_install8/install.rdf @@ -0,0 +1,10 @@ +<?xml version="1.0"?> + +<!-- A multi-package XPI --> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:type>32</em:type> + </Description> +</RDF>
new file mode 100644 index 0000000000000000000000000000000000000000..6e23eb214c8f3564f7f6ac508ff66c8780a13239 GIT binary patch literal 6443 zc$|${1z1!~+ukLmkzA#glv+SQ=@MKTmTt)fmRew!RFDRxTS`DsP*OmVE(rlaI+Rix zq!H<VdDG9=_xrz@YpyfroO$j!bI<cU*ERQnsbb?00xpWa={U5Swz`@S2mo9(A^<%A z;etY2Iyv#V+t}*q5&*E}zdzJ_fN37yL;xVpH_Rv;DvYV5jJLBB*c0xKLLgnFSg!L4 zu*gUg5=t^dAYeJLvxgHJ0dcjocC@sEgAHNIU<gE-P*Pg~3dWeZprlyr(P&q3ets`6 zFFr3JKBT)H|8+4jF@6C-enCM9W^M?|#|3Ta4RJxSvJiqV%5R3^aOeN9bw>IkoSZCw z0}vDt5ax$FUs$uUV7>(mmQ;YFtlbf=Xv{Z)Ev=9qXekyCcNg(Lti=CVE5zB-1z`(E zp)pn$GfBdo#nC>laA_eyN&Y|k7jXPP;9-`-k6Dj2A?z<YL23oiR!MjPD~6zpMvti| zgq;iA#@q^tM5EB|magVjmNp0*b3s0DSHy4pmL;m{g&KwY%<l#2#q|<E#VQy2xyYG$ zix>dVBL@I3o~qy0m4hg2K>7b%zdOou%D_i-_>@MZ$k{1c>Np+Vixi-^m7tJPk#BQ_ zlHy8_;nj%_V;e}Q)4S2G()>8cUPA81xL{R8@w=;Y+MCDUrVq~C&TISq9v{za?n?9= zWbS6}iuD7#G7DyVcLu@+9G$>WSH3hIGBC;^SPo3g(@Y%BRW9EgB!CyNpGv`*8f6dc zD(46w=IRa&kh2h=w`o1RaztTqtV8s+){$!Ztc~C;A)#VW39rDdfLTThITrOxY$9Z2 znd4ePv9vAtifC4Es}BGN&F+Uuhw{E}->UU~3U_TGYbpTS&MmI4EUs|d&aHph4D;%+ z0PH;m;`lVLX|gA6YO+%?7KGHf2F|V&p$`j0*n?tupwzH4Aq89VvZ_r@gGSDv)UsC1 z)9i1x_YLO=Qh}GHZxYiqV}W{^qps?I{luq>t*aL>!%Q9`hh-{bZFfk$*|gY4<hluj zvSkpo^e0B3cBv{oQKZeT>oi{#)n^s`CC|gDDJkylU4qh+pT@I@G$oR?Oik#dF0ZSe zq$U9b4L_H8;J1LlHUju<8~14ht`J)pZoR+9aO^W^Uc_S<ecqx$tVny~%dXizVC?p2 z@ew-Md(KE%_r_HHr}5_YZKd6L@m7l0T-k2=-<SjmG@?9im`H@~I#G@15lMadw#gl3 zYW*&(Q_4gK%U$5!5P`HRjy5AR>t=#ip*1V5I9X``jdY96>rd5(3!|<(x-Q=yj9j)= zDcG%>NW0Zgv;SCqr7dD%sboEaDYYttOntDYmS6NhL80QoN3pktZwBCaR*ctsvuLB= zFRCzZ6SeT%Va^iHR&sNU)#lgJ&IM?e%9(wt?-wvvN-$p#ADT|&yt?{jfWD%1c`%>* zp+dH9o5u9&t!Oaf;FQ*u#}Oi%QxXayIv{EoB_cp&o<4`8_;J-B!pM&85U&rC6&v^H zRa3gsVM!;$Iyc!O)t~E3T@~HrI!VT-M>Zz-Ms-iIl$>xqDB^9Iw_fQIDN3W%>Kw$Y zpfuS%WqT3OjyTNJ2^K)GK2ylxTH?r9AmBAATA6~pl1-`RoEV7m+*n`w_ObY^h&?aL zt*ZX@Ll^%zT&BulwM?*>e4M!k+`M(3gip%os?n*ymt}<&_eYVJhG6cv@e(XX1Jwdq zO3x<^!ZISN5>z8MI!BFcVpbx(ahATw?AK&}*LEmG>IafMU#ai)GUzt(>~aC$yi>3f zH9T6ZUK8NQtBn-Bmrt6$?$Uew2%i{uJ>cwST3ka1=ZmrNmoJL~Lpw$7P<<RD5Z4#+ z87q0y(<sr<bd*>5jx5YbZJ1f}i|Ffzn_3nxOWY=g6%OsF7H{5q@0eTWv-f=<z%56f zI14sFODuVHsd>n(wfOy{U=xhr(W{uAQRUTI`lW=&<?Saac5Hhuc?z)Zlgzg}a!D?) z>D-`YCtrnx?Ul(e&U+*~)VEj&hHIt8sFw-WvUUx3+xZ}(<zM;~4<xILI6(wz#Bx=1 zHm&bCk}Pvo?PcyItH#jA%9m^zSV`|Km1gEZZ5^lw97^uX<6hxy7@XPs{0j2L<k2y2 z#JJv=*+!vl_0zn^B87L)lHvV{ROdo$EC;&}xB4s)O;%hv18=_Gnh})I8!QXm?rtrY z4rQ6V251>K7aoNlbc_1G{?N1(7okDV9(b4%+^9qFu4?HH!L^SWV#Ox0N`>l)#<Mo| z*w#A9kJVBYdw5Ncy5kG%wF`&Q8MURXUzZEsO+we+PZxz#acDgts`3|p<GRMicaH{8 zBrG`N-T&l87>xTYnxS~mpnTPIT-Z8tg4U&PL%Adj;mic<NNgR2HbF<f2>Zp8_|(r@ zlSt6jNU<*9=Nw5)Jjj%!jSd>Gsah$UUS&~x!tpgrcu%o#|2W(vUT)FXQ&H6MDLxXd z1sRd`7?Ijdx(3R8uxspFwr+iTxdmLE=+DyzxY9@vm&@d@EgF&tJQ?GYTi@~8x}V&5 z8#tG}_kK+tOzuTMw@-Vu(~RRpqmKJga5uLhTyMc_Qg1r=MN`{(#{Nz=sQ#%5d^!F@ zqZe-i$rY^cL0pj_xtR?2NJM2xsZrRBd~No&=S{Yn;^!%s$fBg4b2ZrPG!IlUTL=#2 z*Wb<!=eQgWQZ^mwd3+Mn^P!f+Hd>m~dRV(X=M}+jr>=MpmlLfQIh#l>GCnwB=`cn0 zGm8ej=V4U<9YGCS+wD18tPXvnR)(}__KlF8eq*b^ECTbg8`Idyp`0n2Mww{!{kJCg z=EbAPNA978i{BfxG~~umGo%$FR9oX`YNBT4Y^*c|Y_vU!@n22uoXW-;JKnw9>a$mq z>C)!eG`IDf{B~KN53hW6kZ(m~;3F!9x<_qS6F%3%n3Z1uv*1x|&S?|hBX!z1S_j(v zJoty4eKXtEro!;66>gb?*q<!ERN@)vLhgr~Q(mhq$QGkN2~t@kwbE`*m<Of$#U<d9 zjX3CA`{A8m{(9R(<3=~y4VFouVM~^<{8TfUF<VpKw7}764{zq-2Xt;ycF7Afgn*hn z5|nY5j)~ZQ1YNJvN#tqY89AlJ65{hwypRUrW5gtLo|BgR4x9zGrW~sK){3KzU`sp_ z6`4M-ynP&(Egb)4zq1ck;^j7`DdR6W4AUMPeLfd%15AUmAs2YQA5RJ^rS75X$yYa; ze3ycQADy%bzgnwm-L$DIQ}5gn)l*VMNxZ;@o!)q9Nx-32vHUX46PpTzRW1HV6ORF_ z3(OlZ?yO?TMujIUo5K*`N9MNfFPB?qO^ok-P`<|q1@O9Z;EG<tvksB<k4hJiNTTgl zuU~C8+iC<a$nV3w-IB`FrL*mM$?w!(OAQHI!kRO({4~=4%sC>->v7<YUQ%5GOS^ab z$JpK0w$1RfSgt@~EFib5ZKu6FY_FE_5@GUOa;*}ZFCu5)Tsqobk~I2Y8@A)#cp_qT z{PK*ER2Y{mrq@yzTV9+p+bE`>;fAz@&?JWowSG%ad=-kINrT!$hSTRrio_}>xJQy# zSXJdD-$asZdTwp>fu!+NnQ7I1<x53wEaXqj^`B{ooz}VoMx^`lRO1F|n~HZ`cBBf# z>tdQ!8@L-s5$$(jXV3?r(&*e&W5N|%0|zFVkZI0MjiallQ(qi{>&v@Dh=yp+Pgjwm z%ptN@hMZ7yPXfuFMcSc0q%wYwH7FgI9(eQkw&UEkup;igUhbaoC7#*$T?gh{WQk=8 z<@5&vAET!4%fv@I?IZ}y?5VIjhHSw(6`$?fQa9u%iO(H#1W^|GtQtn=wz-=qCAMDk zKDryBppBl+TlNe-Qdu{XUWR5iR1(3*Z@Jj+Fk>~gcT9M*q7I)les=x5r={?g_Cs*3 zIP7)@<wV<;<4}$n5#CFE)R~CtwMPB6BRYv#v_oxr_2Wc{`N4`xSR{d*ed%Y}w=84T zZo_*GpG|NlMy_uRs;S<y(hMCmC?O)+;MpL1VhdzJ2W!`A+%dYBFqAOi9Ui~Y<EcB< zKrukISox~|Mr~*J@aUu6u;9GdMcHShCCJ9u(1yW1viB)Y+e3?kS5}Of(%(-Mfa(Q9 zD=2&>UB+GyPcSZX%pz*0>9$9a_hUrK)$czc;SX}RJ`P?>hY6+ikme}H65XJ5N!K^@ zX~Vy6)0H+w)M0fn3~YUV&fhcNud_b(HN_Es>)aiu<zznzJH%hv0u<9UB^EHx&qY<) z!ysxJFH!L6p%v)ibV2l<wpz0&<jmPa?wtkR=)qpWv4Ep8?-QOivP5JB4b;ywZIk}< zgN>5Pv9U(x&?b+E>1~~<bRIGyI{WcQhl-c=zLrOT5}}eF5;1BOho8v2`a4<KU*GGf z%ea-usgn7cKjNA_qRb%!+pcs$oT?o!iF6+4i&9Ewx(q$lSu5Snx<<Nu?e^hp0tgSB zY9#i;7V@;?1=idfv62xL30GfRoqdin^%Fs#?c-jZYm2Y#=v+#_#Z-g)xXSt;4-yX* z<r9P%I<C{ba?U4?XOnLw)qTbLk<_66vi?R6DbpLOg(OoNBPR)_$+awRY5&=EP>8Ny zbC+>Ar|v=NDui_TDfP3d#xF<4yVF;`jljpL;V$fDXVFT)UgPZ>tFMcO_U1+2oB9hq zMH+7B*6<z-WX`4FOIuVAsTj4yG#dz=S2t9r-H~Jfmpb{RH_o}xn7yg&SO4IeVP9Fy zP~}g&&22BcMei8Cg^qC9Sf<e_ga<x<aDSBfIhPJ3BIR<{W{QL|ROpG;%+}qj9QtOZ z*Io(bz<CMH@9r=Fo>U#YO)D*s^fPSF-D#Q3C(ko3u+9|{kB4o1$8vmeS;q*DT8+RV zOBm?y%J5^WcXEs3ZRju%U97fD5jVNVFfejohsAA(Qkcx-%qBmHtM3kOWXxr6<$l5u zRZ!>WsqJNjX`KM{Al`;4?5M~wiCR-9=&cO!_Y)Ep=7eO02!h@xy8V?Gb9gGk0RV^r zAi!S-E<Oj8o-Qr`+u#Yq&u6SxiJGcyvgGZXhg9+!B}viVRo)b24TXj)H=Gg?loRVA zSrd4do9ZajUxT0Q#a2&s_9AEQmLCjP%?}V|aK5tES(EDGB<Z4(53DwNP5S7DE;Kyz z{dK2RtciNJ#FzqQ?Kr!@_O_mT-}Wwfx#k(CwD7#>4uZ&QO-#0N!?2y3sXru82L%R7 znl1n#iP?LqY8Ga8Ggm;k`4w1w6>LSzM9)2HN2`lbd4mtLnmtj(Oa#r-2In|*Q`tT~ zqr04D6a=c}C56|7z~#Mni3ruNNgP~V@`h=YO2+wGLzCO}#iHN5Pu{@RNbH~g5ZH(F z;HwyeA-nU|CoDWxT3!pqle_?K73HiZd+aeqaR$kG#yaGjo*3kW^ziP{^7#rS{Y3M0 zCoYeu!2Q)(qQBWT!?3+`{GC@FLt{DCg=hf!PtgEFw)Q{C4o&EBX%!&9eQ-!9Qp^xe zrN=^;tCXZyC+B>?aoyrwY}aF9L{nqTE<oX_uO*jPU)B|Kc9UxD9DZBgF$e0tJW<mn zxwx;@67$^MrL97;$rOXa>cf)I$;_^MHKqxo%*sLPK5xI70G(e}R0_yWZFeNSqbnPh zF}<`($L3ZKR`c92TJB&@a!!K~mo=9JF3XLT6FTM%wj{g50|^7!^-j8YMGZu_tOXa~ zq#8aOe0QZdSp7U$!tEB7sg5VVO9AmqT7q)*NT1n0u2GVUm9mKPg!0JpzfeFDFQ;JL z2ANNCavPm(jLKzHyfr?A-pP!s<>o4nJ#~~;U_;Bb8<{B4Zp1Y*)&$fa2I1=v<t)Cn z(9sK`rF!P|$yUrkN7q?zT!`{>_8VyU{3=QbF&D@)N0a}xP-cJjHt2xGE7E5tHXO$b z4gJKQhTrtN=71p|5Qf<ezgK0BBAC9&Z+Za;fQF;cd}wd<Z(EV^&#ln4N1(tLgTS^3 zCpg&J(gkb<2U|L!;qGu7Fd7NAa)(<wf-j^Cgo_>6+8%E0h~gvsPf5af+>!49a~BKz zS(5leOyP63jef6=3=#lzY)fPZ)y4OvI^Svqz9kXypaOALQ{?LA%9=dg9U`mj4Zeod z+&M{{xvG_5XG)AYrfKWQGaxh{R><ku7KXS7c57Q7o4Q?UwAFv$2vyK>lQ)y-cbGM1 zeah6<Gy}^Uw){8}c87xpI+VwFS|7wJtiE78{bIZ<a+KTIg7*7yIpcAk`-hMLBCedn zvH}mt8;imL(i2O62@)EdZvMCe$}8v;wFdadsoh4Bp2%UlDL%<pk!*q4chQV>M-r_f zclMrg<a<A&nWJJ}=`~?$ddcA)NgM#08LI4#q$F&RRpcFQqZBpRZ3&ND>04bf|4*Vi zlf!}?Fn2zfM)@o424zdsr`E5^%LP#xAyI{J_x1KyY6}bt&F*+yAMtpnrU~I2*jMlE zQNtE2rn)CZ!V(;r*qte_b8E0FcnXD`Xe!j>>|7HhMNe`)Swf8FSacyJ%?^|VTFF=v z3(fx?HeYi$<(gvJxVsw=08<5GQDOhPu=HmM00RC+X1YLN#Qyihdr>Zo^8;B(0RK}O z{t@w4$>ndvI8wm>l4^bj{P+0y8;}vR?Ef!Xex~ea$N!tMMDkyU!OsXkd&J)e^_PF? zA23y1yo&*NnEDzstpNq#4~g)1;Zf0V9D3P%7JLb_;)_NApa-Ds;m+{CR7R*^q@EV0 z{ZeJTAYK6jk>%^UXNq~XjrD6OSQY~y`%jQiZFS9H0Z`*E54S`rRz~>c4&gM3=izYQ z8`QM}`H+0SYs+QT0y!qOR!G0>*n<`9mD*%1zw6H4Pxbw&Le9i@>(SK{ERmd*JE{TV z%=w7)oHYDo=>&UBS{LAOi}G-5Uc5-_f<iO$Q!Akai3Y|-r>@NQNb>NxAc2YJc8zA? zuv$xwv3fi@?1BDO9G>%hJ;6eXQeqf$dHzGDN5G=hn@`m^>br7m5~KpMGJ2%A#M&r5 zyzOq1wKCFePN%trU!Po60s3m=`dZ40P;s`7i(zv4>&?-9yi*S>qJuCeWE)tqKE%`= zSB;1JjzlV-)Ppb!g9s4<4G+1%hOdt+a}Bi?bd!<;Z7j)Na<XR)bbc_fpxYqH8ms}1 zDB{I>K)f}~D%N7J_dP59C@#W|$@8^}q4TSL_F}69w84%n&Yg#+Y0G%leD0LRc4yn< zn_*cL$C`H2{6EIVKey;dSH9@|92>vbtUp}|h^bnbYc3G*L&W_0rvT_L5B>Et^8frj z@FU<qW$_;YH8CdtT_nf-T_pc6GyK#4f1II<8t`K@;Qk&B|H2D4rea|pQvQbUe?zdS AS^xk5
new file mode 100644 index 0000000000000000000000000000000000000000..0ba0f30d1ac93ad3851a6f20bbd4f319b57e8201 GIT binary patch literal 6563 zc$|${2UHVVv`rwOQ~{A9E%X{XLa3n_kX{9(6G9O}OF~DAG(kEdMT!yxk=_vm1VNf0 zC{>yY3MeQ=inJf}72nhM|G%^Dnmd^}d*<wO&b{mI=xY&zNC7A9vgHK4fxe-E6bJyE zbTR-lz{MSnv2$}1_I7YIF(v^Js_w>`+{SmT9~l4$+QvTxqQ#raEBT?^z&=QCw2Ozk z0*9EeD2I|FDXBa=1OiqDqp)rm7l@}F!qv_R2{zNe1cpEqN#zaI;9$I&J6eIm8H4eZ z6%q0E^%eG&5ccqP5)p$!p(3K<BI4o@{JSA&e|L<XAH*Ha$w3M}Y2OWHk*I&zqC5gz z+}!NG1Bi=?N{S#+C)S)C_-_G&<<*dAgtv<)2LFv<J9`f-Mu7wC?JoO=mFyqBLQr<@ zE{;ev25)upCV3=E7US=URFn{x7x{C10w?kl9^M@hyg!Ph`hU@xil=jrHz2wQPtZxH z!_#T+?cs`ax3Tx|z@RbScAhrk!hW7E-<hq)vs(WvvnO~b=hJ{wc6Z?ekKTo?kOKfl zQ~<!quR0fvl_8gO;Ua%}?v1vaG4+>8Bcqp&tLk?C<j9~V7Ulbi-GM(vp_*&|d=z6K zNdeErv7}Fvz2}qb+?ot~bT2uO6jpy`!+u_!-9KDE{Hidw8Fbt-v>!OMk&DhnU;6+B zkd%`ZQ1iGZjVFc{w=;4zio?PMLzThIEHJ`?r=qa-J886l84+%M;u}YduzPL*jifu| zrx`%Rov^7dXN3COla=aVm{V|el+qp8&3sYNL=ROoNahR>LU1?!jHeH_4*u{h39Dat z-EE0k7G7f8GliJbPn2*)b=ctmP)kn3>Rp1V7n8#?lQYyPXT2|8^>xqQ0IF08Prr?L zmZxd<R*$>1tgg5&hh;X0Z)N*sHec?1Oe*N<F?llaQvuk)a%@ajAeETX*N`c}V>@J= zFJa?k!aNSZ4~Ui{>3NHLPMI$W<_8{70z;SyY`-zetycIQ=gJAT2c0gaQm<W~U9%n= zTysV^1==vbQy#wd3gq+c+2`ZRd+<;by?1_qSBbQl=8r$!u<naaY~uzcwFiZi$;PA* zs1QgPG6xkArM81(6W&k0s10->;Mua6Yc6xxn$T*Fj~t7W_h8#e%6{2kTSEO>8WL$r zm7&A8uM-}zJTvx{Ch5K|EA=&jnL6^^;0On>l&)(jqPJJ=pl&C%K)%3h);uM0!w|?j zYSQm_MRF&n=&M-AlCyCd@9N$KtDG+i@m*ToU7{g!*V1X__!6QPTVwQvnMT$3>og}D zO~W0}m-Qq+{Mb}t9FyU<gZ9!{R^%NYFpHJo?puApU285nP%nJ@ajo?ziK5|^xrBG~ znv0Vyz}-AH8B+iKZ=51GQ0*yJRp>#&n!;Wthmba&%@v)JNatjYTbfEX4dH57&o-&W z_M*xowL|**Y2&5B1MW1NM#c7l%3<Q%?CZr>9WR@!?q8E=bqtx@g?}2b7RDrnAjM<n zEJM*MIi|W_nzYCJ_AQDHBN|NAnT!@nH)_io?ce!k-6*-8PU-1L4}1~xfj;0fF4gyK zh1jrK+lN5fF((mKu8LS*@hWd_9uUYq@uA7a9sj{DF)JT?j6p17S&SvJP_EGT1(DlG zpk6k%ne}>%7D<~ymcq+Y7ufiyuz+Ok8kYSMz=Qv?`}@brN86{~C7{(U>d3a&tTjFx zJTm0zSGjrlOqixw$ncSw->UsATDZXWd7K4xZw#=-_yx_FYT(M-*RskPRT@!&v1)8( zp1m*(6-0IM@ns`XkQ(sYGfNdprUG1Xmf-pWrLW_4MSGIfeDpVlIrCHctSR~(BHzRf zPRbuZZ%EA+OgB$tFtQvkESGfmsp*+DR~T>aM?FuV$c%q4^>C26vQe!$14OR-3Suk~ zQ<SAYZ`nKy(eHMcrFK&CJ`U`fU3|iDiQP8|+xM6*In$df&`s}_#g1{d<5b4=PkgS9 zARN?wH|;VrR@c8_uItda0yh^mFKv_cbvwzM#jRXsJ%;Y|)Udj{w?TB4lC0S9F+8hY z&VJZ*H5HD&Ffw@oHK2QR)=slcUV~YM`(ZgJ;0RW5CGR{Is2Q6*`zBCh_EIyWg_q>* z`nG4bPkpbFybQiJT<%nIbkmjpS~4PK7b2lX=Tmotc&eMT$(=6Du)1S)o%3Txx4v(6 zpoXD9AF@@8r1fG+C|@~+lSAm^0r%wh3B<-mL6iNdMTlH!!<Xj}7tYJBHjM3*=Bdt- zNCj#ZsXR)F4YxA9?!00pQ@hIfn!`J`)NjycE5-4F*Ox5)NnCoYYL9B&qeISD5+doh zx1A6YBBuOhdjtC9$Rba6s9ZLVmAAm={`<i-&=*nGx(D^=uM@ony;_ioZdDDimpOIe zB5SSXdP7EVlL70jTZexWL_lJ%CbtI1E@reLYv*Guk}8enzduAf?Jg#oJtx(4@9m7* z&8xYR3*xdBEgP8|lwSKjhaC|zuy7P1*U~FJg6MYs_K;PYZ-m>F2NfwhYy(lY-1F<! zr!et{WH&6D--Pg5k=(i27?IY9+XXJivZ?gWy*YmFy{AV$JIyV&0rKA>goJbwQog<e z?X<Y_G*D%ZhR%Z7NXn<&ceKVwQcbQ@i!MKER;es#;hvRq;mdis>{6?#oBmQg^twuP zb@x*)G$RgS?9|%o>D2P$mdbZ4b(XISHqdu`T5N0I^iZASh+57=TA96HthcM}IluLo zDU&*XonmzGE;21PAbHx%lgQ@VCE)5f_1>Y+J5~K&Z1M=Sb2Gtft-d=<H*xg*duH~O z<=MlxRrYlQH%vubJsa18JD3)P$nJGsjO>_*P1q?f)rp_8^ay_Fa=ezA<lQ;T6mM|& zk%N`R2FLeEv|5bP>BTc}?B31pB(rCQ;zyyHN&KVnp;lIB*=BR+m#M0TXR`*mQcbtn z>jn!to2QkZ_bB^37LA}Dihn+36l`7NviUV#+@?xw!1RHbAyCCw5CjU*t->r`VX*TH z{WOeV^A(<aV(7pw8udoNW@mT0pvHz~>GREg6!uhibe|mUt*pz&Wfz>eVo)y2{#Sc< z>w7G$m>L#??lOe6io~ATmTX8HF!~~@82I1|l;vwC?#UBQ=UcB=r;;|%;a;PR)QWlJ zui-63Ee!jLJ|O;}(`^WSD}@n#{g+ehAL8?gmLs}9Iobuq@K6vCc`&?Aw(=;S$*Wc) zqvzF_V;q=aHxKeK9E#8a35CqwW)3eYD^TAn_7am{AwxM;+MXWJ;E4mtk|e<l0f)*) z_o(zW0R#lAgyhjN>+=+(3JeH__J9~7N-+f(SwVL@EG^BLswdGvRSaf+cffjFC=<<t zwl89BezM_l4>vmD*Abd@pbAxz<RUzMiNf)krIN>6ldUH+dX4w^BJ<?tE^}4MN^o(> zErn>4a8aF33LQ2}mVzx%oTH)@=m{9;?eACB)&4YBWH2yxWgvZCV8!n9_8I&As-xKG zEQ07~ito4%%YD^GZ89|c9%4HdtOP3zJQ)QMTdi@c@H0c_7_VLLRqkQr{<vV&(-m6G zxOgBkA6ze7$W)Rz7xTdj!dbW=c?iDB&0T$wICxQYqMn2fc_1cbEnRq~Ce_oNm^(rD z7HNIDe{!_`HoVnB+k!KPG=t0PxManCX=bE+>GJ~1xGO~m-<pnUjw74!<TV-4XWrt} z`_X+&StEOnS;FrPmm~~YuO1n%>8{vH#5J>y?I5mRq7x!RJk;uQcVgq-iyq0vj?VC) zk?LZ}wQ}n=S=L&(gg0#oII{ZbhYUl#XJe)(??-F(etc+p)vUbM+M{EjJ%xuoXn0|O zccEUQZqbn6?M2y_{%y*O1yL$wHC<bkGn`+#n|lRyuW}R^Y?f@@wNZ|glFF|>rI(nO zHLo(LdqK$S+F;X!<1n^4WsRFK4rZBBTNE1)wYaSejWk*?5xXCx^vy#%`|QMC^xitu zCDFr*I^Wt+b(}qQ>?RsreX+588%Hqx&WSZ%5Sgx~_FCwpZi`IO^kh_-zghcPEyR_t z+T|aEZ*0lxymtnhOR<RidLshGZ*J7J=;@yiscGOy9hvZ8;*48Xzer&Ep%OlgMbVY@ zv!JdGGOy8Jc%(WPfG*r5SXeKLt(51hd6nIs#q&vJBbIyV61F^OK_N$+Q*S~v0o3H@ z*~Y-iEDO^ZI_?+W+oamZHX+RbpW2v}Jwp^001F}{+zIP!rj+f+TBKbpp3!yK*>jCA zN;E`$T1=_mu+!YA+1pMWCE^a_Z<L)ld?0ffJ&k;y=-Nu^(YcVG9<5^>-xYkGNA%`1 z%0`FU)vBsy==kSu#fj?9#Ibmp)0dvLr6x=}lysNo-W+}zV$Kk}pYG%p5c;ID<W0}V zNa%e;8vRt>+R*3#v{an)DW7DX#<dJpdYcH6=X86;<(v?$JP|AQX~n`3{mT!n*mGfg zsBQWT#n!oe_+Z;msng=<!>0;s^hxI~IO_-bi{cU#AKxx%=r;J!Ix731clfK%8~Z%; znRqN?e40Hj8M-1>p4^iBN*gwmDNB87&LPB{^#gU*9_gb8uU~D~f034!8N~J!*ebx< z6Gpf{w3zPNe=wRPdOyE3+?b4@fa{cnaou_`k+zEt9xXT8y%W9&SQ}!h+)mFT_D3ro zj#-v1F3e7REsKoxvS^xbB!@OFUT_$-xT*`DSyJ0IpU#OHb{Z_!${8v$!lKmZyD(bs zr2@ADwPTA&R4&VhWu%!FU)i?0muX{r7@MnU!FGV{!tZxf2G_I6>pSH2as|K15)+2O z8)-_8UMfgn#5V}VOunW2YL_87=Ydl>kdh96>5UtL&57yvQ$O235&@PXh6XkF@;R+X zXPpOk`bRjNV1lcfjTI&#uZ<Fn4r1jQ8JrCS5p>Wf;N!ctWwTBD^;4L8LaNYD*#WYL z2{#~`WeQY&rrBjL<8toUEbi0)*c%bx_eS<E;qVZ$i>pHTZPP;#06-340Q|Md626Ky zF(v{Kn%-mi_wK4LRZpuEM%DFsk5*N;EG^!z){o}Q<6^UADYsOYvy-19awdtX+nUd2 z)r0SCB{s}F8}OJ5tlW83yD&_a%~yvo`l!&uN6|y88rEQ5PnjWQ43EC^T+D5SV6w$4 zHKFK|VUkl=S7(3F_SR`%&w?xIPXtT)!Xc^#lT)1n`h=+Vb1zcPg@=X7TP^}2sd-yk z+O}3sbBqi`xN3sIYOaS%WDl^9#u`e|1tYOJ?LKI7Hj?&P(_;|ROrF30*e0J94T)A| zSuw8!xN;znj8unLZs+_5KYiVD`J?~@{8raxXnf=ITWf^6sY44d!UjP%R-r6r+^8?p z1jL;5LbmD$1)&0(mvY*i3CGoCS>zX3n?2r`Kp_W|dx77Uj+f6ePqxoKBNCJm4Oy8d z`<rbmJlh-lyFxm68Y>A-gaU?t3I%ww4Syv&GNs?WLzL>`&K{|BDN8i12?yz2jWm;H zWz-Ijm~C%jPp+g(TWi85K&>#qj^B4MhtY=HqQNj<#8GJc>bb!J8OsmKNvjQV3j%%R z9TKoxG$WEaujJuVcY1=FEK_9IFNN#)Ki#$fqN=KEL}4@Q-D$l{6|a;mPp>d>d9{GG zeb&sEy4llEnGo`d_Oh@g<?%{V*MgBJx4e;Iq+#492R%YErqcWf@kJ!1uK${FpaKtP zFqR|Q>HQ_z{i$6A;Hk1F;df0a?>pu;X(%Tu*xvMk3&KQ7)I7+S(g`jy*i7*Wm>;c; zDQ8zdy|M?lzLWGwfWI>F&{a{53!~g+ZlOWHmek7H6xy;Ee#(d}|NT>2Ba?7?+WWrK zj?k+{#we2siL-C>8sX6kD`*XuH(`Qr=y0pWN?+$MGVE~p#`$j~MuU7Y@M)3EQ*A$V z20ZzID10=6>ndEO@nh-lN&y1^gG6J5F@Biup~(I<6voajXz<BHU`H1>Bp6}m4z@>v z?c6X(Z=?el;{mq!M%uZ8(JoHzF78fXgfkN1iWVmQrx<Z%!c}+&f0YRQw;1t<fFg`? zjDN1<5iSboUYE-YZ%!V(jZ*0VKBbVx(lYQh(A+h?3$rNPe0ip3;5x5|-o`=d+<Ai( zCrk2zn_h-Ssw^&gvDL~Zj>Qq)VSR>(eM_&?t&W#(xWd&8yi~2^hOW+E;Vfk9Y@5?B zcxCr`G|HMs5dN}&^{^$JQ&MN~%52F*MckMG%9eh2zmj!-(ECNiFd2XTUPTcW(r8;e zOnG2;O^$*N)F+Zube0j5uKgJKdS<hgqCf7H(~PiuT^v_fULc0G`I}sawDndY56&-x z?hP&b@_+?fTNRIY9C;|i+{>E2xU-~>Vd_F-oo8iCH=jhuEf20N+x(NLXSbrRU&UYf z<NMj)sWxawvO(=3Eg^o0<|u_0L||}WsK!wAmBjo8R%{g8tE~qS9{#E`(63D>UP>FJ zK*4c6F17EDs*%b_?e!TnVXCD>KMK_puE0zoc1sRQw=c7pp6SGp#;~0I0UC+hjaq05 zOy^b4obdJn0{#}Lk}W6TfS(%CcYz8Y_wRaC^=_<5_3!j3AOS7me=AadwooA8KlG^+ z1XjZTp0`ihi7_sWnF8?tn#F%c{N3F2H)1R$;QupI{Q&&$^7<Q)0q^$zRc!yJ?B8|s zH)ZiuzptWyBm6s|{zj;${%v~w>Co@P&hXb57iK^M_%jKJek8#!-VhO=JbDU$0PzGR z(E$FCDSyDL2T#DM{pP`^@%K(T2|lYZ&PWvUFQpSM9%pjWf2(w!#83?nkrUv#Wr@GF zPVjpn8ioge{R$FksH1mXl%aJ~P(bcBL3T8Cw`8W=gJ@)c)VW8)I0!D7cd4R5G~dF} z-Xl0Kac7xu`Oz(cU@??m;pJ<z5l6C{EtrN$jyS%W4XsdFcAQIAe&(rLiYd<c1xA#| z>n7H#{r$-TBQ7zgI<p)}l4^Rz{Lqu#*@G(jO}Oaf1E*H2X#GcaJmW3IOoYQjD<HvR zoQZfbO*y$fdnGQGEd%&)<wBu0PfJg}LyAIZPIkWnKeY2K*3ap}nGRN_&$F3c(PC38 zn!o@<q5wOXG%a7p1Qfrj)PElPN_>bVAlr#@^XLStw?tTa6KM+ySj*iOR=5#mYZ@a> zqKj1yd%T)kbJxsZ(Kzi^n1kJ!Dn9O<;b$*QZJE|6az>hfqw2(oScsplRrSY2vBCT0 zuVtmV@hi?F^~i;lU}xwG3H^0f4%D-~!^|aOgs}J7Qm3PJs?Sk5H2Zo^vm!s2*{>n` zxhhUZ|1PuN1nbXK0^$z?{P`{r@Kf=fv|s;{_&Ha9_ZrpTPg#Eg{wm7<45$YL{HH`u k^h2WmU$B48{GYMmbbz1hfapgZ{0Ae1_~YQDR=*?s4{!0c0{{R3
new file mode 100644 index 0000000000000000000000000000000000000000..33101f63c06d75b719177863779b8ffbb5b43b17 GIT binary patch literal 6425 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
new file mode 100644 index 0000000000000000000000000000000000000000..3146870d86bc439a2a2e0985c2515ebe7d418483 GIT binary patch literal 2436 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 K0qFt`5Dx%QjUa&l
--- a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml @@ -130,16 +130,42 @@ </devices> <feature> WEBRTC_HW_ACCELERATION </feature> <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus> </gfxBlacklistEntry> <gfxBlacklistEntry> <os>All</os> <vendor>0xabcd</vendor> + <versionRange minVersion="42.0" maxVersion="13.0b2"/> + <devices> + <device>0x2783</device> + <device>0x1234</device> + <device>0x2782</device> + </devices> + <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature> + <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus> + </gfxBlacklistEntry> + + <gfxBlacklistEntry> + <os>All</os> + <vendor>0xabcd</vendor> + <versionRange minVersion="42.0" maxVersion="13.0b2"/> + <devices> + <device>0x2783</device> + <device>0x1234</device> + <device>0x2782</device> + </devices> + <feature> WEBRTC_HW_ACCELERATION_DECODE </feature> + <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus> + </gfxBlacklistEntry> + + <gfxBlacklistEntry> + <os>All</os> + <vendor>0xabcd</vendor> <versionRange minVersion="17.2a2" maxVersion="15.0"/> <devices> <device>0x2783</device> <device>0x1234</device> <device>0x2782</device> </devices> <feature> DIRECT3D_11_LAYERS </feature> <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -1103,19 +1103,23 @@ const AddonListener = { do_check_eq("onOperationCancelled", event); return check_test_completed(arguments); } }; const InstallListener = { onNewInstall: function(install) { if (install.state != AddonManager.STATE_DOWNLOADED && + install.state != AddonManager.STATE_DOWNLOAD_FAILED && install.state != AddonManager.STATE_AVAILABLE) do_throw("Bad install state " + install.state); - do_check_eq(install.error, 0); + if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) + do_check_eq(install.error, 0); + else + do_check_neq(install.error, 0); do_check_eq("onNewInstall", getExpectedInstall()); return check_test_completed(arguments); }, onDownloadStarted: function(install) { do_check_eq(install.state, AddonManager.STATE_DOWNLOADING); do_check_eq(install.error, 0); do_check_eq("onDownloadStarted", getExpectedInstall());
--- a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js @@ -102,16 +102,22 @@ function run_test() { do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION); status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_STAGEFRIGHT); do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION); do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE); + do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE); + do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); + status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS); do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING); do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK); status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE); do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js @@ -645,28 +645,41 @@ function check_test_10(install) { // Tests that a multi-package install shows up as multiple installs with the // correct sourceURI. function run_test_11() { prepare_test({ }, [ "onNewInstall", "onNewInstall", "onNewInstall", + "onNewInstall", + "onNewInstall", "onNewInstall" ]); AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) { ensure_test_completed(); do_check_neq(install, null); do_check_neq(install.linkedInstalls, null); - do_check_eq(install.linkedInstalls.length, 3); + do_check_eq(install.linkedInstalls.length, 5); // Might be in any order so sort them based on ID let installs = [install].concat(install.linkedInstalls); installs.sort(function(a, b) { + if (a.state != b.state) { + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 1; + else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED) + return -1; + } + + // Don't care what order the failed installs show up in + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 0; + if (a.addon.id < b.addon.id) return -1; if (a.addon.id > b.addon.id) return 1; return 0; }); // Comes from addon4.xpi and is made compatible by an update check @@ -704,16 +717,22 @@ function run_test_11() { do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org"); do_check_false(installs[3].addon.appDisabled); do_check_eq(installs[3].version, "5.0"); do_check_eq(installs[3].name, "Multi Test 4"); do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED); do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_INSTALL)); + do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE); + + do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE); + AddonManager.getAllInstalls(function(aInstalls) { do_check_eq(aInstalls.length, 4); prepare_test({ "addon4@tests.mozilla.org": [ "onInstalling" ], "addon5@tests.mozilla.org": [ @@ -810,16 +829,18 @@ function run_test_12() { "onInstalling" ] }, { "NO_ID": [ "onDownloadStarted", "onNewInstall", "onNewInstall", "onNewInstall", + "onNewInstall", + "onNewInstall", "onDownloadEnded" ], "addon4@tests.mozilla.org": [ "onInstallStarted", "onInstallEnded" ], "addon5@tests.mozilla.org": [ "onInstallStarted", @@ -834,21 +855,32 @@ function run_test_12() { "onInstallEnded" ] }, callback_soon(check_test_12)); install.install(); }, "application/x-xpinstall", null, "Multi Test 4"); } function check_test_12() { - do_check_eq(gInstall.linkedInstalls.length, 3); + do_check_eq(gInstall.linkedInstalls.length, 5); // Might be in any order so sort them based on ID let installs = [gInstall].concat(gInstall.linkedInstalls); installs.sort(function(a, b) { + if (a.state != b.state) { + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 1; + else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED) + return -1; + } + + // Don't care what order the failed installs show up in + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 0; + if (a.addon.id < b.addon.id) return -1; if (a.addon.id > b.addon.id) return 1; return 0; }); // Comes from addon4.xpi and is made compatible by an update check @@ -878,16 +910,22 @@ function check_test_12() { // Comes from addon7.jar and is made compatible by an update check do_check_eq(installs[3].sourceURI, gInstall.sourceURI); do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org"); do_check_false(installs[3].addon.appDisabled); do_check_eq(installs[3].version, "5.0"); do_check_eq(installs[3].name, "Multi Test 4"); do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED); + do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE); + + do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE); + restartManager(); AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org", "addon5@tests.mozilla.org", "addon6@tests.mozilla.org", "addon7@tests.mozilla.org"], function([a4, a5, a6, a7]) { do_check_neq(a4, null); @@ -1750,12 +1788,50 @@ function check_test_29(install) { //ensure_test_completed(); do_check_eq(install.state, AddonManager.STATE_DOWNLOADED); do_check_neq(install.addon, null); do_check_false(install.addon.isCompatible); do_check_true(install.addon.appDisabled); prepare_test({}, [ "onDownloadCancelled" - ], do_test_finished); + ], run_test_30); install.cancel(); return false; } + +// Tests that a multi-package XPI with no add-ons inside shows up as a +// corrupt file +function run_test_30() { + prepare_test({ }, [ + "onNewInstall" + ]); + + AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) { + ensure_test_completed(); + + do_check_neq(install, null); + do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE); + do_check_eq(install.linkedInstalls, null); + + run_test_31(); + }); +} + +// Tests that a multi-package XPI with no valid add-ons inside shows up as a +// corrupt file +function run_test_31() { + prepare_test({ }, [ + "onNewInstall" + ]); + + AddonManager.getInstallForFile(do_get_addon("test_install8"), function(install) { + ensure_test_completed(); + + do_check_neq(install, null); + do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE); + do_check_eq(install.linkedInstalls, null); + + end_test(); + }); +}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js @@ -634,28 +634,41 @@ function check_test_10(install) { // Tests that a multi-package install shows up as multiple installs with the // correct sourceURI. function run_test_11() { prepare_test({ }, [ "onNewInstall", "onNewInstall", "onNewInstall", + "onNewInstall", + "onNewInstall", "onNewInstall" ]); AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) { ensure_test_completed(); do_check_neq(install, null); do_check_neq(install.linkedInstalls, null); - do_check_eq(install.linkedInstalls.length, 3); + do_check_eq(install.linkedInstalls.length, 5); // Might be in any order so sort them based on ID let installs = [install].concat(install.linkedInstalls); installs.sort(function(a, b) { + if (a.state != b.state) { + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 1; + else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED) + return -1; + } + + // Don't care what order the failed installs show up in + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 0; + if (a.addon.id < b.addon.id) return -1; if (a.addon.id > b.addon.id) return 1; return 0; }); // Comes from addon4.xpi and is made compatible by an update check @@ -693,16 +706,22 @@ function run_test_11() { do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org"); do_check_false(installs[3].addon.appDisabled); do_check_eq(installs[3].version, "5.0"); do_check_eq(installs[3].name, "Multi Test 4"); do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED); do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_INSTALL)); + do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE); + + do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE); + AddonManager.getAllInstalls(function(aInstalls) { do_check_eq(aInstalls.length, 4); prepare_test({ "addon4@tests.mozilla.org": [ "onInstalling" ], "addon5@tests.mozilla.org": [ @@ -801,16 +820,18 @@ function run_test_12() { "onInstalling" ] }, { "NO_ID": [ "onDownloadStarted", "onNewInstall", "onNewInstall", "onNewInstall", + "onNewInstall", + "onNewInstall", "onDownloadEnded" ], "addon4@tests.mozilla.org": [ "onInstallStarted", "onInstallEnded" ], "addon5@tests.mozilla.org": [ "onInstallStarted", @@ -825,21 +846,32 @@ function run_test_12() { "onInstallEnded" ] }, callback_soon(check_test_12)); install.install(); }, "application/x-xpinstall", null, "Multi Test 4"); } function check_test_12() { - do_check_eq(gInstall.linkedInstalls.length, 3); + do_check_eq(gInstall.linkedInstalls.length, 5); // Might be in any order so sort them based on ID let installs = [gInstall].concat(gInstall.linkedInstalls); installs.sort(function(a, b) { + if (a.state != b.state) { + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 1; + else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED) + return -1; + } + + // Don't care what order the failed installs show up in + if (a.state == AddonManager.STATE_DOWNLOAD_FAILED) + return 0; + if (a.addon.id < b.addon.id) return -1; if (a.addon.id > b.addon.id) return 1; return 0; }); // Comes from addon4.xpi and is made compatible by an update check @@ -869,16 +901,22 @@ function check_test_12() { // Comes from addon7.jar and is made compatible by an update check do_check_eq(installs[3].sourceURI, gInstall.sourceURI); do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org"); do_check_false(installs[3].addon.appDisabled); do_check_eq(installs[3].version, "5.0"); do_check_eq(installs[3].name, "Multi Test 4"); do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED); + do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE); + + do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE); + restartManager(); AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org", "addon5@tests.mozilla.org", "addon6@tests.mozilla.org", "addon7@tests.mozilla.org"], function([a4, a5, a6, a7]) { do_check_neq(a4, null); @@ -1645,10 +1683,48 @@ function finish_test_27(aInstall) { }, [ "onInstallCancelled" ]); aInstall.cancel(); ensure_test_completed(); - end_test(); + run_test_30(); } + +// Tests that a multi-package XPI with no add-ons inside shows up as a +// corrupt file +function run_test_30() { + prepare_test({ }, [ + "onNewInstall" + ]); + + AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) { + ensure_test_completed(); + + do_check_neq(install, null); + do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE); + do_check_eq(install.linkedInstalls, null); + + run_test_31(); + }); +} + +// Tests that a multi-package XPI with no valid add-ons inside shows up as a +// corrupt file +function run_test_31() { + prepare_test({ }, [ + "onNewInstall" + ]); + + AddonManager.getInstallForFile(do_get_addon("test_install8"), function(install) { + ensure_test_completed(); + + do_check_neq(install, null); + do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); + do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE); + do_check_eq(install.linkedInstalls, null); + + end_test(); + }); +}
new file mode 100644 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_multi.js @@ -0,0 +1,55 @@ +// Enable signature checks for these tests +Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); +// Disable update security +Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + +const DATA = "data/signing_checks/"; + +// Each multi-package XPI contains one valid theme and one other add-on that +// has the following error state: +const ADDONS = { + "multi_signed.xpi": 0, + "multi_badid.xpi": AddonManager.ERROR_CORRUPT_FILE, + "multi_broken.xpi": AddonManager.ERROR_CORRUPT_FILE, + "multi_unsigned.xpi": AddonManager.ERROR_SIGNEDSTATE_REQUIRED, +}; + +function createInstall(filename) { + return new Promise(resolve => { + AddonManager.getInstallForFile(do_get_file(DATA + filename), resolve, "application/x-xpinstall"); + }); +} + +function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); + startupManager(); + + run_next_test(); +} + +function* test_addon(filename) { + do_print("Testing " + filename); + + let install = yield createInstall(filename); + do_check_eq(install.state, AddonManager.STATE_DOWNLOADED); + do_check_eq(install.error, 0); + + do_check_neq(install.linkedInstalls, null); + do_check_eq(install.linkedInstalls.length, 1); + + let linked = install.linkedInstalls[0]; + do_print(linked.state); + do_check_eq(linked.error, ADDONS[filename]); + if (linked.error == 0) { + do_check_eq(linked.state, AddonManager.STATE_DOWNLOADED); + linked.cancel(); + } + else { + do_check_eq(linked.state, AddonManager.STATE_DOWNLOAD_FAILED); + } + + install.cancel(); +} + +for (let filename of Object.keys(ADDONS)) + add_task(test_addon.bind(null, filename));
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini @@ -237,16 +237,17 @@ fail-if = buildapp == "mulet" || os == " [test_pref_properties.js] [test_registry.js] [test_safemode.js] [test_signed_verify.js] [test_signed_inject.js] [test_signed_install.js] run-sequentially = Uses hardcoded ports in xpi files. [test_signed_migrate.js] +[test_signed_multi.js] [test_startup.js] # Bug 676992: test consistently fails on Android fail-if = os == "android" [test_syncGUID.js] [test_strictcompatibility.js] [test_targetPlatforms.js] [test_theme.js] # Bug 676992: test consistently fails on Android
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini @@ -21,9 +21,10 @@ skip-if = appname != "firefox" [test_provider_markSafe.js] [test_provider_shutdown.js] [test_provider_unsafe_access_shutdown.js] [test_provider_unsafe_access_startup.js] [test_shutdown.js] [test_XPIcancel.js] [test_XPIStates.js] + [include:xpcshell-shared.ini]
--- a/widget/GfxInfoBase.cpp +++ b/widget/GfxInfoBase.cpp @@ -146,16 +146,22 @@ GetPrefNameForFeature(int32_t aFeature) name = BLACKLIST_PREF_BRANCH "webgl.msaa"; break; case nsIGfxInfo::FEATURE_STAGEFRIGHT: name = BLACKLIST_PREF_BRANCH "stagefright"; break; case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION: name = BLACKLIST_PREF_BRANCH "webrtc.hw.acceleration"; break; + case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE: + name = BLACKLIST_PREF_BRANCH "webrtc.hw.acceleration.encode"; + break; + case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE: + name = BLACKLIST_PREF_BRANCH "webrtc.hw.acceleration.decode"; + break; default: break; }; return name; } // Returns the value of the pref for the relevant feature in aValue. @@ -335,16 +341,20 @@ BlacklistFeatureToGfxFeature(const nsASt else if (aFeature.EqualsLiteral("WEBGL_OPENGL")) return nsIGfxInfo::FEATURE_WEBGL_OPENGL; else if (aFeature.EqualsLiteral("WEBGL_ANGLE")) return nsIGfxInfo::FEATURE_WEBGL_ANGLE; else if (aFeature.EqualsLiteral("WEBGL_MSAA")) return nsIGfxInfo::FEATURE_WEBGL_MSAA; else if (aFeature.EqualsLiteral("STAGEFRIGHT")) return nsIGfxInfo::FEATURE_STAGEFRIGHT; + else if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION_ENCODE")) + return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE; + else if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION_DECODE")) + return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE; else if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION")) return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION; // If we don't recognize the feature, it may be new, and something // this version doesn't understand. So, nothing to do. This is // different from feature not being specified at all, in which case // this method should not get called and we should continue with the // "all features" blocklisting. @@ -967,16 +977,18 @@ GfxInfoBase::EvaluateDownloadedBlacklist nsIGfxInfo::FEATURE_DIRECT3D_10_LAYERS, nsIGfxInfo::FEATURE_DIRECT3D_10_1_LAYERS, nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, nsIGfxInfo::FEATURE_DIRECT3D_11_ANGLE, nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_WEBGL_ANGLE, + nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, + nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE, nsIGfxInfo::FEATURE_WEBGL_MSAA, nsIGfxInfo::FEATURE_STAGEFRIGHT, nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, 0 }; // For every feature we know about, we evaluate whether this blacklist has a // non-STATUS_OK status. If it does, we set the pref we evaluate in
--- a/widget/android/AndroidBridge.cpp +++ b/widget/android/AndroidBridge.cpp @@ -467,16 +467,37 @@ AndroidBridge::GetHandlersForMimeType(co getHandlersFromStringArray(env, arr.Get(), len, aHandlersArray, aDefaultApp, aAction, NS_ConvertUTF16toUTF8(aMimeType)); return true; } bool +AndroidBridge::GetHWEncoderCapability() +{ + ALOG_BRIDGE("AndroidBridge::GetHWEncoderCapability"); + + bool value = GeckoAppShell::GetHWEncoderCapability(); + + return value; +} + + +bool +AndroidBridge::GetHWDecoderCapability() +{ + ALOG_BRIDGE("AndroidBridge::GetHWDecoderCapability"); + + bool value = GeckoAppShell::GetHWDecoderCapability(); + + return value; +} + +bool AndroidBridge::GetHandlersForURL(const nsAString& aURL, nsIMutableArray* aHandlersArray, nsIHandlerApp **aDefaultApp, const nsAString& aAction) { ALOG_BRIDGE("AndroidBridge::GetHandlersForURL"); auto arr = GeckoAppShell::GetHandlersForURLWrapper(aURL, aAction);
--- a/widget/android/AndroidBridge.h +++ b/widget/android/AndroidBridge.h @@ -202,16 +202,19 @@ public: nsIHandlerApp **aDefaultApp = nullptr, const nsAString& aAction = EmptyString()); bool GetHandlersForMimeType(const nsAString& aMimeType, nsIMutableArray* handlersArray = nullptr, nsIHandlerApp **aDefaultApp = nullptr, const nsAString& aAction = EmptyString()); + bool GetHWEncoderCapability(); + bool GetHWDecoderCapability(); + void GetMimeTypeFromExtensions(const nsACString& aFileExt, nsCString& aMimeType); void GetExtensionFromMimeType(const nsACString& aMimeType, nsACString& aFileExt); bool GetClipboardText(nsAString& aText); void ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText,
--- a/widget/android/GeneratedJNIWrappers.cpp +++ b/widget/android/GeneratedJNIWrappers.cpp @@ -273,16 +273,32 @@ mozilla::jni::String::LocalRef GeckoAppS constexpr char GeckoAppShell::GetExternalPublicDirectory_t::name[]; constexpr char GeckoAppShell::GetExternalPublicDirectory_t::signature[]; mozilla::jni::String::LocalRef GeckoAppShell::GetExternalPublicDirectory(mozilla::jni::String::Param a0) { return mozilla::jni::Method<GetExternalPublicDirectory_t>::Call(nullptr, nullptr, a0); } +constexpr char GeckoAppShell::GetHWDecoderCapability_t::name[]; +constexpr char GeckoAppShell::GetHWDecoderCapability_t::signature[]; + +bool GeckoAppShell::GetHWDecoderCapability() +{ + return mozilla::jni::Method<GetHWDecoderCapability_t>::Call(nullptr, nullptr); +} + +constexpr char GeckoAppShell::GetHWEncoderCapability_t::name[]; +constexpr char GeckoAppShell::GetHWEncoderCapability_t::signature[]; + +bool GeckoAppShell::GetHWEncoderCapability() +{ + return mozilla::jni::Method<GetHWEncoderCapability_t>::Call(nullptr, nullptr); +} + constexpr char GeckoAppShell::GetHandlersForMimeTypeWrapper_t::name[]; constexpr char GeckoAppShell::GetHandlersForMimeTypeWrapper_t::signature[]; mozilla::jni::ObjectArray::LocalRef GeckoAppShell::GetHandlersForMimeTypeWrapper(mozilla::jni::String::Param a0, mozilla::jni::String::Param a1) { return mozilla::jni::Method<GetHandlersForMimeTypeWrapper_t>::Call(nullptr, nullptr, a0, a1); }
--- a/widget/android/GeneratedJNIWrappers.h +++ b/widget/android/GeneratedJNIWrappers.h @@ -640,16 +640,50 @@ public: static const bool isMultithreaded = false; static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT; }; static mozilla::jni::String::LocalRef GetExternalPublicDirectory(mozilla::jni::String::Param); public: + struct GetHWDecoderCapability_t { + typedef GeckoAppShell Owner; + typedef bool ReturnType; + typedef bool SetterType; + typedef mozilla::jni::Args<> Args; + static constexpr char name[] = "getHWDecoderCapability"; + static constexpr char signature[] = + "()Z"; + static const bool isStatic = true; + static const bool isMultithreaded = false; + static const mozilla::jni::ExceptionMode exceptionMode = + mozilla::jni::ExceptionMode::ABORT; + }; + + static bool GetHWDecoderCapability(); + +public: + struct GetHWEncoderCapability_t { + typedef GeckoAppShell Owner; + typedef bool ReturnType; + typedef bool SetterType; + typedef mozilla::jni::Args<> Args; + static constexpr char name[] = "getHWEncoderCapability"; + static constexpr char signature[] = + "()Z"; + static const bool isStatic = true; + static const bool isMultithreaded = false; + static const mozilla::jni::ExceptionMode exceptionMode = + mozilla::jni::ExceptionMode::ABORT; + }; + + static bool GetHWEncoderCapability(); + +public: struct GetHandlersForMimeTypeWrapper_t { typedef GeckoAppShell Owner; typedef mozilla::jni::ObjectArray::LocalRef ReturnType; typedef mozilla::jni::ObjectArray::Param SetterType; typedef mozilla::jni::Args< mozilla::jni::String::Param, mozilla::jni::String::Param> Args; static constexpr char name[] = "getHandlersForMimeType";
--- a/widget/android/GfxInfo.cpp +++ b/widget/android/GfxInfo.cpp @@ -582,30 +582,25 @@ GfxInfo::GetFeatureStatusImpl(int32_t aF // Blocklist all Sony devices if (cManufacturer.Find("Sony", true) != -1) { *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; return NS_OK; } } } - if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION) { - NS_LossyConvertUTF16toASCII cManufacturer(mManufacturer); - NS_LossyConvertUTF16toASCII cModel(mModel); - NS_LossyConvertUTF16toASCII cHardware(mHardware); - - if (cHardware.EqualsLiteral("hammerhead") && - CompareVersions(mOSVersion.get(), "4.4.2") >= 0 && - cManufacturer.Equals("lge", nsCaseInsensitiveCStringComparator()) && - cModel.Equals("nexus 5", nsCaseInsensitiveCStringComparator())) { - *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION_ENCODE) { + if (mozilla::AndroidBridge::Bridge()) { + *aStatus = mozilla::AndroidBridge::Bridge()->GetHWEncoderCapability() ? nsIGfxInfo::FEATURE_STATUS_OK : nsIGfxInfo::FEATURE_BLOCKED_DEVICE; return NS_OK; - } else { - // Blocklist all other devices except Nexus 5 which VP8 hardware acceleration is supported - *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + } + } + if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION_DECODE) { + if (mozilla::AndroidBridge::Bridge()) { + *aStatus = mozilla::AndroidBridge::Bridge()->GetHWDecoderCapability() ? nsIGfxInfo::FEATURE_STATUS_OK : nsIGfxInfo::FEATURE_BLOCKED_DEVICE; return NS_OK; } } } return GfxInfoBase::GetFeatureStatusImpl(aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, &os); }
--- a/widget/nsIGfxInfo.idl +++ b/widget/nsIGfxInfo.idl @@ -3,17 +3,17 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" /* NOTE: this interface is completely undesigned, not stable and likely to change */ -[scriptable, uuid(98690931-c9a5-4675-9ab4-90932ec32bf2)] +[scriptable, uuid(4b5ea59e-af89-44f7-8c1c-2dea47a170d1)] interface nsIGfxInfo : nsISupports { /* * These are win32-specific */ readonly attribute boolean D2DEnabled; readonly attribute boolean DWriteEnabled; readonly attribute DOMString DWriteVersion; @@ -97,16 +97,20 @@ interface nsIGfxInfo : nsISupports /* Whether Webrtc Hardware acceleration is supported, starting in 31. */ const long FEATURE_WEBRTC_HW_ACCELERATION = 10; /* Whether Direct3D 11 is supported for layers, starting in 32. */ const long FEATURE_DIRECT3D_11_LAYERS = 11; /* Whether hardware accelerated video decoding is supported, starting in 36. */ const long FEATURE_HARDWARE_VIDEO_DECODING = 12; /* Whether Direct3D 11 is supported for ANGLE, starting in 38. */ const long FEATURE_DIRECT3D_11_ANGLE = 13; + /* Whether Webrtc Hardware acceleration is supported, starting in 42. */ + const long FEATURE_WEBRTC_HW_ACCELERATION_ENCODE = 14; + /* Whether Webrtc Hardware acceleration is supported, starting in 42. */ + const long FEATURE_WEBRTC_HW_ACCELERATION_DECODE = 15; /* * A set of return values from GetFeatureStatus */ /* The driver is safe to the best of our knowledge */ const long FEATURE_STATUS_OK = 1; /* We don't know the status of the feature yet. The analysis probably hasn't finished yet. */