mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-26 19:55:39 +00:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
6d140e81d7
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -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)
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<ul className="conversation-toolbar">
|
||||
<li className="conversation-toolbar-btn-box">
|
||||
@ -115,25 +117,31 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
<li className="conversation-toolbar-btn-box">
|
||||
<MediaControlButton action={this.handleToggleVideo}
|
||||
enabled={this.props.video.enabled}
|
||||
visible={this.props.video.visible}
|
||||
scope="local" type="video" />
|
||||
</li>
|
||||
<li className="conversation-toolbar-btn-box">
|
||||
<MediaControlButton action={this.handleToggleAudio}
|
||||
enabled={this.props.audio.enabled}
|
||||
visible={this.props.audio.visible}
|
||||
scope="local" type="audio" />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
/* 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 (
|
||||
<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 />;
|
||||
return (
|
||||
<FeedbackReceived
|
||||
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
|
||||
);
|
||||
case "form":
|
||||
return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
|
||||
sendFeedback={this.sendFeedback}
|
||||
|
@ -13,6 +13,9 @@
|
||||
# to the Gruntfile and getting rid of this Makefile entirely.
|
||||
|
||||
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
|
||||
LOOP_FEEDBACK_API_URL := $(shell echo $${LOOP_FEEDBACK_API_URL-"https://input.allizom.org/api/v1/feedback"})
|
||||
LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
|
||||
|
||||
NODE_LOCAL_BIN=./node_modules/.bin
|
||||
|
||||
install: npm_install tos
|
||||
@ -67,4 +70,6 @@ remove_old_config:
|
||||
config:
|
||||
@echo "var loop = loop || {};" > 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
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
|
||||
<script type="text/javascript" src="shared/js/websocket.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
@ -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));
|
||||
|
@ -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 <html> 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,
|
||||
|
@ -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 (
|
||||
<p>{mozL10n.get("welcome")}</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
return (
|
||||
<div className="expired-url-info">
|
||||
<div className="info-panel">
|
||||
@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
<PromoteFirefoxView helper={this.props.helper} />
|
||||
</div>
|
||||
);
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
});
|
||||
|
||||
@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
});
|
||||
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
<header className="standalone-header header-box container-box">
|
||||
<ConversationBranding />
|
||||
<div className="loop-logo" title="Firefox WebRTC! logo"></div>
|
||||
@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
{callUrlCreationDateString}
|
||||
</h4>
|
||||
</header>
|
||||
/* 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 */
|
||||
<div className="container">
|
||||
<div className="container-box">
|
||||
<header className="pending-header header-box">
|
||||
@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
<ConversationFooter />
|
||||
</div>
|
||||
/* 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 */
|
||||
<div className="container">
|
||||
<div className="container-box">
|
||||
|
||||
@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
<ConversationFooter />
|
||||
</div>
|
||||
/* 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 (
|
||||
<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}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -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 />
|
||||
return <HomeView />;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -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 <html> 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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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(){});
|
||||
|
@ -60,8 +60,7 @@ describe("loop.shared.router", function() {
|
||||
conversation = new loop.shared.models.ConversationModel({
|
||||
loopToken: "fakeToken"
|
||||
}, {
|
||||
sdk: {},
|
||||
pendingCallTimeout: 1000
|
||||
sdk: {}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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({
|
||||
|
@ -36,6 +36,7 @@
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneClient.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
|
@ -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() {
|
||||
|
@ -9,6 +9,10 @@
|
||||
* @type {Object}
|
||||
*/
|
||||
navigator.mozL10n = document.mozL10n = {
|
||||
initialize: function(){},
|
||||
|
||||
getDirection: function(){},
|
||||
|
||||
get: function(stringId, vars) {
|
||||
|
||||
// upcase the first letter
|
||||
|
@ -7,6 +7,7 @@
|
||||
* @type {Object}
|
||||
*/
|
||||
navigator.mozLoop = {
|
||||
ensureRegistered: function() {},
|
||||
getLoopCharPref: function() {},
|
||||
getLoopBoolPref: function() {}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"},
|
||||
|
@ -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 @@
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="EndedConversationView">
|
||||
<Example summary="Displays the feedback form">
|
||||
<div className="standalone">
|
||||
<EndedConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
conversation={mockConversationModel}
|
||||
feedbackApiClient={stageFeedbackApiClient}
|
||||
onAfterFeedbackReceived={noop} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="AlertMessages">
|
||||
<Example summary="Various alerts">
|
||||
<div className="alert alert-warning">
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -31,3 +31,4 @@ support-files =
|
||||
[test_manifestUpdate.html]
|
||||
[test_addons.html]
|
||||
[test_deviceinfo.html]
|
||||
[test_autoconnect_runtime.html]
|
||||
|
91
browser/devtools/webide/test/test_autoconnect_runtime.html
Normal file
91
browser/devtools/webide/test/test_autoconnect_runtime.html
Normal file
@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title></title>
|
||||
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="head.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
Task.spawn(function* () {
|
||||
|
||||
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
|
||||
DebuggerServer.init(function () { return true; });
|
||||
DebuggerServer.addBrowserActors();
|
||||
|
||||
let win = yield openWebIDE();
|
||||
|
||||
let fakeRuntime = {
|
||||
type: "USB",
|
||||
connect: function(connection) {
|
||||
ok(connection, win.AppManager.connection, "connection is valid");
|
||||
connection.host = null; // force connectPipe
|
||||
connection.connect();
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
getID: function() {
|
||||
return "fakeRuntime";
|
||||
},
|
||||
|
||||
getName: function() {
|
||||
return "fakeRuntime";
|
||||
}
|
||||
};
|
||||
win.AppManager.runtimeList.usb.push(fakeRuntime);
|
||||
win.AppManager.update("runtimelist");
|
||||
|
||||
let panelNode = win.document.querySelector("#runtime-panel");
|
||||
let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
|
||||
is(items.length, 1, "Found one runtime button");
|
||||
|
||||
let deferred = promise.defer();
|
||||
win.AppManager.connection.once(
|
||||
win.Connection.Events.CONNECTED,
|
||||
() => deferred.resolve());
|
||||
items[0].click();
|
||||
|
||||
ok(win.document.querySelector("window").className, "busy", "UI is busy");
|
||||
yield win.UI._busyPromise;
|
||||
is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
|
||||
|
||||
yield nextTick();
|
||||
|
||||
yield closeWebIDE(win);
|
||||
|
||||
is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
|
||||
|
||||
win = yield openWebIDE();
|
||||
|
||||
win.AppManager.runtimeList.usb.push(fakeRuntime);
|
||||
win.AppManager.update("runtimelist");
|
||||
|
||||
yield waitForUpdate(win, "list-tabs-response");
|
||||
|
||||
is(Object.keys(DebuggerServer._connections).length, 1, "Automatically reconnected");
|
||||
|
||||
yield win.Cmds.disconnectRuntime();
|
||||
|
||||
yield closeWebIDE(win);
|
||||
|
||||
DebuggerServer.destroy();
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -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", "");
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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");
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
|
||||
<view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
|
||||
android:id="@+id/tabs_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dip"
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
|
||||
<view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
|
||||
android:id="@+id/tabs_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
@ -4,5 +4,5 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
|
||||
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
|
||||
</resources>
|
||||
|
@ -4,5 +4,5 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
|
||||
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
|
||||
</resources>
|
||||
|
@ -4,5 +4,5 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<item type="layout" name="tabs_row">@layout/tabs_item_row</item>
|
||||
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_row</item>
|
||||
</resources>
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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":
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
295
toolkit/components/places/Bookmarks.jsm
Normal file
295
toolkit/components/places/Bookmarks.jsm
Normal file
@ -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");
|
||||
})
|
||||
});
|
@ -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",
|
||||
|
@ -62,6 +62,7 @@ if CONFIG['MOZ_PLACES']:
|
||||
EXTRA_JS_MODULES += [
|
||||
'BookmarkHTMLUtils.jsm',
|
||||
'BookmarkJSONUtils.jsm',
|
||||
'Bookmarks.jsm',
|
||||
'ClusterLib.js',
|
||||
'ColorAnalyzer_worker.js',
|
||||
'ColorConversion.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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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<OpenedConnection>
|
||||
*/
|
||||
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.
|
||||
|
@ -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();
|
||||
|
@ -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<nsIContent> mAboutItemContent;
|
||||
nsCOMPtr<nsIContent> mUpdateItemContent;
|
||||
nsCOMPtr<nsIContent> mPrefItemContent;
|
||||
nsCOMPtr<nsIContent> mQuitItemContent;
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user