diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 88d4664e25e8..9ec69d1afbc7 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -697,12 +697,15 @@ pref("plugin.state.ciscowebcommunicator", 2); pref("plugin.state.npmcafeemss", 2); #endif -// Cisco VGConnect for directv.com, bug 981403 +// Cisco VGConnect for directv.com, bug 981403 & bug 1051772 #ifdef XP_WIN pref("plugin.state.npplayerplugin", 2); #endif #ifdef XP_MACOSX pref("plugin.state.playerplugin", 2); +pref("plugin.state.playerplugin.dtv", 2); +pref("plugin.state.playerplugin.ciscodrm", 2); +pref("plugin.state.playerplugin.charter", 2); #endif // Cisco Jabber Client, bug 981905 @@ -843,6 +846,15 @@ pref("plugin.state.personalplugin", 2); pref("plugin.state.libplugins", 2); #endif +// Novell iPrint Client, bug 1036693 +#ifdef XP_WIN +pref("plugin.state.npnipp", 2); +pref("plugin.state.npnisp", 2); +#endif +#ifdef XP_MACOSX +pref("plugin.state.iprint", 2); +#endif + #ifdef XP_MACOSX pref("browser.preferences.animateFadeIn", true); #else diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index 5bef639d6350..c9d93660e1db 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -114,7 +114,7 @@ loop.Client = (function($) { } else { sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST; } - + this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST", {callerId: nickname}, function (error, responseText) { diff --git a/browser/components/loop/content/js/conversation.js b/browser/components/loop/content/js/conversation.js index 5e3eec22890f..e15d82359c77 100644 --- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -406,6 +406,7 @@ loop.conversation = (function(OT, mozL10n) { /*jshint newcap:false*/ this.loadReactComponent(sharedViews.ConversationView({ + initiate: true, sdk: OT, model: this._conversation, video: {enabled: videoStream} @@ -440,7 +441,8 @@ loop.conversation = (function(OT, mozL10n) { }); this.loadReactComponent(sharedViews.FeedbackView({ - feedbackApiClient: feedbackClient + feedbackApiClient: feedbackClient, + onAfterFeedbackReceived: window.close.bind(window) })); } }); diff --git a/browser/components/loop/content/js/conversation.jsx b/browser/components/loop/content/js/conversation.jsx index 8ac7ff778471..b6e7ec6ffbd5 100644 --- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -406,6 +406,7 @@ loop.conversation = (function(OT, mozL10n) { /*jshint newcap:false*/ this.loadReactComponent(sharedViews.ConversationView({ + initiate: true, sdk: OT, model: this._conversation, video: {enabled: videoStream} @@ -440,7 +441,8 @@ loop.conversation = (function(OT, mozL10n) { }); this.loadReactComponent(sharedViews.FeedbackView({ - feedbackApiClient: feedbackClient + feedbackApiClient: feedbackClient, + onAfterFeedbackReceived: window.close.bind(window) })); } }); diff --git a/browser/components/loop/content/shared/js/feedbackApiClient.js b/browser/components/loop/content/shared/js/feedbackApiClient.js index 5f5143f32237..e2aca70546b8 100644 --- a/browser/components/loop/content/shared/js/feedbackApiClient.js +++ b/browser/components/loop/content/shared/js/feedbackApiClient.js @@ -51,7 +51,8 @@ loop.FeedbackAPIClient = (function($, _) { "platform", "version", "channel", - "user_agent"], + "user_agent", + "url"], /** * Creates a formatted payload object compliant with the Feedback API spec diff --git a/browser/components/loop/content/shared/js/views.js b/browser/components/loop/content/shared/js/views.js index 411ecbe049fe..c0d677c88c29 100644 --- a/browser/components/loop/content/shared/js/views.js +++ b/browser/components/loop/content/shared/js/views.js @@ -30,11 +30,12 @@ loop.shared.views = (function(_, OT, l10n) { scope: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired, action: React.PropTypes.func.isRequired, - enabled: React.PropTypes.bool.isRequired + enabled: React.PropTypes.bool.isRequired, + visible: React.PropTypes.bool.isRequired }, getDefaultProps: function() { - return {enabled: true}; + return {enabled: true, visible: true}; }, handleClick: function() { @@ -48,7 +49,8 @@ loop.shared.views = (function(_, OT, l10n) { "btn": true, "media-control": true, "local-media": this.props.scope === "local", - "muted": !this.props.enabled + "muted": !this.props.enabled, + "hide": !this.props.visible }; classesObj["btn-mute-" + this.props.type] = true; return cx(classesObj); @@ -78,8 +80,8 @@ loop.shared.views = (function(_, OT, l10n) { var ConversationToolbar = React.createClass({displayName: 'ConversationToolbar', getDefaultProps: function() { return { - video: {enabled: true}, - audio: {enabled: true} + video: {enabled: true, visible: true}, + audio: {enabled: true, visible: true} }; }, @@ -103,7 +105,7 @@ loop.shared.views = (function(_, OT, l10n) { }, render: function() { - /* jshint ignore:start */ + var cx = React.addons.classSet; return ( React.DOM.ul({className: "conversation-toolbar"}, React.DOM.li({className: "conversation-toolbar-btn-box"}, @@ -115,25 +117,31 @@ loop.shared.views = (function(_, OT, l10n) { React.DOM.li({className: "conversation-toolbar-btn-box"}, MediaControlButton({action: this.handleToggleVideo, enabled: this.props.video.enabled, + visible: this.props.video.visible, scope: "local", type: "video"}) ), React.DOM.li({className: "conversation-toolbar-btn-box"}, MediaControlButton({action: this.handleToggleAudio, enabled: this.props.audio.enabled, + visible: this.props.audio.visible, scope: "local", type: "audio"}) ) ) ); - /* jshint ignore:end */ } }); + /** + * Conversation view. + */ var ConversationView = React.createClass({displayName: 'ConversationView', mixins: [Backbone.Events], propTypes: { sdk: React.PropTypes.object.isRequired, - model: React.PropTypes.object.isRequired + video: React.PropTypes.object, + audio: React.PropTypes.object, + initiate: React.PropTypes.bool }, // height set to 100%" to fix video layout on Google Chrome @@ -149,10 +157,11 @@ loop.shared.views = (function(_, OT, l10n) { } }, - getInitialProps: function() { + getDefaultProps: function() { return { - video: {enabled: true}, - audio: {enabled: true} + initiate: true, + video: {enabled: true, visible: true}, + audio: {enabled: true, visible: true} }; }, @@ -164,26 +173,30 @@ loop.shared.views = (function(_, OT, l10n) { }, componentWillMount: function() { - this.publisherConfig.publishVideo = this.props.video.enabled; + if (this.props.initiate) { + this.publisherConfig.publishVideo = this.props.video.enabled; + } }, componentDidMount: function() { - this.listenTo(this.props.model, "session:connected", - this.startPublishing); - this.listenTo(this.props.model, "session:stream-created", - this._streamCreated); - this.listenTo(this.props.model, ["session:peer-hungup", - "session:network-disconnected", - "session:ended"].join(" "), - this.stopPublishing); - - this.props.model.startSession(); + if (this.props.initiate) { + this.listenTo(this.props.model, "session:connected", + this.startPublishing); + this.listenTo(this.props.model, "session:stream-created", + this._streamCreated); + this.listenTo(this.props.model, ["session:peer-hungup", + "session:network-disconnected", + "session:ended"].join(" "), + this.stopPublishing); + this.props.model.startSession(); + } /** * OT inserts inline styles into the markup. Using a listener for * resize events helps us trigger a full width/height on the element * so that they update to the correct dimensions. - * */ + * XXX: this should be factored as a mixin. + */ window.addEventListener('orientationchange', this.updateVideoContainer); window.addEventListener('resize', this.updateVideoContainer); }, @@ -282,10 +295,12 @@ loop.shared.views = (function(_, OT, l10n) { * Unpublishes local stream. */ stopPublishing: function() { - // Unregister listeners for publisher events - this.stopListening(this.publisher); + if (this.publisher) { + // Unregister listeners for publisher events + this.stopListening(this.publisher); - this.props.model.session.unpublish(this.publisher); + this.props.model.session.unpublish(this.publisher); + } }, render: function() { @@ -362,7 +377,7 @@ loop.shared.views = (function(_, OT, l10n) { return {category: "", description: ""}; }, - getInitialProps: function() { + getDefaultProps: function() { return {pending: false}; }, @@ -467,8 +482,16 @@ loop.shared.views = (function(_, OT, l10n) { /** * Feedback received view. + * + * Props: + * - {Function} onAfterFeedbackReceived Function to execute after the + * WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed */ var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived', + propTypes: { + onAfterFeedbackReceived: React.PropTypes.func + }, + getInitialState: function() { return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS}; }, @@ -488,7 +511,9 @@ loop.shared.views = (function(_, OT, l10n) { render: function() { if (this.state.countdown < 1) { clearInterval(this._timer); - window.close(); + if (this.props.onAfterFeedbackReceived) { + this.props.onAfterFeedbackReceived(); + } } return ( FeedbackLayout({title: l10n.get("feedback_thank_you_heading")}, @@ -509,6 +534,7 @@ loop.shared.views = (function(_, OT, l10n) { propTypes: { // A loop.FeedbackAPIClient instance feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func, // The current feedback submission flow step name step: React.PropTypes.oneOf(["start", "form", "finished"]) }, @@ -517,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) { return {pending: false, step: this.props.step || "start"}; }, - getInitialProps: function() { + getDefaultProps: function() { return {step: "start"}; }, @@ -552,7 +578,10 @@ loop.shared.views = (function(_, OT, l10n) { render: function() { switch(this.state.step) { case "finished": - return FeedbackReceived(null); + return ( + FeedbackReceived({ + onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}) + ); case "form": return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient, sendFeedback: this.sendFeedback, diff --git a/browser/components/loop/content/shared/js/views.jsx b/browser/components/loop/content/shared/js/views.jsx index 307e40016e17..dffc1ee01b85 100644 --- a/browser/components/loop/content/shared/js/views.jsx +++ b/browser/components/loop/content/shared/js/views.jsx @@ -30,11 +30,12 @@ loop.shared.views = (function(_, OT, l10n) { scope: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired, action: React.PropTypes.func.isRequired, - enabled: React.PropTypes.bool.isRequired + enabled: React.PropTypes.bool.isRequired, + visible: React.PropTypes.bool.isRequired }, getDefaultProps: function() { - return {enabled: true}; + return {enabled: true, visible: true}; }, handleClick: function() { @@ -48,7 +49,8 @@ loop.shared.views = (function(_, OT, l10n) { "btn": true, "media-control": true, "local-media": this.props.scope === "local", - "muted": !this.props.enabled + "muted": !this.props.enabled, + "hide": !this.props.visible }; classesObj["btn-mute-" + this.props.type] = true; return cx(classesObj); @@ -78,8 +80,8 @@ loop.shared.views = (function(_, OT, l10n) { var ConversationToolbar = React.createClass({ getDefaultProps: function() { return { - video: {enabled: true}, - audio: {enabled: true} + video: {enabled: true, visible: true}, + audio: {enabled: true, visible: true} }; }, @@ -103,7 +105,7 @@ loop.shared.views = (function(_, OT, l10n) { }, render: function() { - /* jshint ignore:start */ + var cx = React.addons.classSet; return ( ); - /* jshint ignore:end */ } }); + /** + * Conversation view. + */ var ConversationView = React.createClass({ mixins: [Backbone.Events], propTypes: { sdk: React.PropTypes.object.isRequired, - model: React.PropTypes.object.isRequired + video: React.PropTypes.object, + audio: React.PropTypes.object, + initiate: React.PropTypes.bool }, // height set to 100%" to fix video layout on Google Chrome @@ -149,10 +157,11 @@ loop.shared.views = (function(_, OT, l10n) { } }, - getInitialProps: function() { + getDefaultProps: function() { return { - video: {enabled: true}, - audio: {enabled: true} + initiate: true, + video: {enabled: true, visible: true}, + audio: {enabled: true, visible: true} }; }, @@ -164,26 +173,30 @@ loop.shared.views = (function(_, OT, l10n) { }, componentWillMount: function() { - this.publisherConfig.publishVideo = this.props.video.enabled; + if (this.props.initiate) { + this.publisherConfig.publishVideo = this.props.video.enabled; + } }, componentDidMount: function() { - this.listenTo(this.props.model, "session:connected", - this.startPublishing); - this.listenTo(this.props.model, "session:stream-created", - this._streamCreated); - this.listenTo(this.props.model, ["session:peer-hungup", - "session:network-disconnected", - "session:ended"].join(" "), - this.stopPublishing); - - this.props.model.startSession(); + if (this.props.initiate) { + this.listenTo(this.props.model, "session:connected", + this.startPublishing); + this.listenTo(this.props.model, "session:stream-created", + this._streamCreated); + this.listenTo(this.props.model, ["session:peer-hungup", + "session:network-disconnected", + "session:ended"].join(" "), + this.stopPublishing); + this.props.model.startSession(); + } /** * OT inserts inline styles into the markup. Using a listener for * resize events helps us trigger a full width/height on the element * so that they update to the correct dimensions. - * */ + * XXX: this should be factored as a mixin. + */ window.addEventListener('orientationchange', this.updateVideoContainer); window.addEventListener('resize', this.updateVideoContainer); }, @@ -282,10 +295,12 @@ loop.shared.views = (function(_, OT, l10n) { * Unpublishes local stream. */ stopPublishing: function() { - // Unregister listeners for publisher events - this.stopListening(this.publisher); + if (this.publisher) { + // Unregister listeners for publisher events + this.stopListening(this.publisher); - this.props.model.session.unpublish(this.publisher); + this.props.model.session.unpublish(this.publisher); + } }, render: function() { @@ -362,7 +377,7 @@ loop.shared.views = (function(_, OT, l10n) { return {category: "", description: ""}; }, - getInitialProps: function() { + getDefaultProps: function() { return {pending: false}; }, @@ -467,8 +482,16 @@ loop.shared.views = (function(_, OT, l10n) { /** * Feedback received view. + * + * Props: + * - {Function} onAfterFeedbackReceived Function to execute after the + * WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed */ var FeedbackReceived = React.createClass({ + propTypes: { + onAfterFeedbackReceived: React.PropTypes.func + }, + getInitialState: function() { return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS}; }, @@ -488,7 +511,9 @@ loop.shared.views = (function(_, OT, l10n) { render: function() { if (this.state.countdown < 1) { clearInterval(this._timer); - window.close(); + if (this.props.onAfterFeedbackReceived) { + this.props.onAfterFeedbackReceived(); + } } return ( @@ -509,6 +534,7 @@ loop.shared.views = (function(_, OT, l10n) { propTypes: { // A loop.FeedbackAPIClient instance feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func, // The current feedback submission flow step name step: React.PropTypes.oneOf(["start", "form", "finished"]) }, @@ -517,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) { return {pending: false, step: this.props.step || "start"}; }, - getInitialProps: function() { + getDefaultProps: function() { return {step: "start"}; }, @@ -552,7 +578,10 @@ loop.shared.views = (function(_, OT, l10n) { render: function() { switch(this.state.step) { case "finished": - return ; + return ( + + ); case "form": return content/config.js @echo "loop.config = loop.config || {};" >> content/config.js - @echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js + @echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js + @echo "loop.config.feedbackApiUrl = '`echo $(LOOP_FEEDBACK_API_URL)`';" >> content/config.js + @echo "loop.config.feedbackProductName = '`echo $(LOOP_FEEDBACK_PRODUCT_NAME)`';" >> content/config.js diff --git a/browser/components/loop/standalone/README.md b/browser/components/loop/standalone/README.md index 3766fe025cc3..7ebfeab18af7 100644 --- a/browser/components/loop/standalone/README.md +++ b/browser/components/loop/standalone/README.md @@ -29,8 +29,12 @@ appropriate configuration file: - `LOOP_SERVER_URL` defines the root url of the loop server, without trailing slash (default: `http://localhost:5000`). -- `LOOP_PENDING_CALL_TIMEOUT` defines the amount of time a pending outgoing call - should be considered timed out, in milliseconds (default: `20000`). +- `LOOP_FEEDBACK_API_URL` sets the root URL for the + [input API](https://input.mozilla.org/); defaults to the input stage server + (https://input.allizom.org/api/v1/feedback). **Don't forget to set this + value to the production server URL when deploying to production.** +- `LOOP_FEEDBACK_PRODUCT_NAME` defines the product name to be sent to the input + API (defaults: Loop). Usage ----- diff --git a/browser/components/loop/standalone/content/css/webapp.css b/browser/components/loop/standalone/content/css/webapp.css index a9494479b958..421ee4e4b95b 100644 --- a/browser/components/loop/standalone/content/css/webapp.css +++ b/browser/components/loop/standalone/content/css/webapp.css @@ -207,3 +207,41 @@ body, flex: 1; } +/** + * Feedback form overlay (standalone only) + */ +.standalone .ended-conversation { + position: relative; + height: 100%; + background-color: #444; + text-align: left; /* as backup */ + text-align: start; +} + +.standalone .ended-conversation .feedback { + position: absolute; + width: 50%; + max-width: 400px; + margin: 10px auto; + top: 20px; + left: 10%; + right: 10%; + background: #FFF; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4); + border-radius: 3px; + z-index: 1002; /* ensures the form is always on top of the control bar */ +} + +.standalone .ended-conversation .local-stream { + /* Hide local media stream when feedback form is shown. */ + display: none; +} + +@media screen and (max-width:640px) { + .standalone .ended-conversation .feedback { + width: 92%; + top: 10%; + left: 5px; + right: 5px; + } +} diff --git a/browser/components/loop/standalone/content/index.html b/browser/components/loop/standalone/content/index.html index 52c29645e0f1..b4d01d6d9f0a 100644 --- a/browser/components/loop/standalone/content/index.html +++ b/browser/components/loop/standalone/content/index.html @@ -41,6 +41,7 @@ + diff --git a/browser/components/loop/standalone/content/js/standaloneClient.js b/browser/components/loop/standalone/content/js/standaloneClient.js index e5f3746c7380..cd4df3ee51d7 100644 --- a/browser/components/loop/standalone/content/js/standaloneClient.js +++ b/browser/components/loop/standalone/content/js/standaloneClient.js @@ -122,7 +122,7 @@ loop.StandaloneClient = (function($) { try { cb(null, this._validate(sessionData, expectedCallsProperties)); } catch (err) { - console.log("Error requesting call info", err); + console.error("Error requesting call info", err.message); cb(err); } }.bind(this)); diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index a0e717cc2fd8..fffb8295e5d7 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -5,7 +5,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true, React */ -/* jshint newcap:false */ +/* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.webapp = (function($, _, OT, mozL10n) { @@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) { var sharedModels = loop.shared.models, sharedViews = loop.shared.views; - /** - * App router. - * @type {loop.webapp.WebappRouter} - */ - var router; - /** * Homepage view. */ @@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { return ( React.DOM.p(null, mozL10n.get("welcome")) - ) + ); } }); @@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }, render: function() { - /* jshint ignore:start */ return ( React.DOM.div({className: "expired-url-info"}, React.DOM.div({className: "info-panel"}, @@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) { PromoteFirefoxView({helper: this.props.helper}) ) ); - /* jshint ignore:end */ } }); @@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */ React.DOM.header({className: "standalone-header header-box container-box"}, ConversationBranding(null), React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), @@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) { callUrlCreationDateString ) ) - /* jshint ignore:end */ ); } }); @@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) { getInitialState: function() { return { callState: this.props.callState || "connecting" - } + }; }, propTypes: { @@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { var callState = mozL10n.get("call_progress_" + this.state.callState + "_description"); return ( - /* jshint ignore:start */ React.DOM.div({className: "container"}, React.DOM.div({className: "container-box"}, React.DOM.header({className: "pending-header header-box"}, @@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) { ConversationFooter(null) ) - /* jshint ignore:end */ ); } }); @@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) { /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. + * + * Required properties: + * - {loop.shared.models.ConversationModel} model Conversation model. + * - {loop.shared.models.NotificationCollection} notifications */ var StartConversationView = React.createClass({displayName: 'StartConversationView', - /** - * Constructor. - * - * Required options: - * - {loop.shared.models.ConversationModel} model Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - */ + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifications: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - getInitialProps: function() { + getDefaultProps: function() { return {showCallOptionsMenu: false}; }, @@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }; }, - propTypes: { - model: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - // XXX Check more tightly here when we start injecting window.loop.* - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - componentDidMount: function() { // Listen for events & hide dropdown menu if user clicks away window.addEventListener("click", this.clickHandler); @@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */ React.DOM.div({className: "container"}, React.DOM.div({className: "container-box"}, @@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) { ConversationFooter(null) ) - /* jshint ignore:end */ + ); + } + }); + + /** + * Ended conversation view. + */ + var EndedConversationView = React.createClass({displayName: 'EndedConversationView', + propTypes: { + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func.isRequired + }, + + render: function() { + return ( + React.DOM.div({className: "ended-conversation"}, + sharedViews.FeedbackView({ + feedbackApiClient: this.props.feedbackApiClient, + onAfterFeedbackReceived: this.props.onAfterFeedbackReceived} + ), + sharedViews.ConversationView({ + initiate: false, + sdk: this.props.sdk, + model: this.props.conversation, + audio: {enabled: false, visible: false}, + video: {enabled: false, visible: false}} + ) + ) ); } }); @@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) { this.props.conversation.off(null, null, this); }, + shouldComponentUpdate: function(nextProps, nextState) { + // Only rerender if current state has actually changed + return nextState.callStatus !== this.state.callStatus; + }, + + callStatusSwitcher: function(status) { + return function() { + this.setState({callStatus: status}); + }.bind(this); + }, + /** * Renders the conversation views. */ render: function() { switch (this.state.callStatus) { case "failure": - case "end": case "start": { return ( StartConversationView({ @@ -472,19 +495,30 @@ loop.webapp = (function($, _, OT, mozL10n) { case "connected": { return ( sharedViews.ConversationView({ + initiate: true, sdk: this.props.sdk, model: this.props.conversation, video: {enabled: this.props.conversation.hasVideoStream("outgoing")}} ) ); } + case "end": { + return ( + EndedConversationView({ + sdk: this.props.sdk, + conversation: this.props.conversation, + feedbackApiClient: this.props.feedbackApiClient, + onAfterFeedbackReceived: this.callStatusSwitcher("start")} + ) + ); + } case "expired": { return ( CallUrlExpiredView({helper: this.props.helper}) ); } default: { - return HomeView(null) + return HomeView(null); } } }, @@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {{code: number, message: string}} error */ _notifyError: function(error) { - console.log(error); + console.error(error); this.props.notifications.errorL10n("connection_error_see_console_notification"); this.setState({callStatus: "end"}); }, @@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {String} reason The reason the call was terminated. */ _handleCallTerminated: function(reason) { - this.setState({callStatus: "end"}); - // For reasons other than cancel, display some notification text. if (reason !== "cancel") { // XXX This should really display the call failed view - bug 1046959 // will implement this. this.props.notifications.errorL10n("call_timeout_notification_text"); } + // redirects the user to the call start view + // XXX should switch callStatus to failed for specific reasons when we + // get the call failed view; for now, switch back to start. + this.setState({callStatus: "start"}); }, /** @@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -679,7 +716,8 @@ loop.webapp = (function($, _, OT, mozL10n) { conversation: this.props.conversation, helper: this.props.helper, notifications: this.props.notifications, - sdk: this.props.sdk} + sdk: this.props.sdk, + feedbackApiClient: this.props.feedbackApiClient} ) ); } else { @@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) { var conversation = new sharedModels.ConversationModel({}, { sdk: OT }); + var feedbackApiClient = new loop.FeedbackAPIClient( + loop.config.feedbackApiUrl, { + product: loop.config.feedbackProductName, + user_agent: navigator.userAgent, + url: document.location.origin + }); // Obtain the loopToken and pass it to the conversation var locationHash = helper.locationHash(); @@ -733,7 +777,8 @@ loop.webapp = (function($, _, OT, mozL10n) { conversation: conversation, helper: helper, notifications: notifications, - sdk: OT} + sdk: OT, + feedbackApiClient: feedbackApiClient} ), document.querySelector("#main")); // Set the 'lang' and 'dir' attributes to when the page is translated @@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) { PendingConversationView: PendingConversationView, StartConversationView: StartConversationView, OutgoingConversationView: OutgoingConversationView, + EndedConversationView: EndedConversationView, HomeView: HomeView, UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedDeviceView: UnsupportedDeviceView, diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index dda3ba1173c8..0630e8247456 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -5,7 +5,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true, React */ -/* jshint newcap:false */ +/* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.webapp = (function($, _, OT, mozL10n) { @@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) { var sharedModels = loop.shared.models, sharedViews = loop.shared.views; - /** - * App router. - * @type {loop.webapp.WebappRouter} - */ - var router; - /** * Homepage view. */ @@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { return (

{mozL10n.get("welcome")}

- ) + ); } }); @@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }, render: function() { - /* jshint ignore:start */ return (
@@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
); - /* jshint ignore:end */ } }); @@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */
@@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) { {callUrlCreationDateString}
- /* jshint ignore:end */ ); } }); @@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) { getInitialState: function() { return { callState: this.props.callState || "connecting" - } + }; }, propTypes: { @@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) { render: function() { var callState = mozL10n.get("call_progress_" + this.state.callState + "_description"); return ( - /* jshint ignore:start */
@@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
- /* jshint ignore:end */ ); } }); @@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) { /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. + * + * Required properties: + * - {loop.shared.models.ConversationModel} model Conversation model. + * - {loop.shared.models.NotificationCollection} notifications */ var StartConversationView = React.createClass({ - /** - * Constructor. - * - * Required options: - * - {loop.shared.models.ConversationModel} model Conversation model. - * - {loop.shared.models.NotificationCollection} notifications - * - */ + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifications: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - getInitialProps: function() { + getDefaultProps: function() { return {showCallOptionsMenu: false}; }, @@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }; }, - propTypes: { - model: React.PropTypes.instanceOf(sharedModels.ConversationModel) - .isRequired, - // XXX Check more tightly here when we start injecting window.loop.* - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - componentDidMount: function() { // Listen for events & hide dropdown menu if user clicks away window.addEventListener("click", this.clickHandler); @@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) { }); return ( - /* jshint ignore:start */
@@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) {
- /* jshint ignore:end */ + ); + } + }); + + /** + * Ended conversation view. + */ + var EndedConversationView = React.createClass({ + propTypes: { + conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired, + onAfterFeedbackReceived: React.PropTypes.func.isRequired + }, + + render: function() { + return ( +
+ + +
); } }); @@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) { this.props.conversation.off(null, null, this); }, + shouldComponentUpdate: function(nextProps, nextState) { + // Only rerender if current state has actually changed + return nextState.callStatus !== this.state.callStatus; + }, + + callStatusSwitcher: function(status) { + return function() { + this.setState({callStatus: status}); + }.bind(this); + }, + /** * Renders the conversation views. */ render: function() { switch (this.state.callStatus) { case "failure": - case "end": case "start": { return ( ); } + case "end": { + return ( + + ); + } case "expired": { return ( ); } default: { - return + return ; } } }, @@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {{code: number, message: string}} error */ _notifyError: function(error) { - console.log(error); + console.error(error); this.props.notifications.errorL10n("connection_error_see_console_notification"); this.setState({callStatus: "end"}); }, @@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) { * @param {String} reason The reason the call was terminated. */ _handleCallTerminated: function(reason) { - this.setState({callStatus: "end"}); - // For reasons other than cancel, display some notification text. if (reason !== "cancel") { // XXX This should really display the call failed view - bug 1046959 // will implement this. this.props.notifications.errorL10n("call_timeout_notification_text"); } + // redirects the user to the call start view + // XXX should switch callStatus to failed for specific reasons when we + // get the call failed view; for now, switch back to start. + this.setState({callStatus: "start"}); }, /** @@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) { helper: React.PropTypes.instanceOf(WebappHelper).isRequired, notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection) .isRequired, - sdk: React.PropTypes.object.isRequired + sdk: React.PropTypes.object.isRequired, + feedbackApiClient: React.PropTypes.object.isRequired }, getInitialState: function() { @@ -680,6 +717,7 @@ loop.webapp = (function($, _, OT, mozL10n) { helper={this.props.helper} notifications={this.props.notifications} sdk={this.props.sdk} + feedbackApiClient={this.props.feedbackApiClient} /> ); } else { @@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) { var conversation = new sharedModels.ConversationModel({}, { sdk: OT }); + var feedbackApiClient = new loop.FeedbackAPIClient( + loop.config.feedbackApiUrl, { + product: loop.config.feedbackProductName, + user_agent: navigator.userAgent, + url: document.location.origin + }); // Obtain the loopToken and pass it to the conversation var locationHash = helper.locationHash(); @@ -734,6 +778,7 @@ loop.webapp = (function($, _, OT, mozL10n) { helper={helper} notifications={notifications} sdk={OT} + feedbackApiClient={feedbackApiClient} />, document.querySelector("#main")); // Set the 'lang' and 'dir' attributes to when the page is translated @@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) { PendingConversationView: PendingConversationView, StartConversationView: StartConversationView, OutgoingConversationView: OutgoingConversationView, + EndedConversationView: EndedConversationView, HomeView: HomeView, UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedDeviceView: UnsupportedDeviceView, diff --git a/browser/components/loop/standalone/content/l10n/loop.en-US.properties b/browser/components/loop/standalone/content/l10n/loop.en-US.properties index 12ccf926f676..64c326c3268e 100644 --- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties +++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties @@ -46,3 +46,32 @@ clientShortname=WebRTC! call_url_creation_date_label=(from {{call_url_creation_date}}) call_progress_connecting_description=Connecting… call_progress_ringing_description=Ringing… + +feedback_call_experience_heading2=How was your conversation? +feedback_what_makes_you_sad=What makes you sad? +feedback_thank_you_heading=Thank you for your feedback! +feedback_category_audio_quality=Audio quality +feedback_category_video_quality=Video quality +feedback_category_was_disconnected=Was disconnected +feedback_category_confusing=Confusing +feedback_category_other=Other: +feedback_custom_category_text_placeholder=What went wrong? +feedback_submit_button=Submit +feedback_back_button=Back +## LOCALIZATION NOTE (feedback_window_will_close_in2): +## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387 +## In this item, don't translate the part between {{..}} +feedback_window_will_close_in2={[ plural(countdown) ]} +feedback_window_will_close_in2[one] = This window will close in {{countdown}} second +feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds +feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds + +## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after +## a signed-in to signed-in user call. +## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback +feedback_rejoin_button=Rejoin +## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of +## an abusive user. +feedback_report_user_button=Report User diff --git a/browser/components/loop/standalone/server.js b/browser/components/loop/standalone/server.js index ba8fdfb93dcb..d3bf4c072912 100644 --- a/browser/components/loop/standalone/server.js +++ b/browser/components/loop/standalone/server.js @@ -7,17 +7,21 @@ var app = express(); var port = process.env.PORT || 3000; var loopServerPort = process.env.LOOP_SERVER_PORT || 5000; +var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL || + "https://input.allizom.org/api/v1/feedback"; +var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop"; function getConfigFile(req, res) { "use strict"; res.set('Content-Type', 'text/javascript'); - res.send( - "var loop = loop || {};" + - "loop.config = loop.config || {};" + - "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" + - "loop.config.pendingCallTimeout = 20000;" - ); + res.send([ + "var loop = loop || {};", + "loop.config = loop.config || {};", + "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';", + "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';", + "loop.config.feedbackProductName = '" + feedbackProductName + "';", + ].join("\n")); } app.get('/content/config.js', getConfigFile); diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js index ba89cd094799..c4ba9e7f8995 100644 --- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -108,8 +108,7 @@ describe("loop.conversation", function() { beforeEach(function() { client = new loop.Client(); conversation = new loop.shared.models.ConversationModel({}, { - sdk: {}, - pendingCallTimeout: 1000, + sdk: {} }); sandbox.spy(conversation, "setIncomingSessionData"); sandbox.stub(conversation, "setOutgoingSessionData"); diff --git a/browser/components/loop/test/shared/feedbackApiClient_test.js b/browser/components/loop/test/shared/feedbackApiClient_test.js index 1b6f1a0da3bf..fbcb705a13e0 100644 --- a/browser/components/loop/test/shared/feedbackApiClient_test.js +++ b/browser/components/loop/test/shared/feedbackApiClient_test.js @@ -138,6 +138,13 @@ describe("loop.FeedbackAPIClient", function() { expect(parsed.user_agent).eql("MOZAGENT"); }); + it("should send url information when provided", function() { + client.send({url: "http://fake.invalid"}, function(){}); + + var parsed = JSON.parse(requests[0].requestBody); + expect(parsed.url).eql("http://fake.invalid"); + }); + it("should throw on invalid feedback data", function() { expect(function() { client.send("invalid data", function(){}); diff --git a/browser/components/loop/test/shared/router_test.js b/browser/components/loop/test/shared/router_test.js index f9223fc6813d..c825c19ab841 100644 --- a/browser/components/loop/test/shared/router_test.js +++ b/browser/components/loop/test/shared/router_test.js @@ -60,8 +60,7 @@ describe("loop.shared.router", function() { conversation = new loop.shared.models.ConversationModel({ loopToken: "fakeToken" }, { - sdk: {}, - pendingCallTimeout: 1000 + sdk: {} }); }); diff --git a/browser/components/loop/test/shared/views_test.js b/browser/components/loop/test/shared/views_test.js index 910dee348153..cdcf6a8dea58 100644 --- a/browser/components/loop/test/shared/views_test.js +++ b/browser/components/loop/test/shared/views_test.js @@ -187,13 +187,12 @@ describe("loop.shared.views", function() { initSession: sandbox.stub().returns(fakeSession) }; model = new sharedModels.ConversationModel(fakeSessionData, { - sdk: fakeSDK, - pendingCallTimeout: 1000 + sdk: fakeSDK }); }); describe("#componentDidMount", function() { - it("should start a session", function() { + it("should start a session by default", function() { sandbox.stub(model, "startSession"); mountTestComponent({ @@ -205,6 +204,19 @@ describe("loop.shared.views", function() { sinon.assert.calledOnce(model.startSession); }); + it("shouldn't start a session if initiate is false", function() { + sandbox.stub(model, "startSession"); + + mountTestComponent({ + initiate: false, + sdk: fakeSDK, + model: model, + video: {enabled: true} + }); + + sinon.assert.notCalled(model.startSession); + }); + it("should set the correct stream publish options", function() { var component = mountTestComponent({ diff --git a/browser/components/loop/test/standalone/index.html b/browser/components/loop/test/standalone/index.html index ef496f0c88ba..cece91948a6d 100644 --- a/browser/components/loop/test/standalone/index.html +++ b/browser/components/loop/test/standalone/index.html @@ -36,6 +36,7 @@ + diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js index f090639cefd3..52bc57e7a264 100644 --- a/browser/components/loop/test/standalone/webapp_test.js +++ b/browser/components/loop/test/standalone/webapp_test.js @@ -13,11 +13,15 @@ describe("loop.webapp", function() { var sharedModels = loop.shared.models, sharedViews = loop.shared.views, sandbox, - notifications; + notifications, + feedbackApiClient; beforeEach(function() { sandbox = sinon.sandbox.create(); notifications = new sharedModels.NotificationCollection(); + feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", { + product: "Loop" + }); }); afterEach(function() { @@ -31,6 +35,7 @@ describe("loop.webapp", function() { sandbox.stub(React, "renderComponent"); sandbox.stub(loop.webapp.WebappHelper.prototype, "locationHash").returns("#call/fake-Token"); + loop.config.feedbackApiUrl = "http://fake.invalid"; conversationSetStub = sandbox.stub(sharedModels.ConversationModel.prototype, "set"); }); @@ -77,7 +82,8 @@ describe("loop.webapp", function() { client: client, conversation: conversation, notifications: notifications, - sdk: {} + sdk: {}, + feedbackApiClient: feedbackApiClient }); }); @@ -305,7 +311,7 @@ describe("loop.webapp", function() { conversation.trigger("session:ended"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); }); @@ -314,7 +320,7 @@ describe("loop.webapp", function() { conversation.trigger("session:peer-hungup"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); it("should notify the user", function() { @@ -333,7 +339,7 @@ describe("loop.webapp", function() { conversation.trigger("session:network-disconnected"); TestUtils.findRenderedComponentWithType(ocView, - loop.webapp.StartConversationView); + loop.webapp.EndedConversationView); }); it("should notify the user", function() { @@ -474,8 +480,10 @@ describe("loop.webapp", function() { loop.webapp.WebappRootView({ client: client, helper: webappHelper, + notifications: notifications, sdk: sdk, - conversation: conversationModel + conversation: conversationModel, + feedbackApiClient: feedbackApiClient })); } @@ -772,6 +780,32 @@ describe("loop.webapp", function() { }); }); + describe("EndedConversationView", function() { + var view, conversation; + + beforeEach(function() { + conversation = new sharedModels.ConversationModel({}, { + sdk: {} + }); + view = React.addons.TestUtils.renderIntoDocument( + loop.webapp.EndedConversationView({ + conversation: conversation, + sdk: {}, + feedbackApiClient: feedbackApiClient, + onAfterFeedbackReceived: function(){} + }) + ); + }); + + it("should render a ConversationView", function() { + TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView); + }); + + it("should render a FeedbackView", function() { + TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView); + }); + }); + describe("PromoteFirefoxView", function() { describe("#render", function() { it("should not render when using Firefox", function() { diff --git a/browser/components/loop/ui/fake-l10n.js b/browser/components/loop/ui/fake-l10n.js index 77b8297382e7..daf2a9dd4948 100644 --- a/browser/components/loop/ui/fake-l10n.js +++ b/browser/components/loop/ui/fake-l10n.js @@ -9,6 +9,10 @@ * @type {Object} */ navigator.mozL10n = document.mozL10n = { + initialize: function(){}, + + getDirection: function(){}, + get: function(stringId, vars) { // upcase the first letter diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js index d8b5b77cf92c..411590015f37 100644 --- a/browser/components/loop/ui/fake-mozLoop.js +++ b/browser/components/loop/ui/fake-mozLoop.js @@ -7,6 +7,7 @@ * @type {Object} */ navigator.mozLoop = { + ensureRegistered: function() {}, getLoopCharPref: function() {}, getLoopBoolPref: function() {} }; diff --git a/browser/components/loop/ui/ui-showcase.css b/browser/components/loop/ui/ui-showcase.css index eea87cc2cc08..7a6ed557cd82 100644 --- a/browser/components/loop/ui/ui-showcase.css +++ b/browser/components/loop/ui/ui-showcase.css @@ -37,7 +37,7 @@ .showcase > section { position: relative; - padding-top: 12em; + padding-top: 14em; clear: both; } @@ -149,3 +149,9 @@ * When tokbox inserts the markup into the page the problem goes away */ bottom: auto; } + +.standalone .ended-conversation .remote_wrapper, +.standalone .video-layout-wrapper { + /* Removes the fake video image for ended conversations */ + background: none; +} diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index ece54dffc0e3..047f7cc19b9e 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -23,6 +23,7 @@ var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var PendingConversationView = loop.webapp.PendingConversationView; var StartConversationView = loop.webapp.StartConversationView; + var EndedConversationView = loop.webapp.EndedConversationView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -338,6 +339,19 @@ ) ), + Section({name: "EndedConversationView"}, + Example({summary: "Displays the feedback form"}, + React.DOM.div({className: "standalone"}, + EndedConversationView({sdk: mockSDK, + video: {enabled: true}, + audio: {enabled: true}, + conversation: mockConversationModel, + feedbackApiClient: stageFeedbackApiClient, + onAfterFeedbackReceived: noop}) + ) + ) + ), + Section({name: "AlertMessages"}, Example({summary: "Various alerts"}, React.DOM.div({className: "alert alert-warning"}, diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index 715f45feb759..eb7cfb8e34e9 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -23,6 +23,7 @@ var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var PendingConversationView = loop.webapp.PendingConversationView; var StartConversationView = loop.webapp.StartConversationView; + var EndedConversationView = loop.webapp.EndedConversationView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -338,6 +339,19 @@ +
+ +
+ +
+
+
+
diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js index 0b59b64894ff..174677dd6aa3 100644 --- a/browser/devtools/webide/content/webide.js +++ b/browser/devtools/webide/content/webide.js @@ -75,6 +75,8 @@ let UI = { } Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false); + this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime"); + this.setupDeck(); }, @@ -125,6 +127,7 @@ let UI = { switch (what) { case "runtimelist": this.updateRuntimeList(); + this.autoConnectRuntime(); break; case "connection": this.updateRuntimeButton(); @@ -145,6 +148,7 @@ let UI = { break; case "runtime": this.updateRuntimeButton(); + this.saveLastConnectedRuntime(); break; case "project-validated": this.updateTitle(); @@ -343,6 +347,34 @@ let UI = { } }, + autoConnectRuntime: function () { + // Automatically reconnect to the previously selected runtime, + // if available and has an ID + if (AppManager.selectedRuntime || !this.lastConnectedRuntime) { + return; + } + let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/); + + type = type.toLowerCase(); + + // Local connection is mapped to AppManager.runtimeList.custom array + if (type == "local") { + type = "custom"; + } + + // We support most runtimes except simulator, that needs to be manually + // launched + if (type == "usb" || type == "wifi" || type == "custom") { + for (let runtime of AppManager.runtimeList[type]) { + // Some runtimes do not expose getID function and don't support + // autoconnect (like remote connection) + if (typeof(runtime.getID) == "function" && runtime.getID() == id) { + this.connectToRuntime(runtime); + } + } + } + }, + connectToRuntime: function(runtime) { let name = runtime.getName(); let promise = AppManager.connectToRuntime(runtime); @@ -359,6 +391,17 @@ let UI = { } }, + saveLastConnectedRuntime: function () { + if (AppManager.selectedRuntime && + typeof(AppManager.selectedRuntime.getID) === "function") { + this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID(); + } else { + this.lastConnectedRuntime = ""; + } + Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", + this.lastConnectedRuntime); + }, + /********** PROJECTS **********/ // Panel & button diff --git a/browser/devtools/webide/modules/app-manager.js b/browser/devtools/webide/modules/app-manager.js index dd4792e9cf3c..73a504eb0dfe 100644 --- a/browser/devtools/webide/modules/app-manager.js +++ b/browser/devtools/webide/modules/app-manager.js @@ -651,7 +651,15 @@ exports.AppManager = AppManager = { let r = new USBRuntime(id); this.runtimeList.usb.push(r); r.updateNameFromADB().then( - () => this.update("runtimelist"), () => {}); + () => { + this.update("runtimelist"); + // Also update the runtime button label, if the currently selected + // runtime name changes + if (r == this.selectedRuntime) { + this.update("runtime"); + } + }, + () => {}); } this.update("runtimelist"); }, diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js index b17dbe49fd06..e9c667457ea0 100644 --- a/browser/devtools/webide/modules/runtimes.js +++ b/browser/devtools/webide/modules/runtimes.js @@ -13,11 +13,21 @@ const promise = require("promise"); const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties"); +// These type strings are used for logging events to Telemetry +let RuntimeTypes = { + usb: "USB", + wifi: "WIFI", + simulator: "SIMULATOR", + remote: "REMOTE", + local: "LOCAL" +}; + function USBRuntime(id) { this.id = id; } USBRuntime.prototype = { + type: RuntimeTypes.usb, connect: function(connection) { let device = Devices.getByName(this.id); if (!device) { @@ -59,6 +69,7 @@ function WiFiRuntime(deviceName) { } WiFiRuntime.prototype = { + type: RuntimeTypes.wifi, connect: function(connection) { let service = discovery.getRemoteService("devtools", this.deviceName); if (!service) { @@ -82,6 +93,7 @@ function SimulatorRuntime(version) { } SimulatorRuntime.prototype = { + type: RuntimeTypes.simulator, connect: function(connection) { let port = ConnectionManager.getFreeTCPPort(); let simulator = Simulator.getByVersion(this.version); @@ -105,6 +117,7 @@ SimulatorRuntime.prototype = { } let gLocalRuntime = { + type: RuntimeTypes.local, connect: function(connection) { if (!DebuggerServer.initialized) { DebuggerServer.init(); @@ -118,9 +131,13 @@ let gLocalRuntime = { getName: function() { return Strings.GetStringFromName("local_runtime"); }, + getID: function () { + return "local"; + } } let gRemoteRuntime = { + type: RuntimeTypes.remote, connect: function(connection) { let win = Services.wm.getMostRecentWindow("devtools:webide"); if (!win) { diff --git a/browser/devtools/webide/test/chrome.ini b/browser/devtools/webide/test/chrome.ini index 9af2900f9c11..65cc7281f98f 100644 --- a/browser/devtools/webide/test/chrome.ini +++ b/browser/devtools/webide/test/chrome.ini @@ -31,3 +31,4 @@ support-files = [test_manifestUpdate.html] [test_addons.html] [test_deviceinfo.html] +[test_autoconnect_runtime.html] diff --git a/browser/devtools/webide/test/test_autoconnect_runtime.html b/browser/devtools/webide/test/test_autoconnect_runtime.html new file mode 100644 index 000000000000..6087a98af97f --- /dev/null +++ b/browser/devtools/webide/test/test_autoconnect_runtime.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + diff --git a/browser/devtools/webide/webide-prefs.js b/browser/devtools/webide/webide-prefs.js index 3c828ee27ae1..6f6807e4f571 100644 --- a/browser/devtools/webide/webide-prefs.js +++ b/browser/devtools/webide/webide-prefs.js @@ -15,3 +15,4 @@ pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozil pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi"); pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org"); pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000"); +pref("devtools.webide.lastConnectedRuntime", ""); diff --git a/browser/themes/shared/devtools/webaudioeditor.inc.css b/browser/themes/shared/devtools/webaudioeditor.inc.css index 95faced4d4db..9e33f2b8e6e5 100644 --- a/browser/themes/shared/devtools/webaudioeditor.inc.css +++ b/browser/themes/shared/devtools/webaudioeditor.inc.css @@ -105,9 +105,9 @@ g.edgePath.param-connection { fill: #4c9ed9; /* Select Highlight Blue */ } -/* Text in nodes */ +/* Text in nodes and edges */ text { - cursor: pointer; + cursor: default; /* override the "text" cursor */ font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; @@ -123,6 +123,10 @@ text { fill: #f0f1f2; /* Toolbars */ } +.nodes text { + cursor: pointer; +} + /** * Inspector Styles */ diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index eb4d956c9448..77eea56ea0f6 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -283,6 +283,13 @@ pref("browser.search.official", true); // Control media casting feature pref("browser.casting.enabled", true); +#ifdef RELEASE_BUILD +pref("browser.mirroring.enabled", false); +pref("browser.mirroring.enabled.roku", false); +#else +pref("browser.mirroring.enabled", true); +pref("browser.mirroring.enabled.roku", true); +#endif // Enable sparse localization by setting a few package locale overrides pref("chrome.override_package.global", "browser"); diff --git a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml index 4b05034e3330..3a84e4b83627 100644 --- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml +++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml @@ -26,7 +26,7 @@ - - diff --git a/mobile/android/base/resources/values-land/layout.xml b/mobile/android/base/resources/values-land/layout.xml index 863376cabcb1..00cc1d624540 100644 --- a/mobile/android/base/resources/values-land/layout.xml +++ b/mobile/android/base/resources/values-land/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_cell + @layout/tabs_item_cell diff --git a/mobile/android/base/resources/values-large-v11/layout.xml b/mobile/android/base/resources/values-large-v11/layout.xml index 863376cabcb1..00cc1d624540 100644 --- a/mobile/android/base/resources/values-large-v11/layout.xml +++ b/mobile/android/base/resources/values-large-v11/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_cell + @layout/tabs_item_cell diff --git a/mobile/android/base/resources/values/layout.xml b/mobile/android/base/resources/values/layout.xml index 9823217a76bc..854537a2ed91 100644 --- a/mobile/android/base/resources/values/layout.xml +++ b/mobile/android/base/resources/values/layout.xml @@ -4,5 +4,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - @layout/tabs_item_row + @layout/tabs_item_row diff --git a/mobile/android/base/tabs/TabsLayoutAdapter.java b/mobile/android/base/tabs/TabsLayoutAdapter.java index 6baaefdfa6d5..26cbcc27165c 100644 --- a/mobile/android/base/tabs/TabsLayoutAdapter.java +++ b/mobile/android/base/tabs/TabsLayoutAdapter.java @@ -81,7 +81,7 @@ public class TabsLayoutAdapter extends BaseAdapter { } View newView(int position, ViewGroup parent) { - final View view = mInflater.inflate(R.layout.tabs_row, parent, false); + final View view = mInflater.inflate(R.layout.tabs_layout_item_view, parent, false); final TabsLayoutItemView item = new TabsLayoutItemView(view); view.setTag(item); return view; diff --git a/mobile/android/base/tabs/TabsPanel.java b/mobile/android/base/tabs/TabsPanel.java index 7e09fbeb7068..58a85c77abd4 100644 --- a/mobile/android/base/tabs/TabsPanel.java +++ b/mobile/android/base/tabs/TabsPanel.java @@ -77,7 +77,7 @@ public class TabsPanel extends LinearLayout private final GeckoApp mActivity; private final LightweightTheme mTheme; private RelativeLayout mHeader; - private TabsListContainer mTabsContainer; + private PanelViewContainer mPanelsContainer; private PanelView mPanel; private PanelView mPanelNormal; private PanelView mPanelPrivate; @@ -137,7 +137,7 @@ public class TabsPanel extends LinearLayout private void initialize() { mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header); - mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container); + mPanelsContainer = (PanelViewContainer) findViewById(R.id.tabs_container); mPanelNormal = (PanelView) findViewById(R.id.normal_tabs); mPanelNormal.setTabsPanel(this); @@ -264,10 +264,10 @@ public class TabsPanel extends LinearLayout return mActivity.onOptionsItemSelected(item); } - private static int getTabContainerHeight(TabsListContainer listContainer) { - Resources resources = listContainer.getContext().getResources(); + private static int getPanelsContainerHeight(PanelViewContainer panelsContainer) { + Resources resources = panelsContainer.getContext().getResources(); - PanelView panelView = listContainer.getCurrentPanelView(); + PanelView panelView = panelsContainer.getCurrentPanelView(); if (panelView != null && !panelView.shouldExpand()) { return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height); } @@ -276,7 +276,7 @@ public class TabsPanel extends LinearLayout int screenHeight = resources.getDisplayMetrics().heightPixels; Rect windowRect = new Rect(); - listContainer.getWindowVisibleDisplayFrame(windowRect); + panelsContainer.getWindowVisibleDisplayFrame(windowRect); int windowHeight = windowRect.bottom - windowRect.top; // The web content area should have at least 1.5x the height of the action bar. @@ -323,9 +323,9 @@ public class TabsPanel extends LinearLayout onLightweightThemeChanged(); } - // Tabs List Container holds the ListView - static class TabsListContainer extends FrameLayout { - public TabsListContainer(Context context, AttributeSet attrs) { + // Panel View Container holds the ListView + static class PanelViewContainer extends FrameLayout { + public PanelViewContainer(Context context, AttributeSet attrs) { super(context, attrs); } @@ -346,7 +346,7 @@ public class TabsPanel extends LinearLayout @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!GeckoAppShell.getGeckoInterface().hasTabsSideBar()) { - int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsListContainer.this), MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(getPanelsContainerHeight(PanelViewContainer.this), MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightSpec); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -468,7 +468,7 @@ public class TabsPanel extends LinearLayout dispatchLayoutChange(getWidth(), getHeight()); } else { int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height); - int height = actionBarHeight + getTabContainerHeight(mTabsContainer); + int height = actionBarHeight + getPanelsContainerHeight(mPanelsContainer); dispatchLayoutChange(getWidth(), height); } mHeaderVisible = true; @@ -526,13 +526,13 @@ public class TabsPanel extends LinearLayout final int tabsPanelWidth = getWidth(); if (mVisible) { ViewHelper.setTranslationX(mHeader, -tabsPanelWidth); - ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth); + ViewHelper.setTranslationX(mPanelsContainer, -tabsPanelWidth); // The footer view is only present on the sidebar, v11+. ViewHelper.setTranslationX(mFooter, -tabsPanelWidth); } final int translationX = (mVisible ? 0 : -tabsPanelWidth); - animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX); + animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX); animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX); animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX); @@ -542,16 +542,16 @@ public class TabsPanel extends LinearLayout final int translationY = (mVisible ? 0 : -toolbarHeight); if (mVisible) { ViewHelper.setTranslationY(mHeader, -toolbarHeight); - ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight); - ViewHelper.setAlpha(mTabsContainer, 0.0f); + ViewHelper.setTranslationY(mPanelsContainer, -toolbarHeight); + ViewHelper.setAlpha(mPanelsContainer, 0.0f); } - animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f); - animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY); + animator.attach(mPanelsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f); + animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY); animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY); } mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mPanelsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); } public void finishTabsAnimation() { @@ -560,7 +560,7 @@ public class TabsPanel extends LinearLayout } mHeader.setLayerType(View.LAYER_TYPE_NONE, null); - mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null); + mPanelsContainer.setLayerType(View.LAYER_TYPE_NONE, null); // If the tray is now hidden, call hide() on current panel and unset it as the current panel // to avoid hide() being called again when the tray is opened next. diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js index d20d7c96ddca..3598204cb5f5 100644 --- a/mobile/android/chrome/content/CastingApps.js +++ b/mobile/android/chrome/content/CastingApps.js @@ -16,6 +16,7 @@ var rokuDevice = { Cu.import("resource://gre/modules/RokuApp.jsm"); return new RokuApp(aService); }, + mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"), types: ["video/mp4"], extensions: ["mp4"] }; @@ -52,7 +53,7 @@ var CastingApps = { mirrorStopMenuId: -1, init: function ca_init() { - if (!this.isEnabled()) { + if (!this.isCastingEnabled()) { return; } @@ -99,22 +100,25 @@ var CastingApps = { NativeWindow.contextmenus.remove(this._castMenuId); }, + _mirrorStarted: function(stopMirrorCallback) { + this.stopMirrorCallback = stopMirrorCallback; + NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false }); + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true }); + }, + serviceAdded: function(aService) { - if (aService.mirror && this.mirrorStartMenuId == -1) { + if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) { this.mirrorStartMenuId = NativeWindow.menu.add({ name: Strings.browser.GetStringFromName("casting.mirrorTab"), callback: function() { - function callbackFunc(aService) { + let callbackFunc = function(aService) { let app = SimpleServiceDiscovery.findAppForService(aService); - if (app) - app.mirror(function() { - }); - } + if (app) { + app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this)); + } + }.bind(this); - function filterFunc(aService) { - return aService.mirror == true; - } - this.prompt(callbackFunc, filterFunc); + this.prompt(callbackFunc, aService => aService.mirror); }.bind(this), parent: NativeWindow.menu.toolsMenuID }); @@ -125,6 +129,9 @@ var CastingApps = { if (this.tabMirror) { this.tabMirror.stop(); this.tabMirror = null; + } else if (this.stopMirrorCallback) { + this.stopMirrorCallback(); + this.stopMirrorCallback = null; } NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true }); NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); @@ -132,7 +139,9 @@ var CastingApps = { parent: NativeWindow.menu.toolsMenuID }); } - NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + if (this.mirrorStartMenuId != -1) { + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + } }, serviceLost: function(aService) { @@ -150,10 +159,14 @@ var CastingApps = { } }, - isEnabled: function isEnabled() { + isCastingEnabled: function isCastingEnabled() { return Services.prefs.getBoolPref("browser.casting.enabled"); }, + isMirroringEnabled: function isMirroringEnabled() { + return Services.prefs.getBoolPref("browser.mirroring.enabled"); + }, + observe: function (aSubject, aTopic, aData) { switch (aTopic) { case "Casting:Play": diff --git a/mobile/android/chrome/content/WebrtcUI.js b/mobile/android/chrome/content/WebrtcUI.js index 9e6e717cd395..854e85f6b204 100644 --- a/mobile/android/chrome/content/WebrtcUI.js +++ b/mobile/android/chrome/content/WebrtcUI.js @@ -4,14 +4,9 @@ "use strict"; XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "contentPrefs", - "@mozilla.org/content-pref/service;1", - "nsIContentPrefService2"); var WebrtcUI = { _notificationId: null, - VIDEO_SOURCE: "videoSource", - AUDIO_SOURCE: "audioDevice", observe: function(aSubject, aTopic, aData) { if (aTopic === "getUserMedia:request") { @@ -83,42 +78,39 @@ var WebrtcUI = { contentWindow.navigator.mozGetUserMediaDevices( constraints, - function (aDevices) { - WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio, constraints.video, aDevices); + function (devices) { + WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio, + constraints.video, devices); }, - Cu.reportError, aSubject.innerWindowID); + function (error) { + Cu.reportError(error); + }, + aSubject.innerWindowID); }, - getDeviceButtons: function(aAudioDevices, aVideoDevices, aCallID, aHost) { + getDeviceButtons: function(audioDevices, videoDevices, aCallID) { return [{ label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"), - callback: () => { + callback: function() { Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID); } - }, { + }, + { label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"), - callback: (checked /* ignored */, inputs) => { + callback: function(checked /* ignored */, inputs) { let allowedDevices = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray); let audioId = 0; - if (inputs && inputs[this.AUDIO_SOURCE] != undefined) { - audioId = inputs[this.AUDIO_SOURCE]; - } - - if (aAudioDevices[audioId]) { - allowedDevices.AppendElement(aAudioDevices[audioId]); - this.setDefaultDevice(this.AUDIO_SOURCE, aAudioDevices[audioId].name, aHost); - } + if (inputs && inputs.audioDevice != undefined) + audioId = inputs.audioDevice; + if (audioDevices[audioId]) + allowedDevices.AppendElement(audioDevices[audioId]); let videoId = 0; - if (inputs && inputs[this.VIDEO_SOURCE] != undefined) { - videoId = inputs[this.VIDEO_SOURCE]; - } - - if (aVideoDevices[videoId]) { - allowedDevices.AppendElement(aVideoDevices[videoId]); - this.setDefaultDevice(this.VIDEO_SOURCE, aVideoDevices[videoId].name, aHost); - } + if (inputs && inputs.videoSource != undefined) + videoId = inputs.videoSource; + if (videoDevices[videoId]) + allowedDevices.AppendElement(videoDevices[videoId]); Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); } @@ -129,34 +121,30 @@ var WebrtcUI = { _getList: function(aDevices, aType) { let defaultCount = 0; return aDevices.map(function(device) { - let name = device.name; - // if this is a Camera input, convert the name to something readable - let res = /Camera\ \d+,\ Facing (front|back)/.exec(name); - if (res) { - return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera"); - } + // if this is a Camera input, convert the name to something readable + let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name); + if (res) + return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera"); - if (name.startsWith("&") && name.endsWith(";")) { - return Strings.browser.GetStringFromName(name.substring(1, name.length -1)); - } + if (device.name.startsWith("&") && device.name.endsWith(";")) + return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1)); - if (name.trim() == "") { - defaultCount++; - return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1); - } - - return name; - }, this); + if (device.name.trim() == "") { + defaultCount++; + return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1); + } + return device.name + }, this); }, - _addDevicesToOptions: function(aDevices, aType, aOptions, aHost, aContext) { - if (aDevices.length == 0) { - return Promise.resolve(aOptions); - } + _addDevicesToOptions: function(aDevices, aType, aOptions, extraOptions) { + if (aDevices.length) { - let updateOptions = () => { // Filter out empty items from the list let list = this._getList(aDevices, aType); + if (extraOptions) + list = list.concat(extraOptions); + if (list.length > 0) { aOptions.inputs.push({ id: aType, @@ -164,96 +152,40 @@ var WebrtcUI = { label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"), values: list }); + } - - return aOptions; - } - - return this.getDefaultDevice(aType, aHost, aContext).then((defaultDevice) => { - aDevices.sort((a, b) => { - if (b.name === defaultDevice) return 1; - return 0; - }); - return updateOptions(); - }).catch(updateOptions); - }, - - // Sets the default for a aHost. If no aHost is specified, sets the browser wide default. - // Saving is async, but this doesn't wait for a result. - setDefaultDevice: function(aType, aValue, aHost, aContext) { - if (aHost) { - contentPrefs.set(aHost, "webrtc." + aType, aValue, aContext); - } else { - contentPrefs.setGlobal("webrtc." + aType, aValue, aContext); } }, - _checkContentPref(aHost, aType, aContext) { - return new Promise((resolve, reject) => { - let result = null; - let handler = { - handleResult: (aResult) => result = aResult, - handleCompletion: function(aReason) { - if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK && - result instanceof Components.interfaces.nsIContentPref) { - resolve(result.value); - } else { - reject(result); - } - } - }; - - if (aHost) { - contentPrefs.getByDomainAndName(aHost, "webrtc." + aType, aContext, handler); - } else { - contentPrefs.getGlobal("webrtc." + aType, aContext, handler); - } - }); - }, - - // Returns the default device for this aHost. If no aHost is specified, returns a browser wide default - getDefaultDevice: function(aType, aHost, aContext) { - return this._checkContentPref(aHost, aType, aContext).catch(() => { - // If we found nothing for the initial pref, try looking for a global one - return this._checkContentPref(null, aType, aContext); - }); - }, - - prompt: function (aWindow, aCallID, aAudioRequested, aVideoRequested, aDevices) { + prompt: function prompt(aContentWindow, aCallID, aAudioRequested, + aVideoRequested, aDevices) { let audioDevices = []; let videoDevices = []; - - // Split up all the available aDevices into audio and video categories for (let device of aDevices) { device = device.QueryInterface(Ci.nsIMediaDevice); switch (device.type) { case "audio": - if (aAudioRequested) { + if (aAudioRequested) audioDevices.push(device); - } break; case "video": - if (aVideoRequested) { + if (aVideoRequested) videoDevices.push(device); - } break; } } - // Bsaed on the aTypes available, setup the prompt and icon text let requestType; - if (audioDevices.length && videoDevices.length) { + if (audioDevices.length && videoDevices.length) requestType = "CameraAndMicrophone"; - } else if (audioDevices.length) { + else if (audioDevices.length) requestType = "Microphone"; - } else if (videoDevices.length) { + else if (videoDevices.length) requestType = "Camera"; - } else { + else return; - } - let host = aWindow.document.documentURIObject.host; - // Show the app name if this is a WebRT app, otherwise show the host. + let host = aContentWindow.document.documentURIObject.host; let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host; let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1); @@ -261,20 +193,24 @@ var WebrtcUI = { // if the users only option would be to select "No Audio" or "No Video" // i.e. we're only showing audio or only video and there is only one device for that type // don't bother showing a menulist to select from - if (videoDevices.length > 0 && audioDevices.length > 0) { - videoDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.videoSource.none") }); - audioDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") }); + var extraItems = null; + if (videoDevices.length > 1 || audioDevices.length > 0) { + // Only show the No Video option if there are also Audio devices to choose from + if (audioDevices.length > 0) + extraItems = [ Strings.browser.GetStringFromName("getUserMedia.videoSource.none") ]; + // videoSource is both the string used for l10n lookup and the object that will be returned + this._addDevicesToOptions(videoDevices, "videoSource", options, extraItems); } - let loadContext = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsILoadContext); - // videoSource is both the string used for l10n lookup and the object that will be returned - this._addDevicesToOptions(videoDevices, this.VIDEO_SOURCE, options, host, loadContext).then((aOptions) => { - return this._addDevicesToOptions(audioDevices, this.AUDIO_SOURCE, aOptions, host, loadContext); - }).catch(Cu.reportError).then((aOptions) => { - let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, host); - NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, aOptions); - }); + if (audioDevices.length > 1 || videoDevices.length > 0) { + // Only show the No Audio option if there are also Video devices to choose from + if (videoDevices.length > 0) + extraItems = [ Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") ]; + this._addDevicesToOptions(audioDevices, "audioDevice", options, extraItems); + } + + let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID); + + NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options); } } diff --git a/mobile/android/modules/RokuApp.jsm b/mobile/android/modules/RokuApp.jsm index 8fb5dfdd4177..bcd637162c6b 100644 --- a/mobile/android/modules/RokuApp.jsm +++ b/mobile/android/modules/RokuApp.jsm @@ -11,6 +11,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); +const WEBRTC_PLAYER_NAME = "WebRTC Player"; +const MIRROR_PORT = 8011; +const JSON_MESSAGE_TERMINATOR = "\r\n"; + function log(msg) { //Services.console.logStringMessage(msg); } @@ -29,13 +33,14 @@ function RokuApp(service) { #else this.app = "Firefox Nightly"; #endif - this.appID = -1; + this.mediaAppID = -1; + this.mirrorAppID = -1; } RokuApp.prototype = { status: function status(callback) { // We have no way to know if the app is running, so just return "unknown" - // but we use this call to fetch the appID for the given app name + // but we use this call to fetch the mediaAppID for the given app name let url = this.resourceURL + "query/apps"; let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", url, true); @@ -48,7 +53,9 @@ RokuApp.prototype = { let apps = doc.querySelectorAll("app"); for (let app of apps) { if (app.textContent == this.app) { - this.appID = app.id; + this.mediaAppID = app.id; + } else if (app.textContent == WEBRTC_PLAYER_NAME) { + this.mirrorAppID = app.id } } } @@ -69,11 +76,11 @@ RokuApp.prototype = { }, start: function start(callback) { - // We need to make sure we have cached the appID - if (this.appID == -1) { + // We need to make sure we have cached the mediaAppID + if (this.mediaAppID == -1) { this.status(function() { - // If we found the appID, use it to make a new start call - if (this.appID != -1) { + // If we found the mediaAppID, use it to make a new start call + if (this.mediaAppID != -1) { this.start(callback); } else { // We failed to start the app, so let the caller know @@ -85,7 +92,7 @@ RokuApp.prototype = { // Start a given app with any extra query data. Each app uses it's own data scheme. // NOTE: Roku will also pass "source=external-control" as a param - let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION); + let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION); let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("POST", url, true); xhr.overrideMimeType("text/plain"); @@ -129,7 +136,7 @@ RokuApp.prototype = { }, remoteMedia: function remoteMedia(callback, listener) { - if (this.appID != -1) { + if (this.mediaAppID != -1) { if (callback) { callback(new RemoteMedia(this.resourceURL, listener)); } @@ -138,6 +145,44 @@ RokuApp.prototype = { callback(); } } + }, + + mirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // The status function may not have been called yet if mirrorAppID is -1 + this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback)); + } else { + this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback); + } + }, + + _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // TODO: Inform user to install Roku WebRTC Player Channel. + log("RokuApp: Failed to find Mirror App ID."); + } else { + let url = this.resourceURL + "launch/" + this.mirrorAppID; + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + xhr.open("POST", url, true); + xhr.overrideMimeType("text/plain"); + + xhr.addEventListener("load", (function() { + // 204 seems to be returned if the channel is already running + if ((xhr.status == 200) || (xhr.status == 204)) { + this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback); + } + }).bind(this), false); + + xhr.addEventListener("error", function() { + log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME); + }, false); + + xhr.send(null); + } + + if (callback) { + callback(); + } } } @@ -225,11 +270,153 @@ RemoteMedia.prototype = { this._sendMsg({ type: "STOP" }); }, - load: function load(aData) { - this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster }); + load: function load(data) { + this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster }); }, get status() { return this._status; } } + +function RemoteMirror(url, win, viewport, mirrorStartedCallback) { + this._serverURI = Services.io.newURI(url , null, null); + this._window = win; + this._iceCandidates = []; + this.mirrorStarted = mirrorStartedCallback; + + // This code insures the generated tab mirror is not wider than 800 nor taller than 600 + // Better dimensions should be chosen after the Roku Channel is working. + let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + let cWidth = Math.max(viewport.cssWidth, viewport.width); + let cHeight = Math.max(viewport.cssHeight, viewport.height); + + const MAX_WIDTH = 800; + const MAX_HEIGHT = 600; + + let tWidth = 0; + let tHeight = 0; + + if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) { + tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight); + tWidth = MAX_WIDTH; + } else { + tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth); + tHeight = MAX_HEIGHT; + } + + let constraints = { + video: { + mediaSource: "browser", + browserWindow: windowId, + scrollWithPage: true, + advanced: [ + { + width: { min: tWidth, max: tWidth }, + height: { min: tHeight, max: tHeight } + }, + { aspectRatio: cWidth / cHeight } + ] + } + }; + + this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {}); +} + +RemoteMirror.prototype = { + _sendOffer: function(offer) { + if (!this._baseSocket) { + this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket); + } + this._jsonOffer = JSON.stringify(offer); + this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" }); + this._socket.onopen = this._onSocketOpen.bind(this); + this._socket.ondata = this._onSocketData.bind(this); + this._socket.onerror = this._onSocketError.bind(this); + }, + + _onReceiveGUMStream: function(stream) { + this._pc = new this._window.mozRTCPeerConnection; + this._pc.addStream(stream); + this._pc.onicecandidate = (evt => { + // Usually the last candidate is null, expected? + if (!evt.candidate) { + return; + } + let jsonCandidate = JSON.stringify(evt.candidate); + this._iceCandidates.push(jsonCandidate); + this._sendIceCandidates(); + }); + + this._pc.createOffer(offer => { + this._pc.setLocalDescription( + new this._window.mozRTCSessionDescription(offer), + () => this._sendOffer(offer), + () => log("RemoteMirror: Failed to set local description.")); + }, + () => log("RemoteMirror: Failed to create offer.")); + }, + + _stopMirror: function() { + if (this._socket) { + this._socket.close(); + this._socket = null; + } + if (this._pc) { + this._pc.close(); + this._pc = null; + } + this._jsonOffer = null; + this._iceCandidates = []; + }, + + _onSocketData: function(response) { + if (response.type == "data") { + response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => { + if (data) { + let parsedData = JSON.parse(data); + if (parsedData.type == "answer") { + this._pc.setRemoteDescription( + new this._window.mozRTCSessionDescription(parsedData), + () => this.mirrorStarted(this._stopMirror.bind(this)), + () => log("RemoteMirror: Failed to set remote description.")); + } else { + this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData)) + } + } else { + log("RemoteMirror: data is null"); + } + }); + } else if (response.type == "error") { + log("RemoteMirror: Got socket error."); + this._stopMirror(); + } else { + log("RemoteMirror: Got unhandled socket event: " + response.type); + } + }, + + _onSocketError: function(err) { + log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA")); + this._stopMirror(); + }, + + _onSocketOpen: function() { + this._open = true; + if (this._jsonOffer) { + let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR; + this._socket.send(jsonOffer, jsonOffer.length); + this._jsonOffer = null; + this._sendIceCandidates(); + } + }, + + _sendIceCandidates: function() { + if (this._socket && this._open) { + this._iceCandidates.forEach(value => { + value = value + JSON_MESSAGE_TERMINATOR; + this._socket.send(value, value.length); + }); + this._iceCandidates = []; + } + } +}; diff --git a/mobile/android/modules/SimpleServiceDiscovery.jsm b/mobile/android/modules/SimpleServiceDiscovery.jsm index d23295b96a75..ca352e205b31 100644 --- a/mobile/android/modules/SimpleServiceDiscovery.jsm +++ b/mobile/android/modules/SimpleServiceDiscovery.jsm @@ -409,6 +409,10 @@ var SimpleServiceDiscovery = { // Only add and notify if we don't already know about this service if (!this._services.has(service.uuid)) { + let device = this._devices.get(service.target); + if (device && device.mirror) { + service.mirror = true; + } this._services.set(service.uuid, service); Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid); } diff --git a/toolkit/components/places/Bookmarks.jsm b/toolkit/components/places/Bookmarks.jsm new file mode 100644 index 000000000000..88818bdd4e67 --- /dev/null +++ b/toolkit/components/places/Bookmarks.jsm @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This module provides an asynchronous API for managing bookmarks. + * + * Bookmarks are organized in a tree structure, and can be bookmarked URIs, + * folders or separators. Multiple bookmarks for the same URI are allowed. + * + * Note that if you are handling bookmarks operations in the UI, you should + * not use this API directly, but rather use PlacesTransactions.jsm, so that + * any operation is undo/redo-able. + * + * Each bookmarked item is represented by an object having the following + * properties: + * + * - guid (string) + * The globally unique identifier of the item. + * - parentGuid (string) + * The globally unique identifier of the folder containing the item. + * This will be an empty string for the Places root folder. + * - index (number) + * The 0-based position of the item in the parent folder. + * - dateAdded (number, microseconds from the epoch) + * The time at which the item was added. This is a PRTime (microseconds). + * - lastModified (number, microseconds from the epoch) + * The time at which the item was last modified. This is a PRTime (microseconds). + * - type (number) + * The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR. + * + * The following properties are only valid for bookmarks or folders. + * + * - title (string) + * The item's title, if any. Empty titles and null titles are considered + * the same and the property is unset on retrieval in such a case. + * + * The following properties are only valid for bookmarks: + * + * - uri (nsIURI) + * The item's URI. + * - keyword (string) + * The associated keyword, if any. + * + * Each successful operation notifies through the nsINavBookmarksObserver + * interface. To listen to such notifications you must register using + * nsINavBookmarksService addObserver and removeObserver methods. + * Note that bookmark addition or order changes won't notify onItemMoved for + * items that have their indexes changed. + * Similarly, lastModified changes not done explicitly (like changing another + * property) won't fire an onItemChanged notification for the lastModified + * property. + * @see nsINavBookmarkObserver + * + * @note livemarks are implemented as empty folders. + * @see mozIAsyncLivemarks.idl + */ + +this.EXPORTED_SYMBOLS = [ "Bookmarks" ]; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", + "resource://gre/modules/Sqlite.jsm"); + +const URI_LENGTH_MAX = 65536; +const TITLE_LENGTH_MAX = 4096; + +let Bookmarks = Object.freeze({ + /** + * Item's type constants. + * These should stay consistent with nsINavBookmarksService.idl + */ + TYPE_BOOKMARK: 1, + TYPE_FOLDER: 2, + TYPE_SEPARATOR: 3, + + /** + * Creates or updates a bookmarked item. + * + * If the given guid is found the corresponding item is updated, otherwise, + * if no guid is provided, a bookmark is created and a new guid is assigned + * to it. + * + * In the creation case, a minimum set of properties must be provided: + * - type + * - parentGuid + * - URI, only for bookmarks + * If an index is not specified, it defaults to appending. + * It's also possible to pass a non-existent guid to force creation of an + * item with the given guid, but unless you have a very sound reason, such as + * an undo manager implementation or synchronization, you should not do that. + * + * In the update case, you should only set the properties which should be + * changed, undefined properties won't be taken into account for the update. + * Moreover, the item's type and the guid are ignored, since they are + * immutable after creation. Note that if the passed in values are not + * coherent with the known values, this rejects. + * Passing null or an empty string as keyword clears any keyword + * associated with this bookmark. + * + * Note that any known property that doesn't apply to the specific item type + * causes rejection. + * + * @param info + * object representing a bookmarked item, as defined above. + * + * @return {Promise} resolved when the update is complete. + * @resolves to the input object, updated with relevant information. + * @rejects JavaScript exception. + * + * @note title is truncated to TITLE_LENGTH_MAX and URI is rejected if + * greater than URI_LENGTH_MAX. + */ + // XXX WIP XXX Will replace functionality from these methods: + // long long insertBookmark(in long long aParentId, in nsIURI aURI, in long aIndex, in AUTF8String aTitle, [optional] in ACString aGUID); + // long long createFolder(in long long aParentFolder, in AUTF8String name, in long index, [optional] in ACString aGUID); + // void moveItem(in long long aItemId, in long long aNewParentId, in long aIndex); + // long long insertSeparator(in long long aParentId, in long aIndex, [optional] in ACString aGUID); + // void setItemTitle(in long long aItemId, in AUTF8String aTitle); + // void setItemDateAdded(in long long aItemId, in PRTime aDateAdded); + // void setItemLastModified(in long long aItemId, in PRTime aLastModified); + // void changeBookmarkURI(in long long aItemId, in nsIURI aNewURI); + // void setKeywordForBookmark(in long long aItemId, in AString aKeyword); + update: Task.async(function* (info) { + throw new Error("Not yet implemented"); + }), + + /** + * Removes a bookmarked item. + * + * Input can either be a guid or an object with one of the following + * properties set: + * - guid: if set, only the corresponding item is removed. + * - parentGuid: if it's set and is a folder, any children of that folder is + * removed, but not the folder itself. + * - URI: if set, any bookmark for that URI is removed. + * If multiple of these properties are set, the method rejects. + * + * Any other property is ignored, known properties may be overwritten. + * + * @param guidOrInfo + * The globally unique identifier of the item to remove, or an + * object representing it, as defined above. + * + * @return {Promise} resolved when the removal is complete. + * @resolves to the removed object or an array of them. + * @rejects JavaScript exception. + */ + // XXX WIP XXX Will replace functionality from these methods: + // removeItem(in long long aItemId); + // removeFolderChildren(in long long aItemId); + remove: Task.async(function* (guidOrInfo) { + throw new Error("Not yet implemented"); + }), + + /** + * Fetches information about a bookmarked item. + * + * Input can be either a guid or an object with one, and only one, of these + * filtering properties set: + * - guid + * retrieves the item with the specified guid + * - parentGuid and index + * retrieves the item by its position + * - URI + * retrieves all items having the given URI. + * - keyword + * retrieves all items having the given keyword. + * + * Any other property is ignored. Known properties may be overwritten. + * + * @param guidOrInfo + * The globally unique identifier of the item to fetch, or an + * object representing it, as defined above. + * + * @return {Promise} resolved when the fetch is complete. + * @resolves to an object representing the found item, as described above, or + * an array of such objects. if no item is found, the returned + * promise is resolved to null. + * @rejects JavaScript exception. + */ + // XXX WIP XXX Will replace functionality from these methods: + // long long getIdForItemAt(in long long aParentId, in long aIndex); + // AUTF8String getItemTitle(in long long aItemId); + // PRTime getItemDateAdded(in long long aItemId); + // PRTime getItemLastModified(in long long aItemId); + // nsIURI getBookmarkURI(in long long aItemId); + // long getItemIndex(in long long aItemId); + // unsigned short getItemType(in long long aItemId); + // boolean isBookmarked(in nsIURI aURI); + // long long getFolderIdForItem(in long long aItemId); + // void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count, [array, retval, size_is(count)] out long long bookmarks); + // AString getKeywordForURI(in nsIURI aURI); + // AString getKeywordForBookmark(in long long aItemId); + // nsIURI getURIForKeyword(in AString keyword); + fetch: Task.async(function* (guidOrInfo) { + throw new Error("Not yet implemented"); + }), + + /** + * Retrieves an object representation of a bookmarked item, along with all of + * its descendants, if any. + * + * Each node in the tree is an object that extends + * the item representation described above with some additional properties: + * + * - [deprecated] id (number) + * the item's id. Defined only if aOptions.includeItemIds is set. + * - annos (array) + * the item's annotations. This is not set if there are no annotations + * set for the item. + * + * The root object of the tree also has the following properties set: + * - itemsCount (number, not enumerable) + * the number of items, including the root item itself, which are + * represented in the resolved object. + * + * Bookmarked URIs may also have the following properties: + * - tags (string) + * csv string of the bookmark's tags, if any. + * - charset (string) + * the last known charset of the bookmark, if any. + * - iconuri (string) + * the bookmark's favicon URL, if any. + * + * Folders may also have the following properties: + * - children (array) + * the folder's children information, each of them having the same set of + * properties as above. + * + * @param [optional] guid + * the topmost item to be queried. If it's not passed, the Places + * root folder is queried: that is, you get a representation of the + * entire bookmarks hierarchy. + * @param [optional] options + * Options for customizing the query behavior, in the form of an + * object with any of the following properties: + * - excludeItemsCallback: a function for excluding items, along with + * their descendants. Given an item object (that has everything set + * apart its potential children data), it should return true if the + * item should be excluded. Once an item is excluded, the function + * isn't called for any of its descendants. This isn't called for + * the root item. + * WARNING: since the function may be called for each item, using + * this option can slow down the process significantly if the + * callback does anything that's not relatively trivial. It is + * highly recommended to avoid any synchronous I/O or DB queries. + * - includeItemIds: opt-in to include the deprecated id property. + * Use it if you must. It'll be removed once the switch to guids is + * complete. + * + * @return {Promise} resolved when the fetch is complete. + * @resolves to an object that represents either a single item or a + * bookmarks tree. if guid points to a non-existent item, the + * returned promise is resolved to null. + * @rejects JavaScript exception. + */ + // XXX WIP XXX: will replace functionality for these methods: + // PlacesUtils.promiseBookmarksTree() + fetchTree: Task.async(function* (guid = "", options = {}) { + throw new Error("Not yet implemented"); + }), + + /** + * Reorders contents of a folder based on a provided array of GUIDs. + * + * @param parentGuid + * The globally unique identifier of the folder whose contents should + * be reordered. + * @param orderedChildrenGuids + * Ordered array of the children's GUIDs. If this list contains + * non-existing entries they will be ignored. If the list is + * incomplete, missing entries will be appended. + * + * @return {Promise} resolved when reordering is complete. + * @rejects JavaScript exception. + */ + // XXX WIP XXX Will replace functionality from these methods: + // void setItemIndex(in long long aItemId, in long aNewIndex); + reorder: Task.async(function* (parentGuid, orderedChildrenGuids) { + throw new Error("Not yet implemented"); + }) +}); diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm index b415e37b7178..2ab9c422bec8 100644 --- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -25,33 +25,25 @@ this.EXPORTED_SYMBOLS = [ , "PlacesUntagURITransaction" ]; -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cr = Components.results; -const Cu = Components.utils; +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", "resource://gre/modules/Deprecated.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks", + "resource://gre/modules/Bookmarks.jsm"); // The minimum amount of transactions before starting a batch. Usually we do // do incremental updates, a batch will cause views to completely @@ -1834,9 +1826,14 @@ XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons", "@mozilla.org/browser/favicon-service;1", "mozIAsyncFavicons"); -XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks", - "@mozilla.org/browser/nav-bookmarks-service;1", - "nsINavBookmarksService"); +XPCOMUtils.defineLazyGetter(PlacesUtils, "bookmarks", () => { + let bm = Cc["@mozilla.org/browser/nav-bookmarks-service;1"] + .getService(Ci.nsINavBookmarksService); + return Object.freeze(new Proxy(bm, { + get: (target, name) => target.hasOwnProperty(name) ? target[name] + : Bookmarks[name] + })); +}); XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations", "@mozilla.org/browser/annotation-service;1", diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build index cf2d83d0ea5d..d18e019e82fe 100644 --- a/toolkit/components/places/moz.build +++ b/toolkit/components/places/moz.build @@ -62,6 +62,7 @@ if CONFIG['MOZ_PLACES']: EXTRA_JS_MODULES += [ 'BookmarkHTMLUtils.jsm', 'BookmarkJSONUtils.jsm', + 'Bookmarks.jsm', 'ClusterLib.js', 'ColorAnalyzer_worker.js', 'ColorConversion.js', diff --git a/toolkit/devtools/apps/tests/debugger-protocol-helper.js b/toolkit/devtools/apps/tests/debugger-protocol-helper.js index 0434ee86b0ee..b215932cab0b 100644 --- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js +++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js @@ -7,6 +7,9 @@ const Cu = Components.utils; const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); const { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; + const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm"); const { Services } = Cu.import("resource://gre/modules/Services.jsm"); @@ -114,9 +117,46 @@ addMessageListener("install", function (aMessage) { } }); +addMessageListener("getAppActor", function (aMessage) { + let { manifestURL } = aMessage; + let request = {type: "getAppActor", manifestURL: manifestURL}; + webappActorRequest(request, function (aResponse) { + sendAsyncMessage("appActor", aResponse); + }); +}); + +let Frames = []; +addMessageListener("addFrame", function (aMessage) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let doc = win.document; + let frame = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + frame.setAttribute("mozbrowser", "true"); + if (aMessage.mozapp) { + frame.setAttribute("mozapp", aMessage.mozapp); + } + if (aMessage.remote) { + frame.setAttribute("remote", aMessage.remote); + } + if (aMessage.src) { + frame.setAttribute("src", aMessage.src); + } + doc.documentElement.appendChild(frame); + Frames.push(frame); + sendAsyncMessage("frameAdded"); +}); + addMessageListener("cleanup", function () { webappActorRequest({type: "unwatchApps"}, function () { gClient.close(); }); }); +let AppFramesMock = { + list: function () { + return Frames; + }, + addObserver: function () {}, + removeObserver: function () {} +}; + +require("devtools/server/actors/webapps").setAppFramesMock(AppFramesMock); diff --git a/toolkit/devtools/apps/tests/test_webapps_actor.html b/toolkit/devtools/apps/tests/test_webapps_actor.html index 063217da72c1..84424bd5eb50 100644 --- a/toolkit/devtools/apps/tests/test_webapps_actor.html +++ b/toolkit/devtools/apps/tests/test_webapps_actor.html @@ -224,6 +224,21 @@ var steps = [ info("== SETUP == Disable certified app access"); SpecialPowers.popPrefEnv(next); }, + function() { + info("== TEST == Get packaged app actor"); + addFrame( + { mozapp: PACKAGED_APP_MANIFEST, remote: true }, + function () { + getAppActor(PACKAGED_APP_MANIFEST, function (response) { + let tabActor = response.actor; + ok(!!tabActor, "TabActor is correctly instanciated in child.js"); + ok("actor" in tabActor, "Tab actor is available in child"); + ok("consoleActor" in tabActor, "Console actor is available in child"); + next(); + }); + }); + + }, function() { info("== TEST == Uninstall packaged app"); uninstall(PACKAGED_APP_MANIFEST); @@ -309,6 +324,22 @@ function uninstall(manifestURL) { }); } +function getAppActor(manifestURL, callback) { + mm.addMessageListener("appActor", function onAppActor(aResponse) { + mm.removeMessageListener("appActor", onAppActor); + callback(aResponse); + }); + mm.sendAsyncMessage("getAppActor", { manifestURL: manifestURL }); +} + +function addFrame(options, callback) { + mm.addMessageListener("frameAdded", function onFrameAdded() { + mm.removeMessageListener("frameAdded", onFrameAdded); + callback(); + }); + mm.sendAsyncMessage("addFrame", options); +} + diff --git a/toolkit/devtools/server/actors/webapps.js b/toolkit/devtools/server/actors/webapps.js index 044edf407270..def41829a97b 100644 --- a/toolkit/devtools/server/actors/webapps.js +++ b/toolkit/devtools/server/actors/webapps.js @@ -4,10 +4,7 @@ "use strict"; -let Cu = Components.utils; -let Cc = Components.classes; -let Ci = Components.interfaces; -let CC = Components.Constructor; +let {Cu, Cc, Ci} = require("chrome"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -16,7 +13,22 @@ Cu.import("resource://gre/modules/FileUtils.jsm"); let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); +let { ActorPool } = require("devtools/server/actors/common"); +let { DebuggerServer } = require("devtools/server/main"); +let Services = require("Services"); + +let AppFramesMock = null; + +exports.setAppFramesMock = function (mock) { + AppFramesMock = mock; +} + DevToolsUtils.defineLazyGetter(this, "AppFrames", () => { + // Offer a way for unit test to provide a mock + if (AppFramesMock) { + return AppFramesMock; + } try { return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames; } catch(e) {} @@ -518,11 +530,11 @@ WebappsActor.prototype = { // frame script. That will flush the jar cache for this app and allow // loading fresh updated resources if we reload its document. let FlushFrameScript = function (path) { - let jar = Components.classes["@mozilla.org/file/local;1"] - .createInstance(Components.interfaces.nsILocalFile); + let jar = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); jar.initWithPath(path); - let obs = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); + let obs = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); obs.notifyObservers(jar, "flush-cache-entry", null); }; for each (let frame in self._appFrames()) { @@ -1038,4 +1050,4 @@ WebappsActor.prototype.requestTypes = { "getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL }; -DebuggerServer.addGlobalActor(WebappsActor, "webappsActor"); +exports.WebappsActor = WebappsActor; diff --git a/toolkit/devtools/server/child.js b/toolkit/devtools/server/child.js index ff37fd1b14d0..e2b3e70b99b2 100644 --- a/toolkit/devtools/server/child.js +++ b/toolkit/devtools/server/child.js @@ -4,6 +4,8 @@ "use strict"; +try { + let chromeGlobal = this; // Encapsulate in its own scope to allows loading this frame script @@ -64,3 +66,7 @@ let chromeGlobal = this; }); addMessageListener("debug:disconnect", onDisconnect); })(); + +} catch(e) { + dump("Exception in app child process: " + e + "\n"); +} diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 9ab852f455fd..a9417175c935 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -442,7 +442,11 @@ var DebuggerServer = { }); } - this.addActors("resource://gre/modules/devtools/server/actors/webapps.js"); + this.registerModule("devtools/server/actors/webapps", { + prefix: "webapps", + constructor: "WebappsActor", + type: { global: true } + }); this.registerModule("devtools/server/actors/device", { prefix: "device", constructor: "DeviceActor", diff --git a/toolkit/modules/Sqlite.jsm b/toolkit/modules/Sqlite.jsm index 6a2ec123e654..57d432441e84 100644 --- a/toolkit/modules/Sqlite.jsm +++ b/toolkit/modules/Sqlite.jsm @@ -37,6 +37,12 @@ XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService", // used for logging to distinguish connection instances. let connectionCounters = new Map(); +// Tracks identifiers of wrapped connections, that are Storage connections +// opened through mozStorage and then wrapped by Sqlite.jsm to use its syntactic +// sugar API. Since these connections have an unknown origin, we use this set +// to differentiate their behavior. +let wrappedConnections = new Set(); + /** * Once `true`, reject any attempt to open or close a database. */ @@ -66,6 +72,20 @@ function logScriptError(message) { } } +/** + * Gets connection identifier from its database file path. + * + * @param path + * A file string path pointing to a database file. + * @return the connection identifier. + */ +function getIdentifierByPath(path) { + let basename = OS.Path.basename(path); + let number = connectionCounters.get(basename) || 0; + connectionCounters.set(basename, number + 1); + return basename + "#" + number; +} + /** * Barriers used to ensure that Sqlite.jsm is shutdown after all * its clients. @@ -95,17 +115,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => { * The observer is passed the connection identifier of the database * connection that is being finalized. */ - let finalizationObserver = function (subject, topic, connectionIdentifier) { - let connectionData = ConnectionData.byId.get(connectionIdentifier); + let finalizationObserver = function (subject, topic, identifier) { + let connectionData = ConnectionData.byId.get(identifier); if (connectionData === undefined) { logScriptError("Error: Attempt to finalize unknown Sqlite connection: " + - connectionIdentifier + "\n"); + identifier + "\n"); return; } - ConnectionData.byId.delete(connectionIdentifier); - logScriptError("Warning: Sqlite connection '" + connectionIdentifier + + ConnectionData.byId.delete(identifier); + logScriptError("Warning: Sqlite connection '" + identifier + "' was not properly closed. Auto-close triggered by garbage collection.\n"); connectionData.close(); }; @@ -170,13 +190,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => { * OpenedConnection needs to use the methods in this object, it will * dispatch its method calls here. */ -function ConnectionData(connection, basename, number, options) { - this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename, - "Conn #" + number + ": "); +function ConnectionData(connection, identifier, options={}) { + this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + + identifier + ": "); this._log.info("Opened"); this._dbConn = connection; - this._connectionIdentifier = basename + " Conn #" + number; + + // This is a unique identifier for the connection, generated through + // getIdentifierByPath. It may be used for logging or as a key in Maps. + this._identifier = identifier; + this._open = true; this._cachedStatements = new Map(); @@ -204,10 +228,10 @@ function ConnectionData(connection, basename, number, options) { this._closeRequested = false; Barriers.connections.client.addBlocker( - this._connectionIdentifier + ": waiting for shutdown", + this._identifier + ": waiting for shutdown", this._deferredClose.promise, () => ({ - identifier: this._connectionIdentifier, + identifier: this._identifier, isCloseRequested: this._closeRequested, hasDbConn: !!this._dbConn, hasInProgressTransaction: !!this._inProgressTransaction, @@ -224,7 +248,7 @@ function ConnectionData(connection, basename, number, options) { * database. Used by finalization witnesses to be able to close opened * connections on garbage collection. * - * Key: _connectionIdentifier of ConnectionData + * Key: _identifier of ConnectionData * Value: ConnectionData object */ ConnectionData.byId = new Map(); @@ -304,15 +328,23 @@ ConnectionData.prototype = Object.freeze({ // function and asyncClose() finishing. See also bug 726990. this._open = false; - this._log.debug("Calling asyncClose()."); - this._dbConn.asyncClose(() => { + // We must always close the connection at the Sqlite.jsm-level, not + // necessarily at the mozStorage-level. + let markAsClosed = () => { this._log.info("Closed"); this._dbConn = null; // Now that the connection is closed, no need to keep // a blocker for Barriers.connections. Barriers.connections.client.removeBlocker(deferred.promise); deferred.resolve(); - }); + } + if (wrappedConnections.has(this._identifier)) { + wrappedConnections.delete(this._identifier); + markAsClosed(); + } else { + this._log.debug("Calling asyncClose()."); + this._dbConn.asyncClose(markAsClosed); + } }, executeCached: function (sql, params=null, onRow=null) { @@ -722,12 +754,7 @@ function openConnection(options) { } let file = FileUtils.File(path); - - let basename = OS.Path.basename(path); - let number = connectionCounters.get(basename) || 0; - connectionCounters.set(basename, number + 1); - - let identifier = basename + "#" + number; + let identifier = getIdentifierByPath(path); log.info("Opening database: " + path + " (" + identifier + ")"); let deferred = Promise.defer(); @@ -746,8 +773,8 @@ function openConnection(options) { log.info("Connection opened"); try { deferred.resolve( - new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number, - openedOptions)); + new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), + identifier, openedOptions)); } catch (ex) { log.warn("Could not open database: " + CommonUtils.exceptionStr(ex)); deferred.reject(ex); @@ -759,7 +786,7 @@ function openConnection(options) { /** * Creates a clone of an existing and open Storage connection. The clone has * the same underlying characteristics of the original connection and is - * returned in form of on OpenedConnection handle. + * returned in form of an OpenedConnection handle. * * The following parameters can control the cloned connection: * @@ -812,10 +839,7 @@ function cloneStorageConnection(options) { } let path = source.databaseFile.path; - let basename = OS.Path.basename(path); - let number = connectionCounters.get(basename) || 0; - connectionCounters.set(basename, number + 1); - let identifier = basename + "#" + number; + let identifier = getIdentifierByPath(path); log.info("Cloning database: " + path + " (" + identifier + ")"); let deferred = Promise.defer(); @@ -828,8 +852,7 @@ function cloneStorageConnection(options) { log.info("Connection cloned"); try { let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection); - deferred.resolve(new OpenedConnection(conn, basename, number, - openedOptions)); + deferred.resolve(new OpenedConnection(conn, identifier, openedOptions)); } catch (ex) { log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex)); deferred.reject(ex); @@ -838,6 +861,55 @@ function cloneStorageConnection(options) { return deferred.promise; } +/** + * Wraps an existing and open Storage connection with Sqlite.jsm API. The + * wrapped connection clone has the same underlying characteristics of the + * original connection and is returned in form of an OpenedConnection handle. + * + * Clients are responsible for closing both the Sqlite.jsm wrapper and the + * underlying mozStorage connection. + * + * The following parameters can control the wrapped connection: + * + * connection -- (mozIStorageAsyncConnection) The original Storage connection + * to wrap. + * + * @param options + * (Object) Parameters to control connection and wrap options. + * + * @return Promise + */ +function wrapStorageConnection(options) { + let log = Log.repository.getLogger("Sqlite.ConnectionWrapper"); + + let connection = options && options.connection; + if (!connection || !(connection instanceof Ci.mozIStorageAsyncConnection)) { + throw new TypeError("connection not specified or invalid."); + } + + if (isClosed) { + throw new Error("Sqlite.jsm has been shutdown. Cannot wrap connection to: " + connection.database.path); + } + + let path = connection.databaseFile.path; + let identifier = getIdentifierByPath(path); + + log.info("Wrapping database: " + path + " (" + identifier + ")"); + return new Promise(resolve => { + try { + let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection); + let wrapper = new OpenedConnection(conn, identifier); + // We must not handle shutdown of a wrapped connection, since that is + // already handled by the opener. + wrappedConnections.add(identifier); + resolve(wrapper); + } catch (ex) { + log.warn("Could not wrap database: " + CommonUtils.exceptionStr(ex)); + throw ex; + } + }); +} + /** * Handle on an opened SQLite database. * @@ -877,25 +949,24 @@ function cloneStorageConnection(options) { * * @param connection * (mozIStorageConnection) Underlying SQLite connection. - * @param basename - * (string) The basename of this database name. Used for logging. - * @param number - * (Number) The connection number to this database. - * @param options + * @param identifier + * (string) The unique identifier of this database. It may be used for + * logging or as a key in Maps. + * @param options [optional] * (object) Options to control behavior of connection. See * `openConnection`. */ -function OpenedConnection(connection, basename, number, options) { +function OpenedConnection(connection, identifier, options={}) { // Store all connection data in a field distinct from the // witness. This enables us to store an additional reference to this // field without preventing garbage collection of // OpenedConnection. On garbage collection, we will still be able to // close the database using this extra reference. - this._connectionData = new ConnectionData(connection, basename, number, options); + this._connectionData = new ConnectionData(connection, identifier, options); // Store the extra reference in a map with connection identifier as // key. - ConnectionData.byId.set(this._connectionData._connectionIdentifier, + ConnectionData.byId.set(this._connectionData._identifier, this._connectionData); // Make a finalization witness. If this object is garbage collected @@ -904,7 +975,7 @@ function OpenedConnection(connection, basename, number, options) { // connection identifier string of the database. this._witness = FinalizationWitnessService.make( "sqlite-finalization-witness", - this._connectionData._connectionIdentifier); + this._connectionData._identifier); } OpenedConnection.prototype = Object.freeze({ @@ -964,8 +1035,8 @@ OpenedConnection.prototype = Object.freeze({ // Unless cleanup has already been done by a previous call to // `close`, delete the database entry from map and tell the // finalization witness to forget. - if (ConnectionData.byId.has(this._connectionData._connectionIdentifier)) { - ConnectionData.byId.delete(this._connectionData._connectionIdentifier); + if (ConnectionData.byId.has(this._connectionData._identifier)) { + ConnectionData.byId.delete(this._connectionData._identifier); this._witness.forget(); } return this._connectionData.close(); @@ -1175,6 +1246,7 @@ OpenedConnection.prototype = Object.freeze({ this.Sqlite = { openConnection: openConnection, cloneStorageConnection: cloneStorageConnection, + wrapStorageConnection: wrapStorageConnection, /** * Shutdown barrier client. May be used by clients to perform last-minute * cleanup prior to the shutdown of this module. diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js index 58407e7821ef..104fdfb9900e 100644 --- a/toolkit/modules/tests/xpcshell/test_sqlite.js +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -845,12 +845,12 @@ add_task(function test_direct() { add_task(function* test_cloneStorageConnection() { let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, "test_cloneStorageConnection.sqlite")); - let c = yield new Promise((success, failure) => { + let c = yield new Promise((resolve, reject) => { Services.storage.openAsyncDatabase(file, null, (status, db) => { if (Components.isSuccessCode(status)) { - success(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); } else { - failure(new Error(status)); + reject(new Error(status)); } }); }); @@ -913,6 +913,33 @@ add_task(function* test_readOnly_clone() { yield clone.close(); }); +/** + * Test Sqlite.wrapStorageConnection. + */ +add_task(function* test_wrapStorageConnection() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_wrapStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let wrapper = yield Sqlite.wrapStorageConnection({ connection: c }); + // Just check that it works. + yield wrapper.execute("SELECT 1"); + yield wrapper.executeCached("SELECT 1"); + + // Closing the wrapper should just finalize statements but not close the + // database. + yield wrapper.close(); + yield c.asyncClose(); +}); + /** * Test finalization */ @@ -921,7 +948,7 @@ add_task(function* test_closed_by_witness() { let c = yield getDummyDatabase("closed_by_witness"); Services.obs.notifyObservers(null, "sqlite-finalization-witness", - c._connectionData._connectionIdentifier); + c._connectionData._identifier); // Since we triggered finalization ourselves, tell the witness to // forget the connection so it does not trigger a finalization again c._witness.forget(); @@ -933,7 +960,7 @@ add_task(function* test_closed_by_witness() { add_task(function* test_warning_message_on_finalization() { failTestsOnAutoClose(false); let c = yield getDummyDatabase("warning_message_on_finalization"); - let connectionIdentifier = c._connectionData._connectionIdentifier; + let identifier = c._connectionData._identifier; let deferred = Promise.defer(); let listener = { @@ -941,14 +968,14 @@ add_task(function* test_warning_message_on_finalization() { let messageText = msg.message; // Make sure the message starts with a warning containing the // connection identifier - if (messageText.indexOf("Warning: Sqlite connection '" + connectionIdentifier + "'") !== -1) { + if (messageText.indexOf("Warning: Sqlite connection '" + identifier + "'") !== -1) { deferred.resolve(); } } }; Services.console.registerListener(listener); - Services.obs.notifyObservers(null, "sqlite-finalization-witness", connectionIdentifier); + Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier); // Since we triggered finalization ourselves, tell the witness to // forget the connection so it does not trigger a finalization again c._witness.forget(); diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h index 6fc2d818886e..4171a71716e7 100644 --- a/widget/cocoa/nsMenuBarX.h +++ b/widget/cocoa/nsMenuBarX.h @@ -103,7 +103,6 @@ public: // The following content nodes have been removed from the menu system. // We save them here for use in command handling. nsCOMPtr mAboutItemContent; - nsCOMPtr mUpdateItemContent; nsCOMPtr mPrefItemContent; nsCOMPtr mQuitItemContent; diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm index 1934d5697b08..66e4bb992769 100644 --- a/widget/cocoa/nsMenuBarX.mm +++ b/widget/cocoa/nsMenuBarX.mm @@ -37,7 +37,6 @@ BOOL gSomeMenuBarPainted = NO; // window does not have a quit or pref item. We don't need strong refs here because // these items are always strong ref'd by their owning menu bar (instance variable). static nsIContent* sAboutItemContent = nullptr; -static nsIContent* sUpdateItemContent = nullptr; static nsIContent* sPrefItemContent = nullptr; static nsIContent* sQuitItemContent = nullptr; @@ -75,8 +74,6 @@ nsMenuBarX::~nsMenuBarX() // hidden window, thus we need to invalidate the weak references. if (sAboutItemContent == mAboutItemContent) sAboutItemContent = nullptr; - if (sUpdateItemContent == mUpdateItemContent) - sUpdateItemContent = nullptr; if (sQuitItemContent == mQuitItemContent) sQuitItemContent = nullptr; if (sPrefItemContent == mPrefItemContent) @@ -478,13 +475,6 @@ void nsMenuBarX::AquifyMenuBar() if (!sAboutItemContent) sAboutItemContent = mAboutItemContent; - // Hide the software update menu item, since it belongs in the application - // menu on Mac OS X. - HideItem(domDoc, NS_LITERAL_STRING("updateSeparator"), nullptr); - HideItem(domDoc, NS_LITERAL_STRING("checkForUpdates"), getter_AddRefs(mUpdateItemContent)); - if (!sUpdateItemContent) - sUpdateItemContent = mUpdateItemContent; - // remove quit item and its separator HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr); HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent)); @@ -600,7 +590,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) ======================== = About This App = <- aboutName - = Check for Updates... = <- checkForUpdates ======================== = Preferences... = <- menu_preferences ======================== @@ -644,17 +633,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu) addAboutSeparator = TRUE; } - // Add the software update menu item - itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("checkForUpdates"), @selector(menuItemHit:), - eCommand_ID_Update, nsMenuBarX::sNativeEventTarget); - if (itemBeingAdded) { - [sApplicationMenu addItem:itemBeingAdded]; - [itemBeingAdded release]; - itemBeingAdded = nil; - - addAboutSeparator = TRUE; - } - // Add separator if either the About item or software update item exists if (addAboutSeparator) [sApplicationMenu addItem:[NSMenuItem separatorItem]]; @@ -950,12 +928,6 @@ static BOOL gMenuItemsExecuteCommands = YES; nsMenuUtilsX::DispatchCommandTo(mostSpecificContent); return; } - else if (tag == eCommand_ID_Update) { - nsIContent* mostSpecificContent = sUpdateItemContent; - if (menuBar && menuBar->mUpdateItemContent) - mostSpecificContent = menuBar->mUpdateItemContent; - nsMenuUtilsX::DispatchCommandTo(mostSpecificContent); - } else if (tag == eCommand_ID_Prefs) { nsIContent* mostSpecificContent = sPrefItemContent; if (menuBar && menuBar->mPrefItemContent)