Merge fx-team to m-c. a=merge
Merge fx-team to m-c. a=merge
--- a/browser/branding/aurora/configure.sh
+++ b/browser/branding/aurora/configure.sh
@@ -1,6 +1,7 @@
# 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/.
MOZ_APP_DISPLAYNAME=FirefoxDeveloperEdition
MOZ_APP_REMOTINGNAME=firefox-dev
+MOZ_DEV_EDITION=1
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -412,16 +412,18 @@ loop.contacts = (function(_, mozL10n) {
return comp;
}
// If names are equal, compare against unique ids to make sure we have
// consistent ordering.
return contact1._guid - contact2._guid;
},
render: function() {
+ let cx = React.addons.classSet;
+
let viewForItem = item => {
return ContactDetail({key: item._guid, contact: item,
handleContactAction: this.handleContactAction})
};
let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
@@ -439,26 +441,29 @@ loop.contacts = (function(_, mozL10n) {
shownContacts.available = shownContacts.available.filter(filterFn);
}
if (shownContacts.blocked) {
shownContacts.blocked = shownContacts.blocked.filter(filterFn);
}
}
}
- // TODO: bug 1076767 - add a spinner whilst importing contacts.
return (
React.DOM.div(null,
React.DOM.div({className: "content-area"},
ButtonGroup(null,
Button({caption: this.state.importBusy
? mozL10n.get("importing_contacts_progress_button")
: mozL10n.get("import_contacts_button"),
disabled: this.state.importBusy,
- onClick: this.handleImportButtonClick}),
+ onClick: this.handleImportButtonClick},
+ React.DOM.div({className: cx({"contact-import-spinner": true,
+ spinner: true,
+ busy: this.state.importBusy})})
+ ),
Button({caption: mozL10n.get("new_contact_button"),
onClick: this.handleAddContactButtonClick})
),
showFilter ?
React.DOM.input({className: "contact-filter",
placeholder: mozL10n.get("contacts_search_placesholder"),
valueLink: this.linkState("filter")})
: null
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -412,16 +412,18 @@ loop.contacts = (function(_, mozL10n) {
return comp;
}
// If names are equal, compare against unique ids to make sure we have
// consistent ordering.
return contact1._guid - contact2._guid;
},
render: function() {
+ let cx = React.addons.classSet;
+
let viewForItem = item => {
return <ContactDetail key={item._guid} contact={item}
handleContactAction={this.handleContactAction} />
};
let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
@@ -439,26 +441,29 @@ loop.contacts = (function(_, mozL10n) {
shownContacts.available = shownContacts.available.filter(filterFn);
}
if (shownContacts.blocked) {
shownContacts.blocked = shownContacts.blocked.filter(filterFn);
}
}
}
- // TODO: bug 1076767 - add a spinner whilst importing contacts.
return (
<div>
<div className="content-area">
<ButtonGroup>
<Button caption={this.state.importBusy
? mozL10n.get("importing_contacts_progress_button")
: mozL10n.get("import_contacts_button")}
disabled={this.state.importBusy}
- onClick={this.handleImportButtonClick} />
+ onClick={this.handleImportButtonClick}>
+ <div className={cx({"contact-import-spinner": true,
+ spinner: true,
+ busy: this.state.importBusy})} />
+ </Button>
<Button caption={mozL10n.get("new_contact_button")}
onClick={this.handleAddContactButtonClick} />
</ButtonGroup>
{showFilter ?
<input className="contact-filter"
placeholder={mozL10n.get("contacts_search_placesholder")}
valueLink={this.linkState("filter")} />
: null }
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -13,17 +13,17 @@ loop.conversation = (function(mozL10n) {
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
- var DesktopRoomControllerView = loop.roomViews.DesktopRoomControllerView;
+ var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
@@ -579,20 +579,20 @@ loop.conversation = (function(mozL10n) {
}
case "outgoing": {
return (OutgoingConversationView({
store: this.props.conversationStore,
dispatcher: this.props.dispatcher}
));
}
case "room": {
- return (DesktopRoomControllerView({
- mozLoop: navigator.mozLoop,
+ return (DesktopRoomConversationView({
dispatcher: this.props.dispatcher,
- roomStore: this.props.roomStore}
+ roomStore: this.props.roomStore,
+ dispatcher: this.props.dispatcher}
));
}
case "failed": {
return (GenericFailureView({
cancelCall: this.closeWindow}
));
}
default: {
@@ -638,17 +638,18 @@ loop.conversation = (function(mozL10n) {
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: navigator.mozLoop
+ mozLoop: navigator.mozLoop,
+ sdkDriver: sdkDriver
});
var roomStore = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
// XXX Old class creation for the incoming conversation view, whilst
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -13,17 +13,17 @@ loop.conversation = (function(mozL10n) {
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
- var DesktopRoomControllerView = loop.roomViews.DesktopRoomControllerView;
+ var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
@@ -579,20 +579,20 @@ loop.conversation = (function(mozL10n) {
}
case "outgoing": {
return (<OutgoingConversationView
store={this.props.conversationStore}
dispatcher={this.props.dispatcher}
/>);
}
case "room": {
- return (<DesktopRoomControllerView
- mozLoop={navigator.mozLoop}
+ return (<DesktopRoomConversationView
dispatcher={this.props.dispatcher}
roomStore={this.props.roomStore}
+ dispatcher={this.props.dispatcher}
/>);
}
case "failed": {
return (<GenericFailureView
cancelCall={this.closeWindow}
/>);
}
default: {
@@ -638,17 +638,18 @@ loop.conversation = (function(mozL10n) {
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: navigator.mozLoop
+ mozLoop: navigator.mozLoop,
+ sdkDriver: sdkDriver
});
var roomStore = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
// XXX Old class creation for the incoming conversation view, whilst
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -394,28 +394,31 @@ loop.panel = (function(_, mozL10n) {
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
// readOnly attr will suppress a warning regarding this issue
// from the react lib.
var cx = React.addons.classSet;
- var inputCSSClass = cx({
- "pending": this.state.pending,
- // Used in functional testing, signals that
- // call url was received from loop server
- "callUrl": !this.state.pending
- });
return (
React.DOM.div({className: "generate-url"},
React.DOM.header(null, __("share_link_header_text")),
- React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true",
- onCopy: this.handleLinkExfiltration,
- className: inputCSSClass}),
+ React.DOM.div({className: "generate-url-stack"},
+ React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true",
+ onCopy: this.handleLinkExfiltration,
+ className: cx({"generate-url-input": true,
+ pending: this.state.pending,
+ // Used in functional testing, signals that
+ // call url was received from loop server
+ callUrl: !this.state.pending})}),
+ React.DOM.div({className: cx({"generate-url-spinner": true,
+ spinner: true,
+ busy: this.state.pending})})
+ ),
ButtonGroup({additionalClass: "url-actions"},
Button({additionalClass: "button-email",
disabled: !this.state.callUrl,
onClick: this.handleEmailButtonClick,
caption: mozL10n.get("share_button")}),
Button({additionalClass: "button-copy",
disabled: !this.state.callUrl,
onClick: this.handleCopyButtonClick,
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -394,28 +394,31 @@ loop.panel = (function(_, mozL10n) {
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
// readOnly attr will suppress a warning regarding this issue
// from the react lib.
var cx = React.addons.classSet;
- var inputCSSClass = cx({
- "pending": this.state.pending,
- // Used in functional testing, signals that
- // call url was received from loop server
- "callUrl": !this.state.pending
- });
return (
<div className="generate-url">
<header>{__("share_link_header_text")}</header>
- <input type="url" value={this.state.callUrl} readOnly="true"
- onCopy={this.handleLinkExfiltration}
- className={inputCSSClass} />
+ <div className="generate-url-stack">
+ <input type="url" value={this.state.callUrl} readOnly="true"
+ onCopy={this.handleLinkExfiltration}
+ className={cx({"generate-url-input": true,
+ pending: this.state.pending,
+ // Used in functional testing, signals that
+ // call url was received from loop server
+ callUrl: !this.state.pending})} />
+ <div className={cx({"generate-url-spinner": true,
+ spinner: true,
+ busy: this.state.pending})} />
+ </div>
<ButtonGroup additionalClass="url-actions">
<Button additionalClass="button-email"
disabled={!this.state.callUrl}
onClick={this.handleEmailButtonClick}
caption={mozL10n.get("share_button")} />
<Button additionalClass="button-copy"
disabled={!this.state.callUrl}
onClick={this.handleCopyButtonClick}
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -6,16 +6,17 @@
/* jshint newcap:false */
/* global loop:true, React */
var loop = loop || {};
loop.roomViews = (function(mozL10n) {
"use strict";
+ var sharedActions = loop.shared.actions;
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedViews = loop.shared.views;
function noop() {}
/**
* ActiveRoomStore mixin.
* @type {Object}
@@ -32,21 +33,30 @@ loop.roomViews = (function(mozL10n) {
this._onActiveRoomStateChanged);
},
componentWillUnmount: function() {
this.stopListening(this.props.roomStore);
},
_onActiveRoomStateChanged: function() {
- this.setState(this.props.roomStore.getStoreState("activeRoom"));
+ // Only update the state if we're mounted, to avoid the problem where
+ // stopListening doesn't nuke the active listeners during a event
+ // processing.
+ if (this.isMounted()) {
+ this.setState(this.props.roomStore.getStoreState("activeRoom"));
+ }
},
getInitialState: function() {
- return this.props.roomStore.getStoreState("activeRoom");
+ var storeState = this.props.roomStore.getStoreState("activeRoom");
+ return _.extend(storeState, {
+ // Used by the UI showcase.
+ roomState: this.props.roomState || storeState.roomState
+ });
}
};
/**
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: 'DesktopRoomInvitationView',
mixins: [ActiveRoomStoreMixin],
@@ -67,140 +77,182 @@ loop.roomViews = (function(mozL10n) {
handleCopyButtonClick: function(event) {
event.preventDefault();
// XXX
},
render: function() {
return (
- React.DOM.div({className: "room-conversation-wrapper"},
- React.DOM.div({className: "room-invitation-overlay"},
- React.DOM.form({onSubmit: this.handleFormSubmit},
- React.DOM.input({type: "text", ref: "roomName",
- placeholder: mozL10n.get("rooms_name_this_room_label")})
+ React.DOM.div({className: "room-invitation-overlay"},
+ React.DOM.form({onSubmit: this.handleFormSubmit},
+ React.DOM.input({type: "text", ref: "roomName",
+ placeholder: mozL10n.get("rooms_name_this_room_label")})
+ ),
+ React.DOM.p(null, mozL10n.get("invite_header_text")),
+ React.DOM.div({className: "btn-group call-action-group"},
+ React.DOM.button({className: "btn btn-info btn-email",
+ onClick: this.handleEmailButtonClick},
+ mozL10n.get("share_button2")
),
- React.DOM.p(null, mozL10n.get("invite_header_text")),
- React.DOM.div({className: "btn-group call-action-group"},
- React.DOM.button({className: "btn btn-info btn-email",
- onClick: this.handleEmailButtonClick},
- mozL10n.get("share_button2")
- ),
- React.DOM.button({className: "btn btn-info btn-copy",
- onClick: this.handleCopyButtonClick},
- mozL10n.get("copy_url_button2")
- )
+ React.DOM.button({className: "btn btn-info btn-copy",
+ onClick: this.handleCopyButtonClick},
+ mozL10n.get("copy_url_button2")
)
- ),
- DesktopRoomConversationView({roomStore: this.props.roomStore})
+ )
)
);
}
});
/**
* Desktop room conversation view.
*/
var DesktopRoomConversationView = React.createClass({displayName: 'DesktopRoomConversationView',
- mixins: [ActiveRoomStoreMixin],
-
- propTypes: {
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- video: React.PropTypes.object,
- audio: React.PropTypes.object,
- displayInvitation: React.PropTypes.bool
- },
-
- getDefaultProps: function() {
- return {
- video: {enabled: true, visible: true},
- audio: {enabled: true, visible: true}
- };
- },
-
- render: function() {
- var localStreamClasses = React.addons.classSet({
- local: true,
- "local-stream": true,
- "local-stream-audio": !this.props.video.enabled
- });
- return (
- React.DOM.div({className: "room-conversation-wrapper"},
- React.DOM.div({className: "video-layout-wrapper"},
- React.DOM.div({className: "conversation room-conversation"},
- React.DOM.div({className: "media nested"},
- React.DOM.div({className: "video_wrapper remote_wrapper"},
- React.DOM.div({className: "video_inner remote"})
- ),
- React.DOM.div({className: localStreamClasses})
- ),
- sharedViews.ConversationToolbar({
- video: this.props.video,
- audio: this.props.audio,
- publishStream: noop,
- hangup: noop})
- )
- )
- )
- );
- }
- });
-
- /**
- * Desktop room controller view.
- */
- var DesktopRoomControllerView = React.createClass({displayName: 'DesktopRoomControllerView',
mixins: [ActiveRoomStoreMixin, loop.shared.mixins.DocumentTitleMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
+ _renderInvitationOverlay: function() {
+ if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
+ return DesktopRoomInvitationView({
+ roomStore: this.props.roomStore,
+ dispatcher: this.props.dispatcher}
+ );
+ }
+ return null;
+ },
+
+ componentDidMount: function() {
+ /**
+ * 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);
+
+ // The SDK needs to know about the configuration and the elements to use
+ // for display. So the best way seems to pass the information here - ideally
+ // the sdk wouldn't need to know this, but we can't change that.
+ this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
+ publisherConfig: this._getPublisherConfig(),
+ getLocalElementFunc: this._getElement.bind(this, ".local"),
+ getRemoteElementFunc: this._getElement.bind(this, ".remote")
+ }));
+ },
+
+ _getPublisherConfig: function() {
+ // height set to 100%" to fix video layout on Google Chrome
+ // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
+ return {
+ insertMode: "append",
+ width: "100%",
+ height: "100%",
+ publishVideo: !this.state.videoMuted,
+ style: {
+ audioLevelDisplayMode: "off",
+ bugDisplayMode: "off",
+ buttonDisplayMode: "off",
+ nameDisplayMode: "off",
+ videoDisabledDisplayMode: "off"
+ }
+ };
+ },
+
+ /**
+ * Used to update the video container whenever the orientation or size of the
+ * display area changes.
+ */
+ updateVideoContainer: function() {
+ var localStreamParent = this._getElement('.local .OT_publisher');
+ var remoteStreamParent = this._getElement('.remote .OT_subscriber');
+ if (localStreamParent) {
+ localStreamParent.style.width = "100%";
+ }
+ if (remoteStreamParent) {
+ remoteStreamParent.style.height = "100%";
+ }
+ },
+
+ /**
+ * Returns either the required DOMNode
+ *
+ * @param {String} className The name of the class to get the element for.
+ */
+ _getElement: function(className) {
+ return this.getDOMNode().querySelector(className);
+ },
+
+ /**
+ * Closes the window if the cancel button is pressed in the generic failure view.
+ */
closeWindow: function() {
window.close();
},
- _renderRoomView: function(roomState) {
- switch (roomState) {
- case ROOM_STATES.FAILED: {
- return loop.conversation.GenericFailureView({
- cancelCall: this.closeWindow}
- );
- }
- case ROOM_STATES.INIT:
- case ROOM_STATES.GATHER:
- case ROOM_STATES.READY:
- case ROOM_STATES.JOINED: {
- return DesktopRoomInvitationView({
- dispatcher: this.props.dispatcher,
- roomStore: this.props.roomStore}
- );
- }
- // XXX needs bug 1074686/1074702
- case ROOM_STATES.HAS_PARTICIPANTS: {
- return DesktopRoomConversationView({
- dispatcher: this.props.dispatcher,
- roomStore: this.props.roomStore}
- );
- }
- }
+ /**
+ * Used to control publishing a stream - i.e. to mute a stream
+ *
+ * @param {String} type The type of stream, e.g. "audio" or "video".
+ * @param {Boolean} enabled True to enable the stream, false otherwise.
+ */
+ publishStream: function(type, enabled) {
+ this.props.dispatcher.dispatch(
+ new sharedActions.SetMute({
+ type: type,
+ enabled: enabled
+ }));
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
}
- return (
- React.DOM.div({className: "room-conversation-wrapper"},
- this._renderRoomView(this.state.roomState)
- )
- );
+
+ var localStreamClasses = React.addons.classSet({
+ local: true,
+ "local-stream": true,
+ "local-stream-audio": !this.state.videoMuted
+ });
+
+ switch(this.state.roomState) {
+ case ROOM_STATES.FAILED: {
+ return loop.conversation.GenericFailureView({
+ cancelCall: this.closeWindow}
+ );
+ }
+ default: {
+ return (
+ React.DOM.div({className: "room-conversation-wrapper"},
+ this._renderInvitationOverlay(),
+ React.DOM.div({className: "video-layout-wrapper"},
+ React.DOM.div({className: "conversation room-conversation"},
+ React.DOM.div({className: "media nested"},
+ React.DOM.div({className: "video_wrapper remote_wrapper"},
+ React.DOM.div({className: "video_inner remote"})
+ ),
+ React.DOM.div({className: localStreamClasses})
+ ),
+ sharedViews.ConversationToolbar({
+ video: {enabled: !this.state.videoMuted, visible: true},
+ audio: {enabled: !this.state.audioMuted, visible: true},
+ publishStream: this.publishStream,
+ hangup: noop})
+ )
+ )
+ )
+ );
+ }
+ }
}
});
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
- DesktopRoomControllerView: DesktopRoomControllerView,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};
})(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -6,16 +6,17 @@
/* jshint newcap:false */
/* global loop:true, React */
var loop = loop || {};
loop.roomViews = (function(mozL10n) {
"use strict";
+ var sharedActions = loop.shared.actions;
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedViews = loop.shared.views;
function noop() {}
/**
* ActiveRoomStore mixin.
* @type {Object}
@@ -32,21 +33,30 @@ loop.roomViews = (function(mozL10n) {
this._onActiveRoomStateChanged);
},
componentWillUnmount: function() {
this.stopListening(this.props.roomStore);
},
_onActiveRoomStateChanged: function() {
- this.setState(this.props.roomStore.getStoreState("activeRoom"));
+ // Only update the state if we're mounted, to avoid the problem where
+ // stopListening doesn't nuke the active listeners during a event
+ // processing.
+ if (this.isMounted()) {
+ this.setState(this.props.roomStore.getStoreState("activeRoom"));
+ }
},
getInitialState: function() {
- return this.props.roomStore.getStoreState("activeRoom");
+ var storeState = this.props.roomStore.getStoreState("activeRoom");
+ return _.extend(storeState, {
+ // Used by the UI showcase.
+ roomState: this.props.roomState || storeState.roomState
+ });
}
};
/**
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
mixins: [ActiveRoomStoreMixin],
@@ -67,140 +77,182 @@ loop.roomViews = (function(mozL10n) {
handleCopyButtonClick: function(event) {
event.preventDefault();
// XXX
},
render: function() {
return (
- <div className="room-conversation-wrapper">
- <div className="room-invitation-overlay">
- <form onSubmit={this.handleFormSubmit}>
- <input type="text" ref="roomName"
- placeholder={mozL10n.get("rooms_name_this_room_label")} />
- </form>
- <p>{mozL10n.get("invite_header_text")}</p>
- <div className="btn-group call-action-group">
- <button className="btn btn-info btn-email"
- onClick={this.handleEmailButtonClick}>
- {mozL10n.get("share_button2")}
- </button>
- <button className="btn btn-info btn-copy"
- onClick={this.handleCopyButtonClick}>
- {mozL10n.get("copy_url_button2")}
- </button>
- </div>
+ <div className="room-invitation-overlay">
+ <form onSubmit={this.handleFormSubmit}>
+ <input type="text" ref="roomName"
+ placeholder={mozL10n.get("rooms_name_this_room_label")} />
+ </form>
+ <p>{mozL10n.get("invite_header_text")}</p>
+ <div className="btn-group call-action-group">
+ <button className="btn btn-info btn-email"
+ onClick={this.handleEmailButtonClick}>
+ {mozL10n.get("share_button2")}
+ </button>
+ <button className="btn btn-info btn-copy"
+ onClick={this.handleCopyButtonClick}>
+ {mozL10n.get("copy_url_button2")}
+ </button>
</div>
- <DesktopRoomConversationView roomStore={this.props.roomStore} />
</div>
);
}
});
/**
* Desktop room conversation view.
*/
var DesktopRoomConversationView = React.createClass({
- mixins: [ActiveRoomStoreMixin],
-
- propTypes: {
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- video: React.PropTypes.object,
- audio: React.PropTypes.object,
- displayInvitation: React.PropTypes.bool
- },
-
- getDefaultProps: function() {
- return {
- video: {enabled: true, visible: true},
- audio: {enabled: true, visible: true}
- };
- },
-
- render: function() {
- var localStreamClasses = React.addons.classSet({
- local: true,
- "local-stream": true,
- "local-stream-audio": !this.props.video.enabled
- });
- return (
- <div className="room-conversation-wrapper">
- <div className="video-layout-wrapper">
- <div className="conversation room-conversation">
- <div className="media nested">
- <div className="video_wrapper remote_wrapper">
- <div className="video_inner remote"></div>
- </div>
- <div className={localStreamClasses}></div>
- </div>
- <sharedViews.ConversationToolbar
- video={this.props.video}
- audio={this.props.audio}
- publishStream={noop}
- hangup={noop} />
- </div>
- </div>
- </div>
- );
- }
- });
-
- /**
- * Desktop room controller view.
- */
- var DesktopRoomControllerView = React.createClass({
mixins: [ActiveRoomStoreMixin, loop.shared.mixins.DocumentTitleMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
+ _renderInvitationOverlay: function() {
+ if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
+ return <DesktopRoomInvitationView
+ roomStore={this.props.roomStore}
+ dispatcher={this.props.dispatcher}
+ />;
+ }
+ return null;
+ },
+
+ componentDidMount: function() {
+ /**
+ * 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);
+
+ // The SDK needs to know about the configuration and the elements to use
+ // for display. So the best way seems to pass the information here - ideally
+ // the sdk wouldn't need to know this, but we can't change that.
+ this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
+ publisherConfig: this._getPublisherConfig(),
+ getLocalElementFunc: this._getElement.bind(this, ".local"),
+ getRemoteElementFunc: this._getElement.bind(this, ".remote")
+ }));
+ },
+
+ _getPublisherConfig: function() {
+ // height set to 100%" to fix video layout on Google Chrome
+ // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
+ return {
+ insertMode: "append",
+ width: "100%",
+ height: "100%",
+ publishVideo: !this.state.videoMuted,
+ style: {
+ audioLevelDisplayMode: "off",
+ bugDisplayMode: "off",
+ buttonDisplayMode: "off",
+ nameDisplayMode: "off",
+ videoDisabledDisplayMode: "off"
+ }
+ };
+ },
+
+ /**
+ * Used to update the video container whenever the orientation or size of the
+ * display area changes.
+ */
+ updateVideoContainer: function() {
+ var localStreamParent = this._getElement('.local .OT_publisher');
+ var remoteStreamParent = this._getElement('.remote .OT_subscriber');
+ if (localStreamParent) {
+ localStreamParent.style.width = "100%";
+ }
+ if (remoteStreamParent) {
+ remoteStreamParent.style.height = "100%";
+ }
+ },
+
+ /**
+ * Returns either the required DOMNode
+ *
+ * @param {String} className The name of the class to get the element for.
+ */
+ _getElement: function(className) {
+ return this.getDOMNode().querySelector(className);
+ },
+
+ /**
+ * Closes the window if the cancel button is pressed in the generic failure view.
+ */
closeWindow: function() {
window.close();
},
- _renderRoomView: function(roomState) {
- switch (roomState) {
- case ROOM_STATES.FAILED: {
- return <loop.conversation.GenericFailureView
- cancelCall={this.closeWindow}
- />;
- }
- case ROOM_STATES.INIT:
- case ROOM_STATES.GATHER:
- case ROOM_STATES.READY:
- case ROOM_STATES.JOINED: {
- return <DesktopRoomInvitationView
- dispatcher={this.props.dispatcher}
- roomStore={this.props.roomStore}
- />;
- }
- // XXX needs bug 1074686/1074702
- case ROOM_STATES.HAS_PARTICIPANTS: {
- return <DesktopRoomConversationView
- dispatcher={this.props.dispatcher}
- roomStore={this.props.roomStore}
- />;
- }
- }
+ /**
+ * Used to control publishing a stream - i.e. to mute a stream
+ *
+ * @param {String} type The type of stream, e.g. "audio" or "video".
+ * @param {Boolean} enabled True to enable the stream, false otherwise.
+ */
+ publishStream: function(type, enabled) {
+ this.props.dispatcher.dispatch(
+ new sharedActions.SetMute({
+ type: type,
+ enabled: enabled
+ }));
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
}
- return (
- <div className="room-conversation-wrapper">{
- this._renderRoomView(this.state.roomState)
- }</div>
- );
+
+ var localStreamClasses = React.addons.classSet({
+ local: true,
+ "local-stream": true,
+ "local-stream-audio": !this.state.videoMuted
+ });
+
+ switch(this.state.roomState) {
+ case ROOM_STATES.FAILED: {
+ return <loop.conversation.GenericFailureView
+ cancelCall={this.closeWindow}
+ />;
+ }
+ default: {
+ return (
+ <div className="room-conversation-wrapper">
+ {this._renderInvitationOverlay()}
+ <div className="video-layout-wrapper">
+ <div className="conversation room-conversation">
+ <div className="media nested">
+ <div className="video_wrapper remote_wrapper">
+ <div className="video_inner remote"></div>
+ </div>
+ <div className={localStreamClasses}></div>
+ </div>
+ <sharedViews.ConversationToolbar
+ video={{enabled: !this.state.videoMuted, visible: true}}
+ audio={{enabled: !this.state.audioMuted, visible: true}}
+ publishStream={this.publishStream}
+ hangup={noop} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+ }
}
});
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
- DesktopRoomControllerView: DesktopRoomControllerView,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};
})(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/contacts.css
+++ b/browser/components/loop/content/shared/css/contacts.css
@@ -1,12 +1,22 @@
/* 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/. */
+.contact-import-spinner {
+ display: none;
+}
+
+.contact-import-spinner.busy {
+ display: inline-block;
+ vertical-align: middle;
+ -moz-margin-start: 10px;
+}
+
.content-area input.contact-filter {
margin-top: 14px;
border-radius: 10000px;
}
.contact-list {
border-top: 1px solid #ccc;
overflow-x: hidden;
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -41,27 +41,27 @@ body {
border-top-left-radius: 2px;
list-style: none;
}
.tab-view > li {
flex: 1;
text-align: center;
color: #ccc;
- border-right: 1px solid #ccc;
+ -moz-border-end: 1px solid #ccc;
padding: 0 10px;
height: 16px;
cursor: pointer;
background-repeat: no-repeat;
background-size: 16px 16px;
background-position: center;
}
.tab-view > li:last-child {
- border-right-style: none;
+ -moz-border-end-style: none;
}
.tab-view > li[data-tab-name="call"],
.tab-view > li[data-tab-name="rooms"] {
background-image: url("../img/icons-16x16.svg#precall");
}
.tab-view > li[data-tab-name="call"]:hover,
@@ -302,16 +302,20 @@ body {
background-color: #fbfbfb;
color: #333;
border: 1px solid #c1c1c1;
border-radius: 2px;
height: 26px;
font-size: 12px;
}
+.button > .button-caption {
+ vertical-align: middle;
+}
+
.button:hover {
background-color: #ebebeb;
}
.button:hover:active {
background-color: #ccc;
color: #111;
}
@@ -367,34 +371,73 @@ body[dir=rtl] .dropdown-menu-item {
white-space: nowrap;
}
.dropdown-menu-item:hover {
border: 1px solid #ccc;
background-color: #eee;
}
+/* Spinner */
+
+@keyframes spinnerRotate {
+ to { transform: rotate(360deg); }
+}
+
+.spinner {
+ width: 16px;
+ height: 16px;
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+}
+
+.spinner.busy {
+ background-image: url(../img/spinner.png);
+ animation-name: spinnerRotate;
+ animation-duration: 1s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+}
+
+@media (min-resolution: 2dppx) {
+ .spinner.busy {
+ background-image: url(../img/spinner@2x.png);
+ }
+}
+
/* Share tab */
-.generate-url input {
+.generate-url-stack {
margin: 14px 0;
+ position: relative;
+}
+
+.generate-url-input {
outline: 0;
border: 1px solid #ccc; /* Overriding background style for a text input (see
below) resets its borders to a weird beveled style;
defining a default 1px border solves the issue. */
border-radius: 2px;
height: 26px;
padding: 0 10px;
font-size: 1em;
}
-.generate-url input.pending {
- background-image: url(../img/loading-icon.gif);
- background-repeat: no-repeat;
- background-position: right;
+.generate-url-spinner {
+ position: absolute;
+ pointer-events: none;
+ z-index: 1;
+ top: 4px;
+ left: auto;
+ right: 4px;
+}
+
+body[dir=rtl] .generate-url-spinner {
+ left: 4px;
+ right: auto;
}
.generate-url .button {
background-color: #0096dd;
border-color: #0096dd;
color: #fff;
}
deleted file mode 100644
index 1c72ebb554be018511ae972c3f2361dff02dce02..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index 0000000000000000000000000000000000000000..11134bcccac7b69d8364104c4701caeccd99a4c1
GIT binary patch
literal 724
zc$@*$0xSKAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0007;Nkl<Zcmajb
zO^X~=6o>KuIp^N$u9xm<9mkm@GeZ*N5OpOe;>L)ef*>wk2rfmkb0G*q7UI&KA3^*8
zq8kxhC;?G)kzq7sWs4|b#-t~msp_hG?>SbxkVZm$;73t#?x`2Tf7-e7-D5GN*CXT^
zHEfxgL$bdF=?CZd69Ct+5}kVQDwY+aB5T|3o#^rxk&V6j+#7qZ30_0zbWwZMGx2)z
zrStqQfVv0T>K9a#$}bk-BQ4jTK=FVF4<sovRm)O>GbO*gx3+Os-*T?Gxj*!H6+r7?
zK#b`!t!+Lk)p|4%>Ib85Prk7ff7WCY%{`r{e92tlQ#l}2ev;ciKL+p?o%`^o3tCJ*
zptY?EB%;myNW#e#08^|~-%We9m7fN;c9voknoYIhJ1?`bydu>kiq8T<)A(@beMXx0
zu&SzXBag>#P<x_r`HA_4?-ISul;w}RK@kZlNSI%R^S}3SFQbk8hVN!yS&UA7={gk8
zF|vHj-gT5h;MD!n!+pj2M}&naJVzRAvp5ON9E7Z6XzbyB);GuBQgfWJm=8Ur>yBZE
zOiB=vM7)dtX=iUYj2@g{p3Um*(>srR&!pV}X$s4qD1+fehqTedy_`CCz5sA$JQ>c<
zUf8^r4YEI&O~0nF=vX+qa%ANEn@m{mVbyr!;VLfbO8^`@9{`xC{zjKROSpY>_!^g!
zXIUg!TSsl?4Ptk|EWfgo<b3v!4DvIQ4d|bl{wT9t2e9vm8A$O9ZSHPzIog(Ck;HpO
zY%fCfJPU{A6WM2hM4|z(ln%jifIfQubHlRvts@7P7i5_4v3O-dpdJ%-L=*{#{WSoX
z_Lhg!zYA77KDv1tEKl{XyUQ%)atWE<HjPuL?E^T(N`C|MPg9nE`M}Kp0000<MNUMn
GLSTZO!C2V<
new file mode 100644
index 0000000000000000000000000000000000000000..62ccb6c0712ac06da5a781e61d7c7bf807ded845
GIT binary patch
literal 1792
zc$@(M2mknqP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F8000KZNkl<Zcmb``
zd#GJ!naA<(?|I*~_TJ~5ea^{Ao91Scwuwo#BBhGf3#m}7h=_^|6#7S@(3yc@3IhXW
zh8eY>)<3-<c%caPpQ2Ty?F<ae(0XaDD7IF5(Q2oWHnu0}IXSnz_uA`y9tmed4l#x_
z=?7k3HY}ds!*A^+F3`Tu{G#8E$2XVYy%BcM(awPBfP}}uA{IsYy%~>Jb^Q>q!UfRY
z|9FD)Nr@}RZQNF@em~Vzm#Sx;I*D8vB#R(LDHd&G(Tt3CBQaWQvNw;U%-4YDIS=i;
z>!+Mcr&o8{;{ViI?_-*pujuxT)KOTrkR>pSQ1ZZtNa1c#TwYqE8CVQgOoJt_%EkjG
z|4(4ZIb?4OXPfHx_BXD(WBQ7HX?i|UPl0kc27rQyU<-j+APlQT>WX?s-ks0Q`pH{t
zaN>q;_B()QIZL*b*{U;LzrCTpTk|{SG&Sc`9T7%=DG&jZ>|iSplM^x{b&O6WS4WY1
z2iI4R|8UAiw*fzA3kwe>=XCh~R^89pwms9Szui@LLs%eO5MmM>0pZ|n$lD+sBDA%Y
z0Fa5QXFhc~-ru)vBf9qi*RVxqVtT&UZ6g1>&F!4B>G_D1+(7QQ0t+GtciIM;#;uGO
zp<N2coP0$g@5MSbssu?OB2XYjs*R}ym590F>ap+ks`e<b4v=LAKndM$vzPDE^qiBE
z35dWYh;SH>q|w^Tbvt-K#D4)B00>}&7p9wrPc`b^-sah>u#UhKAOb>J^tREoqpQ;3
z<u3qt0%+l19%KIU-rjNUe>pAe&Qx6?Sp)(sO0gJiNQ33?mEquCpl}}JhV1IxHO$-o
z-mB>fB@3U-B1E$xTYjmX(W#@rGurX5k1@T_|7YK}`)Hb;S74Ke5$?$c>$1Vp*MWz4
z1J+i{Z3A1l$Ls#&lSKrIaItZM-ujCV0k`VlJx|WG-MO#Y+^)W=jshmNVbKhv)x`&a
zZ*dV$Esa0FQSbZ*(zF6>G8fu`^2+~@X0&oB7v}@1Gm}(R9zp_XbJK{?>g%<|ceqH_
z4nCV=`4erovnP}VC<0*}N}lVM@ye%l;GW;!m3QnvLU*P^AV9$B_fD+5dN1(bT!Q7r
z<{#Ip-Cv5jFJSWoXt+#w;|~vJ<<1^URS9r}0EloO4?HU#<q}ym_o-R?VpKC#AOHg(
z)lt+roTI9?E9VV_vq^+wrFq4x-UgRwes6De<5+X7csYbJfB-@@E%n}PDc3Sv1qvV_
za2MX5<V|8eSoF3%tehtWgD^RHHcRFvl>leL6)3`ZlXNn_9<;-wkvahaAiSlr@@y6{
zK;fVwHkm9io90dO)^{HGpQnCx{G&tVBUt5xW6kpV?%gxr%urrac#V=HLV%L+l;^dv
z)CGoIf}Mx1NkDb|4g3D>`KMp`$i^@ZS2<4XKeFxN`O9Ws(4nvV@y`0PJqO5j2S5-I
zQPxl4%YVKf_z9O_*TExuf$KQ8V_8vtLp$E5s`fJrD6H-TRrf<04<83wF2at3M?}Qo
zGH$9=cgFm+#Vq29D9ue4Qx#GmLJ+B{gQ@;ptc-339_AvHW^|3^s{c2_3-Fq5d@wk1
z{P-=J-TofsIslgdaK@u3E3bPDR=yAXk~d&}*WLq`t9M(@4gw<Z2!taGNiP9UWdMc$
zZ;Xc5TdIU91P-`zjb`S&B|gq*eKuG1BY>Ybc+Y+{Yb$S4O4lkSkRb>VVF85U#Yy=E
zfYS{CoNnQh)?c{6syZZ1nji@G7?0csYbAyoOL+S;GkX>outn~4I;pDnTdJ<Lx}Hi^
z?J{%F<{W^)BZBbqJK$-6?4@PW!ox9Kzgj76C*_csEMabS*Hg|Ooo+vx;fJGbZdAmg
zq*F3OW<BMqZ&h7VRk`J=AQQp>0z%+HScETI%Fh6tX<*{y@t1d4uYX7C&GayHflIic
zOxh@Tad|0m`i^KN6{X~6smRP_RwTK=fk2ZxBBGRGxc?up%7i}ngArSudi~^EtUGgS
z>P~fK7Dz5p1Tk5t04|~^q98T{r7{V1K><P#fJ2-PAB2}jfhD%c5NtKKu<)nV<)v?z
z@V`sl-ge8Ci!*;rCWDazDFjFWh%*e~5rDhG+fz}>kAYRrlEa;|&CJd%o?2Y|hP=Ir
zPVYLaYGDa8#iZ5{XF>zm@>@Y554jhoEzfxwKM54hLGQbNh4X1f;|1mXE>-o_ma=5F
zS+^7d7qkc`!UX~0M2v}eNz(Jc8t0*-KWe!UD<@C%klrCnS1RW#l+vtZl{UkNcv%iF
iuM+;Eg6Dt{7vL|S?0arv!8<_!0000<MNUMnLSTX;8E4@D
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -104,19 +104,22 @@ loop.shared.actions = (function() {
/**
* Used for hanging up the call at the end of a successful call.
*/
HangupCall: Action.define("hangupCall", {
}),
/**
- * Used to indicate the peer hung up the call.
+ * Used to indicate the remote peer was disconnected for some reason.
+ *
+ * peerHungup is true if the peer intentionally disconnected, false otherwise.
*/
- PeerHungupCall: Action.define("peerHungupCall", {
+ RemotePeerDisconnected: Action.define("remotePeerDisconnected", {
+ peerHungup: Boolean
}),
/**
* Used for notifying of connection progress state changes.
* The connection refers to the overall connection flow as indicated
* on the websocket.
*/
ConnectionProgress: Action.define("connectionProgress", {
@@ -128,16 +131,28 @@ loop.shared.actions = (function() {
* Used for notifying of connection failures.
*/
ConnectionFailure: Action.define("connectionFailure", {
// A string relating to the reason the connection failed.
reason: String
}),
/**
+ * Used to notify that the sdk session is now connected to the servers.
+ */
+ ConnectedToSdkServers: Action.define("connectedToSdkServers", {
+ }),
+
+ /**
+ * Used to notify that a remote peer has connected to the room.
+ */
+ RemotePeerConnected: Action.define("remotePeerConnected", {
+ }),
+
+ /**
* Used by the ongoing views to notify stores about the elements
* required for the sdk.
*/
SetupStreamElements: Action.define("setupStreamElements", {
// The configuration for the publisher/subscribe options
publisherConfig: Object,
// The local stream element
getLocalElementFunc: Function,
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -15,20 +15,22 @@ loop.store.ActiveRoomStore = (function()
// The initial state of the room
INIT: "room-init",
// The store is gathering the room data
GATHER: "room-gather",
// The store has got the room data
READY: "room-ready",
// The room is known to be joined on the loop-server
JOINED: "room-joined",
+ // The room is connected to the sdk server.
+ SESSION_CONNECTED: "room-session-connected",
+ // There are participants in the room.
+ HAS_PARTICIPANTS: "room-has-participants",
// There was an issue with the room
- FAILED: "room-failed",
- // XXX to be implemented in bug 1074686/1074702
- HAS_PARTICIPANTS: "room-has-participants"
+ FAILED: "room-failed"
};
/**
* Store for things that are local to this instance (in this profile, on
* this machine) of this roomRoom store, in addition to a mirror of some
* remote-state.
*
* @extends {Backbone.Events}
@@ -46,41 +48,46 @@ loop.store.ActiveRoomStore = (function()
}
this._dispatcher = options.dispatcher;
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._mozLoop = options.mozLoop;
+ if (!options.sdkDriver) {
+ throw new Error("Missing option sdkDriver");
+ }
+ this._sdkDriver = options.sdkDriver;
+
+ // XXX Further actions are registered in setupWindowData and
+ // fetchServerData when we know what window type this is. At some stage,
+ // we might want to consider store mixins or some alternative which
+ // means the stores would only be created when we want them.
this._dispatcher.register(this, [
- "roomFailure",
"setupWindowData",
- "fetchServerData",
- "updateRoomInfo",
- "joinRoom",
- "joinedRoom",
- "windowUnload",
- "leaveRoom"
+ "fetchServerData"
]);
/**
* Stored data reflecting the local state of a given room, used to drive
* the room's views.
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
* for the main data. Additional properties below.
*
* @property {ROOM_STATES} roomState - the state of the room.
* @property {Error=} error - if the room is an error state, this will be
* set to an Error object reflecting the problem;
* otherwise it will be unset.
*/
this._storeState = {
- roomState: ROOM_STATES.INIT
+ roomState: ROOM_STATES.INIT,
+ audioMuted: false,
+ videoMuted: false
};
}
ActiveRoomStore.prototype = _.extend({
/**
* The time factor to adjust the expires time to ensure that we send a refresh
* before the expiry. Currently set as 90%.
*/
@@ -109,29 +116,51 @@ loop.store.ActiveRoomStore = (function()
this.setStoreState({
error: actionData.error,
roomState: ROOM_STATES.FAILED
});
},
/**
+ * Registers the actions with the dispatcher that this store is interested
+ * in.
+ */
+ _registerActions: function() {
+ this._dispatcher.register(this, [
+ "roomFailure",
+ "updateRoomInfo",
+ "joinRoom",
+ "joinedRoom",
+ "connectedToSdkServers",
+ "connectionFailure",
+ "setMute",
+ "remotePeerDisconnected",
+ "remotePeerConnected",
+ "windowUnload",
+ "leaveRoom"
+ ]);
+ },
+
+ /**
* Execute setupWindowData event action from the dispatcher. This gets
* the room data from the mozLoop api, and dispatches an UpdateRoomInfo event.
* It also dispatches JoinRoom as this action is only applicable to the desktop
* client, and needs to auto-join.
*
* @param {sharedActions.SetupWindowData} actionData
*/
setupWindowData: function(actionData) {
if (actionData.type !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
+ this._registerActions();
+
this.setStoreState({
roomState: ROOM_STATES.GATHER
});
// Get the window data from the mozLoop api.
this._mozLoop.rooms.get(actionData.roomToken,
function(error, roomData) {
if (error) {
@@ -164,16 +193,18 @@ loop.store.ActiveRoomStore = (function()
* @param {sharedActions.FetchServerData} actionData
*/
fetchServerData: function(actionData) {
if (actionData.windowType !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
+ this._registerActions();
+
this.setStoreState({
roomToken: actionData.token,
roomState: ROOM_STATES.READY
});
},
/**
* Handles the updateRoomInfo action. Updates the room data and
@@ -223,16 +254,67 @@ loop.store.ActiveRoomStore = (function()
this.setStoreState({
apiKey: actionData.apiKey,
sessionToken: actionData.sessionToken,
sessionId: actionData.sessionId,
roomState: ROOM_STATES.JOINED
});
this._setRefreshTimeout(actionData.expires);
+ this._sdkDriver.connectSession(actionData);
+ },
+
+ /**
+ * Handles recording when the sdk has connected to the servers.
+ */
+ connectedToSdkServers: function() {
+ this.setStoreState({
+ roomState: ROOM_STATES.SESSION_CONNECTED
+ });
+ },
+
+ /**
+ * Handles disconnection of this local client from the sdk servers.
+ */
+ connectionFailure: function() {
+ // Treat all reasons as something failed. In theory, clientDisconnected
+ // could be a success case, but there's no way we should be intentionally
+ // sending that and still have the window open.
+ this._leaveRoom(ROOM_STATES.FAILED);
+ },
+
+ /**
+ * Records the mute state for the stream.
+ *
+ * @param {sharedActions.setMute} actionData The mute state for the stream type.
+ */
+ setMute: function(actionData) {
+ var muteState = {};
+ muteState[actionData.type + "Muted"] = !actionData.enabled;
+ this.setStoreState(muteState);
+ },
+
+ /**
+ * Handles recording when a remote peer has connected to the servers.
+ */
+ remotePeerConnected: function() {
+ this.setStoreState({
+ roomState: ROOM_STATES.HAS_PARTICIPANTS
+ });
+ },
+
+ /**
+ * Handles a remote peer disconnecting from the session.
+ */
+ remotePeerDisconnected: function() {
+ // As we only support two users at the moment, we just set this
+ // back to joined.
+ this.setStoreState({
+ roomState: ROOM_STATES.SESSION_CONNECTED
+ });
},
/**
* Handles the window being unloaded. Ensures the room is left.
*/
windowUnload: function() {
this._leaveRoom();
},
@@ -270,32 +352,37 @@ loop.store.ActiveRoomStore = (function()
this._setRefreshTimeout(responseData.expires);
}.bind(this));
},
/**
* Handles leaving a room. Clears any membership timeouts, then
* signals to the server the leave of the room.
+ *
+ * @param {ROOM_STATES} nextState Optional; the next state to switch to.
+ * Switches to READY if undefined.
*/
- _leaveRoom: function() {
- if (this._storeState.roomState !== ROOM_STATES.JOINED) {
- return;
- }
+ _leaveRoom: function(nextState) {
+ this._sdkDriver.disconnectSession();
if (this._timeout) {
clearTimeout(this._timeout);
delete this._timeout;
}
- this._mozLoop.rooms.leave(this._storeState.roomToken,
- this._storeState.sessionToken);
+ if (this._storeState.roomState === ROOM_STATES.JOINED ||
+ this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
+ this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
+ this._mozLoop.rooms.leave(this._storeState.roomToken,
+ this._storeState.sessionToken);
+ }
this.setStoreState({
- roomState: ROOM_STATES.READY
+ roomState: nextState ? nextState : ROOM_STATES.READY
});
}
}, Backbone.Events);
return ActiveRoomStore;
})();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -113,28 +113,22 @@ loop.store.ConversationStore = (function
if (!options.sdkDriver) {
throw new Error("Missing option sdkDriver");
}
this.client = options.client;
this.dispatcher = options.dispatcher;
this.sdkDriver = options.sdkDriver;
+ // XXX Further actions are registered in setupWindowData when
+ // we know what window type this is. At some stage, we might want to
+ // consider store mixins or some alternative which means the stores
+ // would only be created when we want them.
this.dispatcher.register(this, [
- "connectionFailure",
- "connectionProgress",
- "setupWindowData",
- "connectCall",
- "hangupCall",
- "peerHungupCall",
- "cancelCall",
- "retryCall",
- "mediaConnected",
- "setMute",
- "fetchEmailLink"
+ "setupWindowData"
]);
},
/**
* Handles the connection failure action, setting the state to
* terminated.
*
* @param {sharedActions.ConnectionFailure} actionData The action data.
@@ -191,16 +185,29 @@ loop.store.ConversationStore = (function
setupWindowData: function(actionData) {
var windowType = actionData.type;
if (windowType !== "outgoing" &&
windowType !== "incoming") {
// Not for this store, don't do anything.
return;
}
+ this.dispatcher.register(this, [
+ "connectionFailure",
+ "connectionProgress",
+ "connectCall",
+ "hangupCall",
+ "remotePeerDisconnected",
+ "cancelCall",
+ "retryCall",
+ "mediaConnected",
+ "setMute",
+ "fetchEmailLink"
+ ]);
+
this.set({
contact: actionData.contact,
outgoing: windowType === "outgoing",
windowId: actionData.windowId,
callType: actionData.callType,
callState: CALL_STATES.GATHER,
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
});
@@ -231,21 +238,33 @@ loop.store.ConversationStore = (function
this._websocket.mediaFail();
}
this._endSession();
this.set({callState: CALL_STATES.FINISHED});
},
/**
- * The peer hungup the call.
+ * The remote peer disconnected from the session.
+ *
+ * @param {sharedActions.RemotePeerDisconnected} actionData
*/
- peerHungupCall: function() {
+ remotePeerDisconnected: function(actionData) {
this._endSession();
- this.set({callState: CALL_STATES.FINISHED});
+
+ // If the peer hungup, we end normally, otherwise
+ // we treat this as a call failure.
+ if (actionData.peerHungup) {
+ this.set({callState: CALL_STATES.FINISHED});
+ } else {
+ this.set({
+ callState: CALL_STATES.TERMINATED,
+ callStateReason: "peerNetworkDisconnected"
+ });
+ }
},
/**
* Cancels a call
*/
cancelCall: function() {
var callState = this.get("callState");
if (this._websocket &&
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -74,16 +74,17 @@ loop.OTSdkDriver = (function() {
* - apiKey: The OT API key
* - sessionToken: The token for the OT session
*
* @param {Object} sessionData The session data for setting up the OT session.
*/
connectSession: function(sessionData) {
this.session = this.sdk.initSession(sessionData.sessionId);
+ this.session.on("connectionCreated", this._onConnectionCreated.bind(this));
this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
this.session.on("connectionDestroyed",
this._onConnectionDestroyed.bind(this));
this.session.on("sessionDisconnected",
this._onSessionDisconnected.bind(this));
// This starts the actual session connection.
this.session.connect(sessionData.apiKey, sessionData.sessionToken,
@@ -125,39 +126,31 @@ loop.OTSdkDriver = (function() {
if (error) {
console.error("Failed to complete connection", error);
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: "couldNotConnect"
}));
return;
}
+ this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers());
this._sessionConnected = true;
this._maybePublishLocalStream();
},
/**
* Handles the connection event for a peer's connection being dropped.
*
* @param {SessionDisconnectEvent} event The event details
* https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
*/
_onConnectionDestroyed: function(event) {
- var action;
- if (event.reason === "clientDisconnected") {
- action = new sharedActions.PeerHungupCall();
- } else {
- // Strictly speaking this isn't a failure on our part, but since our
- // flow requires a full reconnection, then we just treat this as
- // if a failure of our end had occurred.
- action = new sharedActions.ConnectionFailure({
- reason: "peerNetworkDisconnected"
- });
- }
- this.dispatcher.dispatch(action);
+ this.dispatcher.dispatch(new sharedActions.RemotePeerDisconnected({
+ peerHungup: event.reason === "clientDisconnected"
+ }));
},
/**
* Handles the session event for the connection for this client being
* destroyed.
*
* @param {SessionDisconnectEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
@@ -166,16 +159,24 @@ loop.OTSdkDriver = (function() {
// We only need to worry about the network disconnected reason here.
if (event.reason === "networkDisconnected") {
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: "networkDisconnected"
}));
}
},
+ _onConnectionCreated: function(event) {
+ if (this.session.connection.id === event.connection.id) {
+ return;
+ }
+
+ this.dispatcher.dispatch(new sharedActions.RemotePeerConnected());
+ },
+
/**
* Handles the event when the remote stream is created.
*
* @param {StreamEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*/
_onRemoteStreamCreated: function(event) {
this.session.subscribe(event.stream,
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -723,17 +723,18 @@ loop.shared.views = (function(_, OT, l10
var classObject = { button: true, disabled: this.props.disabled };
if (this.props.additionalClass) {
classObject[this.props.additionalClass] = true;
}
return (
React.DOM.button({onClick: this.props.onClick,
disabled: this.props.disabled,
className: cx(classObject)},
- this.props.caption
+ React.DOM.span({className: "button-caption"}, this.props.caption),
+ this.props.children
)
)
}
});
var ButtonGroup = React.createClass({displayName: 'ButtonGroup',
PropTypes: {
additionalClass: React.PropTypes.string
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -723,17 +723,18 @@ loop.shared.views = (function(_, OT, l10
var classObject = { button: true, disabled: this.props.disabled };
if (this.props.additionalClass) {
classObject[this.props.additionalClass] = true;
}
return (
<button onClick={this.props.onClick}
disabled={this.props.disabled}
className={cx(classObject)}>
- {this.props.caption}
+ <span className="button-caption">{this.props.caption}</span>
+ {this.props.children}
</button>
)
}
});
var ButtonGroup = React.createClass({
PropTypes: {
additionalClass: React.PropTypes.string
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -27,17 +27,18 @@ browser.jar:
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
content/browser/loop/shared/css/contacts.css (content/shared/css/contacts.css)
# Shared images
content/browser/loop/shared/img/happy.png (content/shared/img/happy.png)
content/browser/loop/shared/img/sad.png (content/shared/img/sad.png)
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
- content/browser/loop/shared/img/loading-icon.gif (content/shared/img/loading-icon.gif)
+ content/browser/loop/shared/img/spinner.png (content/shared/img/spinner.png)
+ content/browser/loop/shared/img/spinner@2x.png (content/shared/img/spinner@2x.png)
content/browser/loop/shared/img/audio-inverse-14x14.png (content/shared/img/audio-inverse-14x14.png)
content/browser/loop/shared/img/audio-inverse-14x14@2x.png (content/shared/img/audio-inverse-14x14@2x.png)
content/browser/loop/shared/img/facemute-14x14.png (content/shared/img/facemute-14x14.png)
content/browser/loop/shared/img/facemute-14x14@2x.png (content/shared/img/facemute-14x14@2x.png)
content/browser/loop/shared/img/hangup-inverse-14x14.png (content/shared/img/hangup-inverse-14x14.png)
content/browser/loop/shared/img/hangup-inverse-14x14@2x.png (content/shared/img/hangup-inverse-14x14@2x.png)
content/browser/loop/shared/img/mute-inverse-14x14.png (content/shared/img/mute-inverse-14x14.png)
content/browser/loop/shared/img/mute-inverse-14x14@2x.png (content/shared/img/mute-inverse-14x14@2x.png)
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -91,16 +91,17 @@
<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/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
+ <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>
<script>
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -982,26 +982,31 @@ loop.webapp = (function($, _, OT, mozL10
url: document.location.origin
});
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
+ var sdkDriver = new loop.OTSdkDriver({
+ dispatcher: dispatcher,
+ sdk: OT
+ });
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: standaloneMozLoop
+ mozLoop: standaloneMozLoop,
+ sdkDriver: sdkDriver
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(WebappRootView({
client: client,
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -982,26 +982,31 @@ loop.webapp = (function($, _, OT, mozL10
url: document.location.origin
});
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
+ var sdkDriver = new loop.OTSdkDriver({
+ dispatcher: dispatcher,
+ sdk: OT
+ });
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: standaloneMozLoop
+ mozLoop: standaloneMozLoop,
+ sdkDriver: sdkDriver
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(<WebappRootView
client={client}
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -138,17 +138,18 @@ describe("loop.conversation", function()
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.AppControllerView({
client: client,
conversation: conversation,
roomStore: roomStore,
sdk: {},
conversationStore: conversationStore,
- conversationAppStore: conversationAppStore
+ conversationAppStore: conversationAppStore,
+ dispatcher: dispatcher
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {}
@@ -209,17 +210,17 @@ describe("loop.conversation", function()
});
it("should display the RoomView for rooms", function() {
conversationAppStore.setStoreState({windowType: "room"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
- loop.roomViews.DesktopRoomControllerView);
+ loop.roomViews.DesktopRoomConversationView);
});
it("should display the GenericFailureView for failures", function() {
conversationAppStore.setStoreState({windowType: "failed"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -27,17 +27,18 @@ describe("loop.roomViews", function () {
// XXX These stubs should be hoisted in a common file
// Bug 1040968
sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: {}
+ mozLoop: {},
+ sdkDriver: {}
});
roomStore = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: {},
activeRoomStore: activeRoomStore
});
});
@@ -57,46 +58,117 @@ describe("loop.roomViews", function () {
});
var testView = TestUtils.renderIntoDocument(TestView({
roomStore: activeRoomStore
}));
expect(testView.state).eql({
roomState: ROOM_STATES.INIT,
+ audioMuted: false,
+ videoMuted: false,
foo: "bar"
});
});
it("should listen to store changes", function() {
var TestView = React.createClass({
mixins: [loop.roomViews.ActiveRoomStoreMixin],
render: function() { return React.DOM.div(); }
});
var testView = TestUtils.renderIntoDocument(TestView({
roomStore: activeRoomStore
}));
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
- expect(testView.state).eql({roomState: ROOM_STATES.READY});
+ expect(testView.state.roomState).eql(ROOM_STATES.READY);
});
});
- describe("DesktopRoomControllerView", function() {
+ describe("DesktopRoomConversationView", function() {
var view;
+ beforeEach(function() {
+ sandbox.stub(dispatcher, "dispatch");
+ });
+
function mountTestComponent() {
return TestUtils.renderIntoDocument(
- new loop.roomViews.DesktopRoomControllerView({
- mozLoop: {},
+ new loop.roomViews.DesktopRoomConversationView({
+ dispatcher: dispatcher,
roomStore: roomStore
}));
}
+ it("should dispatch a setupStreamElements action when the view is created",
+ function() {
+ view = mountTestComponent();
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "setupStreamElements"));
+ });
+
+ it("should dispatch a setMute action when the audio mute button is pressed",
+ function() {
+ view = mountTestComponent();
+
+ view.setState({audioMuted: true});
+
+ var muteBtn = view.getDOMNode().querySelector('.btn-mute-audio');
+
+ React.addons.TestUtils.Simulate.click(muteBtn);
+
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "setMute"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("enabled", true));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("type", "audio"));
+ });
+
+ it("should dispatch a setMute action when the video mute button is pressed",
+ function() {
+ view = mountTestComponent();
+
+ view.setState({videoMuted: false});
+
+ var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
+
+ React.addons.TestUtils.Simulate.click(muteBtn);
+
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "setMute"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("enabled", false));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("type", "video"));
+ });
+
+ it("should set the mute button as mute off", function() {
+ view = mountTestComponent();
+
+ view.setState({videoMuted: false});
+
+ var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
+
+ expect(muteBtn.classList.contains("muted")).eql(false);
+ });
+
+ it("should set the mute button as mute on", function() {
+ view = mountTestComponent();
+
+ view.setState({audioMuted: true});
+
+ var muteBtn = view.getDOMNode().querySelector('.btn-mute-audio');
+
+ expect(muteBtn.classList.contains("muted")).eql(true);
+ });
+
describe("#render", function() {
it("should set document.title to store.serverData.roomName", function() {
mountTestComponent();
activeRoomStore.setStoreState({roomName: "fakeName"});
expect(fakeWindow.document.title).to.equal("fakeName");
});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -2,17 +2,17 @@
var expect = chai.expect;
var sharedActions = loop.shared.actions;
describe("loop.store.ActiveRoomStore", function () {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
- var sandbox, dispatcher, store, fakeMozLoop;
+ var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
@@ -20,18 +20,26 @@ describe("loop.store.ActiveRoomStore", f
rooms: {
get: sandbox.stub(),
join: sandbox.stub(),
refreshMembership: sandbox.stub(),
leave: sandbox.stub()
}
};
- store = new loop.store.ActiveRoomStore(
- {mozLoop: fakeMozLoop, dispatcher: dispatcher});
+ fakeSdkDriver = {
+ connectSession: sandbox.stub(),
+ disconnectSession: sandbox.stub()
+ };
+
+ store = new loop.store.ActiveRoomStore({
+ dispatcher: dispatcher,
+ mozLoop: fakeMozLoop,
+ sdkDriver: fakeSdkDriver
+ });
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should throw an error if the dispatcher is missing", function() {
@@ -40,16 +48,22 @@ describe("loop.store.ActiveRoomStore", f
}).to.Throw(/dispatcher/);
});
it("should throw an error if mozLoop is missing", function() {
expect(function() {
new loop.store.ActiveRoomStore({dispatcher: dispatcher});
}).to.Throw(/mozLoop/);
});
+
+ it("should throw an error if sdkDriver is missing", function() {
+ expect(function() {
+ new loop.store.ActiveRoomStore({dispatcher: dispatcher, mozLoop: {}});
+ }).to.Throw(/sdkDriver/);
+ });
});
describe("#roomFailure", function() {
var fakeError;
beforeEach(function() {
sandbox.stub(console, "error");
@@ -276,16 +290,26 @@ describe("loop.store.ActiveRoomStore", f
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
var state = store.getStoreState();
expect(state.apiKey).eql(fakeJoinedData.apiKey);
expect(state.sessionToken).eql(fakeJoinedData.sessionToken);
expect(state.sessionId).eql(fakeJoinedData.sessionId);
});
+ it("should start the session connection with the sdk", function() {
+ var actionData = new sharedActions.JoinedRoom(fakeJoinedData);
+
+ store.joinedRoom(actionData);
+
+ sinon.assert.calledOnce(fakeSdkDriver.connectSession);
+ sinon.assert.calledWithExactly(fakeSdkDriver.connectSession,
+ actionData);
+ });
+
it("should call mozLoop.rooms.refreshMembership before the expiresTime",
function() {
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
sandbox.clock.tick(fakeJoinedData.expires * 1000);
sinon.assert.calledOnce(fakeMozLoop.rooms.refreshMembership);
sinon.assert.calledWith(fakeMozLoop.rooms.refreshMembership,
@@ -325,25 +349,118 @@ describe("loop.store.ActiveRoomStore", f
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RoomFailure({
error: fakeError
}));
});
});
+ describe("#connectedToSdkServers", function() {
+ it("should set the state to `SESSION_CONNECTED`", function() {
+ store.connectedToSdkServers(new sharedActions.ConnectedToSdkServers());
+
+ expect(store.getStoreState().roomState).eql(ROOM_STATES.SESSION_CONNECTED);
+ });
+ });
+
+ describe("#connectionFailure", function() {
+ beforeEach(function() {
+ store.setStoreState({
+ roomState: ROOM_STATES.JOINED,
+ roomToken: "fakeToken",
+ sessionToken: "1627384950"
+ });
+ });
+
+ it("should disconnect from the servers via the sdk", function() {
+ store.connectionFailure();
+
+ sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
+ });
+
+ it("should clear any existing timeout", function() {
+ sandbox.stub(window, "clearTimeout");
+ store._timeout = {};
+
+ store.connectionFailure();
+
+ sinon.assert.calledOnce(clearTimeout);
+ });
+
+ it("should call mozLoop.rooms.leave", function() {
+ store.connectionFailure();
+
+ sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
+ sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
+ "fakeToken", "1627384950");
+ });
+
+ it("should set the state to `FAILED`", function() {
+ store.connectionFailure();
+
+ expect(store.getStoreState().roomState).eql(ROOM_STATES.FAILED);
+ });
+ });
+
+ describe("#setMute", function() {
+ it("should save the mute state for the audio stream", function() {
+ store.setStoreState({audioMuted: false});
+
+ store.setMute(new sharedActions.SetMute({
+ type: "audio",
+ enabled: true
+ }));
+
+ expect(store.getStoreState().audioMuted).eql(false);
+ });
+
+ it("should save the mute state for the video stream", function() {
+ store.setStoreState({videoMuted: true});
+
+ store.setMute(new sharedActions.SetMute({
+ type: "video",
+ enabled: false
+ }));
+
+ expect(store.getStoreState().videoMuted).eql(true);
+ });
+ });
+
+ describe("#remotePeerConnected", function() {
+ it("should set the state to `HAS_PARTICIPANTS`", function() {
+ store.remotePeerConnected();
+
+ expect(store.getStoreState().roomState).eql(ROOM_STATES.HAS_PARTICIPANTS);
+ });
+ });
+
+ describe("#remotePeerDisconnected", function() {
+ it("should set the state to `SESSION_CONNECTED`", function() {
+ store.remotePeerDisconnected();
+
+ expect(store.getStoreState().roomState).eql(ROOM_STATES.SESSION_CONNECTED);
+ });
+ });
+
describe("#windowUnload", function() {
beforeEach(function() {
store.setStoreState({
roomState: ROOM_STATES.JOINED,
roomToken: "fakeToken",
sessionToken: "1627384950"
});
});
+ it("should disconnect from the servers via the sdk", function() {
+ store.windowUnload();
+
+ sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
+ });
+
it("should clear any existing timeout", function() {
sandbox.stub(window, "clearTimeout");
store._timeout = {};
store.windowUnload();
sinon.assert.calledOnce(clearTimeout);
});
@@ -367,16 +484,22 @@ describe("loop.store.ActiveRoomStore", f
beforeEach(function() {
store.setStoreState({
roomState: ROOM_STATES.JOINED,
roomToken: "fakeToken",
sessionToken: "1627384950"
});
});
+ it("should disconnect from the servers via the sdk", function() {
+ store.leaveRoom();
+
+ sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
+ });
+
it("should clear any existing timeout", function() {
sandbox.stub(window, "clearTimeout");
store._timeout = {};
store.leaveRoom();
sinon.assert.calledOnce(clearTimeout);
});
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -127,97 +127,97 @@ describe("loop.store.ConversationStore",
describe("#connectionFailure", function() {
beforeEach(function() {
store._websocket = fakeWebsocket;
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
- dispatcher.dispatch(
+ store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should ensure the websocket is closed", function() {
- dispatcher.dispatch(
+ store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the state to 'terminated'", function() {
store.set({callState: CALL_STATES.ALERTING});
- dispatcher.dispatch(
+ store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
expect(store.get("callStateReason")).eql("fake");
});
it("should release mozLoop callsData", function() {
- dispatcher.dispatch(
+ store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
describe("#connectionProgress", function() {
describe("progress: init", function() {
it("should change the state from 'gather' to 'connecting'", function() {
store.set({callState: CALL_STATES.GATHER});
- dispatcher.dispatch(
+ store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.INIT}));
expect(store.get("callState")).eql(CALL_STATES.CONNECTING);
});
});
describe("progress: alerting", function() {
it("should change the state from 'gather' to 'alerting'", function() {
store.set({callState: CALL_STATES.GATHER});
- dispatcher.dispatch(
+ store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
it("should change the state from 'init' to 'alerting'", function() {
store.set({callState: CALL_STATES.INIT});
- dispatcher.dispatch(
+ store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
});
describe("progress: connecting", function() {
beforeEach(function() {
store.set({callState: CALL_STATES.ALERTING});
});
it("should change the state to 'ongoing'", function() {
- dispatcher.dispatch(
+ store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
expect(store.get("callState")).eql(CALL_STATES.ONGOING);
});
it("should connect the session", function() {
store.set(fakeSessionData);
- dispatcher.dispatch(
+ store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
sinon.assert.calledOnce(sdkDriver.connectSession);
sinon.assert.calledWithExactly(sdkDriver.connectSession, {
apiKey: "fakeKey",
sessionId: "321456",
sessionToken: "341256"
});
@@ -381,54 +381,54 @@ describe("loop.store.ConversationStore",
sinon.match.hasOwn("reason", "setup"));
});
});
});
});
describe("#connectCall", function() {
it("should save the call session data", function() {
- dispatcher.dispatch(
+ store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
expect(store.get("apiKey")).eql("fakeKey");
expect(store.get("callId")).eql("142536");
expect(store.get("sessionId")).eql("321456");
expect(store.get("sessionToken")).eql("341256");
expect(store.get("websocketToken")).eql("543216");
expect(store.get("progressURL")).eql("fakeURL");
});
it("should initialize the websocket", function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() { return connectPromise; },
on: sinon.spy()
});
- dispatcher.dispatch(
+ store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
url: "fakeURL",
callId: "142536",
websocketToken: "543216"
});
});
it("should connect the websocket to the server", function() {
- dispatcher.dispatch(
+ store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sinon.assert.calledOnce(store._websocket.promiseConnect);
});
describe("WebSocket connection result", function() {
beforeEach(function() {
- dispatcher.dispatch(
+ store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connection progress action on success", function(done) {
resolveConnectPromise(WS_STATES.INIT);
@@ -475,168 +475,194 @@ describe("loop.store.ConversationStore",
mediaFail: wsMediaFailSpy,
close: wsCloseSpy
};
store.set({callState: CALL_STATES.ONGOING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
- dispatcher.dispatch(new sharedActions.HangupCall());
+ store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should send a media-fail message to the websocket if it is open", function() {
- dispatcher.dispatch(new sharedActions.HangupCall());
+ store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(wsMediaFailSpy);
});
it("should ensure the websocket is closed", function() {
- dispatcher.dispatch(new sharedActions.HangupCall());
+ store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the callState to finished", function() {
- dispatcher.dispatch(new sharedActions.HangupCall());
+ store.hangupCall(new sharedActions.HangupCall());
expect(store.get("callState")).eql(CALL_STATES.FINISHED);
});
it("should release mozLoop callsData", function() {
- dispatcher.dispatch(new sharedActions.HangupCall());
+ store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
- describe("#peerHungupCall", function() {
+ describe("#remotePeerDisconnected", function() {
var wsMediaFailSpy, wsCloseSpy;
beforeEach(function() {
wsMediaFailSpy = sinon.spy();
wsCloseSpy = sinon.spy();
store._websocket = {
mediaFail: wsMediaFailSpy,
close: wsCloseSpy
};
store.set({callState: CALL_STATES.ONGOING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
- dispatcher.dispatch(new sharedActions.PeerHungupCall());
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: true
+ }));
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should ensure the websocket is closed", function() {
- dispatcher.dispatch(new sharedActions.PeerHungupCall());
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: true
+ }));
sinon.assert.calledOnce(wsCloseSpy);
});
- it("should set the callState to finished", function() {
- dispatcher.dispatch(new sharedActions.PeerHungupCall());
+ it("should release mozLoop callsData", function() {
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: true
+ }));
+
+ sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+ sinon.assert.calledWithExactly(
+ navigator.mozLoop.calls.clearCallInProgress, "42");
+ });
+
+ it("should set the callState to finished if the peer hungup", function() {
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: true
+ }));
expect(store.get("callState")).eql(CALL_STATES.FINISHED);
});
- it("should release mozLoop callsData", function() {
- dispatcher.dispatch(new sharedActions.PeerHungupCall());
+ it("should set the callState to terminated if the peer was disconnected" +
+ "for an unintentional reason", function() {
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: false
+ }));
- sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
- sinon.assert.calledWithExactly(
- navigator.mozLoop.calls.clearCallInProgress, "42");
+ expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
+ });
+
+ it("should set the reason to peerNetworkDisconnected if the peer was" +
+ "disconnected for an unintentional reason", function() {
+ store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
+ peerHungup: false
+ }));
+
+ expect(store.get("callStateReason")).eql("peerNetworkDisconnected");
});
});
describe("#cancelCall", function() {
beforeEach(function() {
store._websocket = fakeWebsocket;
store.set({callState: CALL_STATES.CONNECTING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should send a cancel message to the websocket if it is open", function() {
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(wsCancelSpy);
});
it("should ensure the websocket is closed", function() {
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the state to close if the call is connecting", function() {
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
expect(store.get("callState")).eql(CALL_STATES.CLOSE);
});
it("should set the state to close if the call has terminated already", function() {
store.set({callState: CALL_STATES.TERMINATED});
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
expect(store.get("callState")).eql(CALL_STATES.CLOSE);
});
it("should release mozLoop callsData", function() {
- dispatcher.dispatch(new sharedActions.CancelCall());
+ store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
describe("#retryCall", function() {
it("should set the state to gather", function() {
store.set({callState: CALL_STATES.TERMINATED});
- dispatcher.dispatch(new sharedActions.RetryCall());
+ store.retryCall(new sharedActions.RetryCall());
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should request the outgoing call data", function() {
store.set({
callState: CALL_STATES.TERMINATED,
outgoing: true,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
contact: contact
});
- dispatcher.dispatch(new sharedActions.RetryCall());
+ store.retryCall(new sharedActions.RetryCall());
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
});
describe("#mediaConnected", function() {
it("should send mediaUp via the websocket", function() {
store._websocket = fakeWebsocket;
- dispatcher.dispatch(new sharedActions.MediaConnected());
+ store.mediaConnected(new sharedActions.MediaConnected());
sinon.assert.calledOnce(wsMediaUpSpy);
});
});
describe("#setMute", function() {
it("should save the mute state for the audio stream", function() {
store.set({"audioMuted": false});
@@ -658,49 +684,49 @@ describe("loop.store.ConversationStore",
}));
expect(store.get("videoMuted")).eql(true);
});
});
describe("#fetchEmailLink", function() {
it("should request a new call url to the server", function() {
- dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ store.fetchEmailLink(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(client.requestCallUrl);
sinon.assert.calledWith(client.requestCallUrl, "");
});
it("should update the emailLink attribute when the new call url is received",
function() {
client.requestCallUrl = function(callId, cb) {
cb(null, {callUrl: "http://fake.invalid/"});
};
- dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ store.fetchEmailLink(new sharedActions.FetchEmailLink());
expect(store.get("emailLink")).eql("http://fake.invalid/");
});
it("should trigger an error:emailLink event in case of failure",
function() {
var trigger = sandbox.stub(store, "trigger");
client.requestCallUrl = function(callId, cb) {
cb("error");
};
- dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ store.fetchEmailLink(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(trigger);
sinon.assert.calledWithExactly(trigger, "error:emailLink");
});
});
describe("Events", function() {
describe("Websocket progress", function() {
beforeEach(function() {
- dispatcher.dispatch(
+ store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connection failure action on 'terminate'", function() {
store._websocket.trigger("progress", {
state: WS_STATES.TERMINATED,
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -226,36 +226,40 @@ describe("loop.OTSdkDriver", function ()
getRemoteElementFunc: function() {return fakeRemoteElement;},
publisherConfig: publisherConfig
}));
sandbox.stub(dispatcher, "dispatch");
});
describe("connectionDestroyed", function() {
- it("should dispatch a peerHungupCall action if the client disconnected", function() {
- session.trigger("connectionDestroyed", {
- reason: "clientDisconnected"
+ it("should dispatch a remotePeerDisconnected action if the client" +
+ "disconnected", function() {
+ session.trigger("connectionDestroyed", {
+ reason: "clientDisconnected"
+ });
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "remotePeerDisconnected"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("peerHungup", true));
});
- sinon.assert.calledOnce(dispatcher.dispatch);
- sinon.assert.calledWithMatch(dispatcher.dispatch,
- sinon.match.hasOwn("name", "peerHungupCall"));
- });
+ it("should dispatch a remotePeerDisconnected action if the connection" +
+ "failed", function() {
+ session.trigger("connectionDestroyed", {
+ reason: "networkDisconnected"
+ });
- it("should dispatch a connectionFailure action if the connection failed", function() {
- session.trigger("connectionDestroyed", {
- reason: "networkDisconnected"
- });
-
- sinon.assert.calledOnce(dispatcher.dispatch);
- sinon.assert.calledWithMatch(dispatcher.dispatch,
- sinon.match.hasOwn("name", "connectionFailure"));
- sinon.assert.calledWithMatch(dispatcher.dispatch,
- sinon.match.hasOwn("reason", "peerNetworkDisconnected"));
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "remotePeerDisconnected"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("peerHungup", false));
});
});
describe("sessionDisconnected", function() {
it("should dispatch a connectionFailure action if the session was disconnected",
function() {
session.trigger("sessionDisconnected", {
reason: "networkDisconnected"
@@ -291,10 +295,38 @@ describe("loop.OTSdkDriver", function ()
session.trigger("streamCreated", {stream: fakeStream});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "mediaConnected"));
});
});
+
+ describe("connectionCreated", function() {
+ beforeEach(function() {
+ session.connection = {
+ id: "localUser"
+ };
+ });
+
+ it("should dispatch a RemotePeerConnected action if this is for a remote user",
+ function() {
+ session.trigger("connectionCreated", {
+ connection: {id: "remoteUser"}
+ });
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.RemotePeerConnected());
+ });
+
+ it("should not dispatch an action if this is for a local user",
+ function() {
+ session.trigger("connectionCreated", {
+ connection: {id: "localUser"}
+ });
+
+ sinon.assert.notCalled(dispatcher.dispatch);
+ });
+ });
});
});
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -365,17 +365,18 @@ describe("loop.store.RoomStore", functio
});
describe("ActiveRoomStore substore", function() {
var store, activeRoomStore;
beforeEach(function() {
activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: fakeMozLoop
+ mozLoop: fakeMozLoop,
+ sdkDriver: {}
});
store = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop,
activeRoomStore: activeRoomStore
});
});
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -36,16 +36,17 @@
<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="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
+ <script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->
<script src="standalone_client_test.js"></script>
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -607,17 +607,18 @@ describe("loop.webapp", function() {
sdk: sdk
});
client = new loop.StandaloneClient({
baseServerUrl: "fakeUrl"
});
dispatcher = new loop.Dispatcher();
activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: {}
+ mozLoop: {},
+ sdkDriver: {}
});
standaloneAppStore = new loop.store.StandaloneAppStore({
dispatcher: dispatcher,
sdk: sdk,
helper: helper,
conversation: conversationModel
});
// Stub this to stop the StartConversationView kicking in the request and
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -17,33 +17,35 @@
// 1. Desktop components
// 1.1 Panel
var PanelView = loop.panel.PanelView;
// 1.2. Conversation Window
var IncomingCallView = loop.conversation.IncomingCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
- var DesktopRoomInvitationView = loop.roomViews.DesktopRoomInvitationView;
// 2. Standalone webapp
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var ConversationView = loop.shared.views.ConversationView;
var FeedbackView = loop.shared.views.FeedbackView;
+ // Room constants
+ var ROOM_STATES = loop.store.ROOM_STATES;
+
// Local helpers
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
@@ -56,17 +58,18 @@
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
var dispatcher = new loop.Dispatcher();
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: navigator.mozLoop
+ mozLoop: navigator.mozLoop,
+ sdkDriver: {}
});
var roomStore = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
// Local mocks
@@ -528,30 +531,34 @@
Section({name: "UnsupportedDeviceView"},
Example({summary: "Standalone Unsupported Device"},
React.DOM.div({className: "standalone"},
UnsupportedDeviceView(null)
)
)
),
- Section({name: "DesktopRoomInvitationView"},
- Example({summary: "Desktop room invitation", dashed: "true",
+ Section({name: "DesktopRoomConversationView"},
+ Example({summary: "Desktop room conversation (invitation)", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"},
- DesktopRoomInvitationView({roomStore: roomStore})
+ DesktopRoomConversationView({
+ roomStore: roomStore,
+ dispatcher: dispatcher,
+ roomState: ROOM_STATES.INIT})
)
- )
- ),
+ ),
- Section({name: "DesktopRoomConversationView"},
Example({summary: "Desktop room conversation", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"},
- DesktopRoomConversationView({roomStore: roomStore})
+ DesktopRoomConversationView({
+ roomStore: roomStore,
+ dispatcher: dispatcher,
+ roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
)
),
Section({name: "SVG icons preview"},
Example({summary: "16x16"},
SVGIcons(null)
)
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -17,33 +17,35 @@
// 1. Desktop components
// 1.1 Panel
var PanelView = loop.panel.PanelView;
// 1.2. Conversation Window
var IncomingCallView = loop.conversation.IncomingCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
- var DesktopRoomInvitationView = loop.roomViews.DesktopRoomInvitationView;
// 2. Standalone webapp
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var ConversationView = loop.shared.views.ConversationView;
var FeedbackView = loop.shared.views.FeedbackView;
+ // Room constants
+ var ROOM_STATES = loop.store.ROOM_STATES;
+
// Local helpers
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
@@ -56,17 +58,18 @@
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
var dispatcher = new loop.Dispatcher();
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- mozLoop: navigator.mozLoop
+ mozLoop: navigator.mozLoop,
+ sdkDriver: {}
});
var roomStore = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
// Local mocks
@@ -528,30 +531,34 @@
<Section name="UnsupportedDeviceView">
<Example summary="Standalone Unsupported Device">
<div className="standalone">
<UnsupportedDeviceView />
</div>
</Example>
</Section>
- <Section name="DesktopRoomInvitationView">
- <Example summary="Desktop room invitation" dashed="true"
+ <Section name="DesktopRoomConversationView">
+ <Example summary="Desktop room conversation (invitation)" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="fx-embedded">
- <DesktopRoomInvitationView roomStore={roomStore} />
+ <DesktopRoomConversationView
+ roomStore={roomStore}
+ dispatcher={dispatcher}
+ roomState={ROOM_STATES.INIT} />
</div>
</Example>
- </Section>
- <Section name="DesktopRoomConversationView">
<Example summary="Desktop room conversation" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="fx-embedded">
- <DesktopRoomConversationView roomStore={roomStore} />
+ <DesktopRoomConversationView
+ roomStore={roomStore}
+ dispatcher={dispatcher}
+ roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</Example>
</Section>
<Section name="SVG icons preview">
<Example summary="16x16">
<SVGIcons />
</Example>
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -630,31 +630,18 @@ let SessionStoreInternal = {
let tabData = browser.__SS_data;
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activePageData = tabData.entries[tabData.index - 1] || null;
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
- // If the page has a title, set it.
- if (activePageData) {
- if (activePageData.title) {
- tab.label = activePageData.title;
- tab.crop = "end";
- } else if (activePageData.url != "about:blank") {
- tab.label = activePageData.url;
- tab.crop = "center";
- }
- }
-
- // Restore the tab icon.
- if ("image" in tabData) {
- win.gBrowser.setIcon(tab, tabData.image);
- }
+ // Update tab label and icon again after the tab history was updated.
+ this.updateTabLabelAndIcon(tab, tabData);
let event = win.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
tab.dispatchEvent(event);
}
break;
case "SessionStore:restoreTabContentStarted":
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
@@ -1855,16 +1842,36 @@ let SessionStoreInternal = {
},
persistTabAttribute: function ssi_persistTabAttribute(aName) {
if (TabAttributes.persist(aName)) {
this.saveStateDelayed();
}
},
+ updateTabLabelAndIcon(tab, tabData) {
+ let activePageData = tabData.entries[tabData.index - 1] || null;
+
+ // If the page has a title, set it.
+ if (activePageData) {
+ if (activePageData.title) {
+ tab.label = activePageData.title;
+ tab.crop = "end";
+ } else if (activePageData.url != "about:blank") {
+ tab.label = activePageData.url;
+ tab.crop = "center";
+ }
+ }
+
+ // Restore the tab icon.
+ if ("image" in tabData) {
+ tab.ownerDocument.defaultView.gBrowser.setIcon(tab, tabData.image);
+ }
+ },
+
/**
* Restores the session state stored in LastSession. This will attempt
* to merge data into the current session. If a window was opened at startup
* with pinned tab(s), then the remaining data from the previous session for
* that window will be opened into that winddow. Otherwise new windows will
* be opened.
*/
restoreLastSession: function ssi_restoreLastSession() {
@@ -2527,19 +2534,27 @@ let SessionStoreInternal = {
// If provided, set the selected tab.
if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
tabbrowser.selectedTab = aTabs[aSelectTab - 1];
// Update the window state in case we shut down without being notified.
this._windows[aWindow.__SSi].selected = aSelectTab;
}
+ // If we restore the selected tab, make sure it goes first.
+ let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab);
+ if (selectedIndex > -1) {
+ this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]);
+ }
+
// Restore all tabs.
for (let t = 0; t < aTabs.length; t++) {
- this.restoreTab(aTabs[t], aTabData[t]);
+ if (t != selectedIndex) {
+ this.restoreTab(aTabs[t], aTabData[t]);
+ }
}
},
// Restores the given tab state for a given tab.
restoreTab(tab, tabData, options = {}) {
let restoreImmediately = options.restoreImmediately;
let loadArguments = options.loadArguments;
let browser = tab.linkedBrowser;
@@ -2635,16 +2650,20 @@ let SessionStoreInternal = {
formdata: tabData.formdata || null,
disallow: tabData.disallow || null,
pageStyle: tabData.pageStyle || null
});
browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
{tabData: tabData, epoch: epoch});
+ // Update tab label and icon to show something
+ // while we wait for the messages to be processed.
+ this.updateTabLabelAndIcon(tab, tabData);
+
// Restore tab attributes.
if ("attributes" in tabData) {
TabAttributes.set(tab, tabData.attributes);
}
// This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
// it ensures each window will have its selected tab loaded.
if (restoreImmediately || tabbrowser.selectedBrowser == browser || loadArguments) {
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -756,16 +756,20 @@ Toolbox.prototype = {
}
});
// Tilt is handled separately because it is disabled in E10S mode. Because
// we have removed tilt from toolboxButtons we have to deal with it here.
let tiltEnabled = !this.target.isMultiProcess &&
Services.prefs.getBoolPref("devtools.command-button-tilt.enabled");
let tiltButton = this.doc.getElementById("command-button-tilt");
+ // Remote toolboxes don't add the button to the DOM at all
+ if (!tiltButton) {
+ return;
+ }
if (tiltEnabled) {
tiltButton.removeAttribute("hidden");
} else {
tiltButton.setAttribute("hidden", "true");
}
},
--- a/browser/devtools/shared/doorhanger.js
+++ b/browser/devtools/shared/doorhanger.js
@@ -71,16 +71,24 @@ let panelAttrs = {
*/
exports.showDoorhanger = Task.async(function *({ window, type, anchor }) {
let { predicate, success, url, action } = TYPES[type];
// Abort if predicate fails
if (!predicate()) {
return;
}
+ // Call success function to set preferences/cleanup immediately,
+ // so if triggered multiple times, only happens once (Windows/Linux)
+ success();
+
+ // Wait 200ms to prevent flickering where the popup is displayed
+ // before the underlying window (Windows 7, 64bit)
+ yield wait(200);
+
let document = window.document;
let panel = document.createElementNS(XULNS, "panel");
let frame = document.createElementNS(XULNS, "iframe");
let parentEl = document.querySelector("window");
frame.setAttribute("src", url);
let close = () => parentEl.removeChild(panel);
@@ -103,19 +111,16 @@ exports.showDoorhanger = Task.async(func
if (goBtn) {
goBtn.addEventListener("click", () => {
if (action) {
action();
}
close();
});
}
-
- // Call success function to set preferences, etc.
- success();
});
function setDoorhangerStyle (panel, frame) {
Object.keys(panelAttrs).forEach(prop => panel.setAttribute(prop, panelAttrs[prop]));
panel.style.margin = "20px";
panel.style.borderRadius = "5px";
panel.style.border = "none";
panel.style.MozAppearance = "none";
@@ -142,8 +147,14 @@ function onFrameLoad (frame) {
}
return promise;
}
function getGBrowser () {
return getMostRecentBrowserWindow().gBrowser;
}
+
+function wait (n) {
+ let { resolve, promise } = Promise.defer();
+ setTimeout(resolve, n);
+ return promise;
+}
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -140,16 +140,21 @@ Telemetry.prototype = {
userHistogram: "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
},
netmonitor: {
histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
},
+ storage: {
+ histogram: "DEVTOOLS_STORAGE_OPENED_BOOLEAN",
+ userHistogram: "DEVTOOLS_STORAGE_OPENED_PER_USER_FLAG",
+ timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
+ },
tilt: {
histogram: "DEVTOOLS_TILT_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_TILT_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS"
},
paintflashing: {
histogram: "DEVTOOLS_PAINTFLASHING_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_PAINTFLASHING_OPENED_PER_USER_FLAG",
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -55,16 +55,17 @@ skip-if = e10s # Bug 1086492 - Disable t
[browser_telemetry_toolbox.js]
[browser_telemetry_toolboxtabs_canvasdebugger.js]
[browser_telemetry_toolboxtabs_inspector.js]
[browser_telemetry_toolboxtabs_jsdebugger.js]
[browser_telemetry_toolboxtabs_jsprofiler.js]
[browser_telemetry_toolboxtabs_netmonitor.js]
[browser_telemetry_toolboxtabs_options.js]
[browser_telemetry_toolboxtabs_shadereditor.js]
+[browser_telemetry_toolboxtabs_storage.js]
[browser_telemetry_toolboxtabs_styleeditor.js]
[browser_telemetry_toolboxtabs_webaudioeditor.js]
[browser_telemetry_toolboxtabs_webconsole.js]
[browser_templater_basic.js]
[browser_toolbar_basic.js]
[browser_toolbar_tooltip.js]
[browser_toolbar_webconsole_errors_count.js]
skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_storage.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+let STORAGE_PREF = "devtools.storage.enabled";
+Services.prefs.setBoolPref(STORAGE_PREF, true);
+
+function init() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (!this.telemetryInfo) {
+ // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
+ return;
+ }
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+
+ openToolboxTabTwice("storage", false);
+}
+
+function openToolboxTabTwice(id, secondPass) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, id).then(function(toolbox) {
+ info("Toolbox tab " + id + " opened");
+
+ toolbox.once("destroyed", function() {
+ if (secondPass) {
+ checkResults();
+ } else {
+ openToolboxTabTwice(id, true);
+ }
+ });
+ // We use a timeout to check the tools active time
+ setTimeout(function() {
+ gDevTools.closeToolbox(target);
+ }, TOOL_DELAY);
+ }).then(null, reportError);
+}
+
+function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+
+ finishUp();
+}
+
+function reportError(error) {
+ let stack = " " + error.stack.replace(/\n?.*?@/g, "\n JS frame :: ");
+
+ ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
+ error.lineNumber + "\n\nStack trace:" + stack);
+ finishUp();
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+
+ Services.prefs.clearUserPref(STORAGE_PREF);
+
+ TargetFactory = Services = promise = require = null;
+
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ waitForFocus(init, content);
+ }, true);
+
+ content.location = TEST_URI;
+}
--- a/browser/devtools/storage/panel.js
+++ b/browser/devtools/storage/panel.js
@@ -54,17 +54,17 @@ StoragePanel.prototype = {
this.UI = new StorageUI(this._front, this._target, this._panelWin);
this.isReady = true;
this.emit("ready");
return this;
}, console.error);
},
/**
- * Destroy the style editor.
+ * Destroy the storage inspector.
*/
destroy: function() {
if (!this._destroyed) {
this.UI.destroy();
// Destroy front to ensure packet handler is removed from client
this._front.destroy();
this._destroyed = true;
--- a/browser/devtools/storage/ui.js
+++ b/browser/devtools/storage/ui.js
@@ -16,16 +16,18 @@ XPCOMUtils.defineLazyGetter(this, "Table
() => require("devtools/shared/widgets/TableWidget").TableWidget);
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "ViewHelpers",
"resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
"resource:///modules/devtools/VariablesView.jsm");
+let Telemetry = require("devtools/shared/telemetry");
+
/**
* Localization convenience methods.
*/
let L10N = new ViewHelpers.L10N(STORAGE_STRINGS);
const GENERIC_VARIABLES_VIEW_SETTINGS = {
lazyEmpty: true,
lazyEmptyDelay: 10, // ms
@@ -80,28 +82,32 @@ this.StorageUI = function StorageUI(fron
this.front.listStores().then(storageTypes => {
this.populateStorageTree(storageTypes);
});
this.onUpdate = this.onUpdate.bind(this);
this.front.on("stores-update", this.onUpdate);
this.handleKeypress = this.handleKeypress.bind(this);
this._panelDoc.addEventListener("keypress", this.handleKeypress);
+
+ this._telemetry = new Telemetry();
+ this._telemetry.toolOpened("storage");
}
exports.StorageUI = StorageUI;
StorageUI.prototype = {
storageTypes: null,
shouldResetColumns: true,
destroy: function() {
this.front.off("stores-update", this.onUpdate);
this._panelDoc.removeEventListener("keypress", this.handleKeypress);
+ this._telemetry.toolClosed("storage");
},
/**
* Empties and hides the object viewer sidebar
*/
hideSidebar: function() {
this.view.empty();
this.sidebar.hidden = true;
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -280,17 +280,24 @@ let UI = {
this._busyPromise = promise;
this._busyOperationDescription = operationDescription;
this.setupBusyTimeout();
this.busy();
promise.then(() => {
this.cancelBusyTimeout();
this.unbusy();
}, (e) => {
- let message = operationDescription + (e ? (": " + e) : "");
+ let message;
+ if (e.error && e.message) {
+ // Some errors come from fronts that are not based on protocol.js.
+ // Errors are not translated to strings.
+ message = operationDescription + " (" + e.error + "): " + e.message;
+ } else {
+ message = operationDescription + (e ? (": " + e) : "");
+ }
this.cancelBusyTimeout();
let operationCanceled = e && e.canceled;
if (!operationCanceled) {
UI.reportError("error_operationFail", message);
console.error(e);
}
this.unbusy();
});
@@ -812,17 +819,18 @@ let UI = {
playCmd.removeAttribute("disabled");
} else if (AppManager.selectedProject.type == "tab") {
playCmd.removeAttribute("disabled");
stopCmd.setAttribute("disabled", "true");
} else if (AppManager.selectedProject.type == "mainProcess") {
playCmd.setAttribute("disabled", "true");
stopCmd.setAttribute("disabled", "true");
} else {
- if (AppManager.selectedProject.errorsCount == 0) {
+ if (AppManager.selectedProject.errorsCount == 0 &&
+ AppManager.runtimeCanHandleApps()) {
playCmd.removeAttribute("disabled");
} else {
playCmd.setAttribute("disabled", "true");
}
}
}
// Remove command
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -101,28 +101,38 @@ let AppManager = exports.AppManager = {
if (this._appsFront) {
this._appsFront.off("install-progress", this.onInstallProgress);
this._appsFront.unwatchApps();
this._appsFront = null;
}
this._listTabsResponse = null;
} else {
this.connection.client.listTabs((response) => {
- let front = new AppActorFront(this.connection.client,
- response);
- front.on("install-progress", this.onInstallProgress);
- front.watchApps(() => this.checkIfProjectIsRunning())
- .then(() => front.fetchIcons())
- .then(() => {
- this._appsFront = front;
- this.checkIfProjectIsRunning();
- this.update("runtime-apps-found");
- });
- this._listTabsResponse = response;
- this.update("list-tabs-response");
+ if (response.webappsActor) {
+ let front = new AppActorFront(this.connection.client,
+ response);
+ front.on("install-progress", this.onInstallProgress);
+ front.watchApps(() => this.checkIfProjectIsRunning())
+ .then(() => {
+ // This can't be done earlier as many operations
+ // in the apps actor require watchApps to be called
+ // first.
+ this._appsFront = front;
+ this._listTabsResponse = response;
+ this.update("list-tabs-response");
+ return front.fetchIcons();
+ })
+ .then(() => {
+ this.checkIfProjectIsRunning();
+ this.update("runtime-apps-found");
+ });
+ } else {
+ this._listTabsResponse = response;
+ this.update("list-tabs-response");
+ }
});
}
this.update("connection");
},
get apps() {
if (this._appsFront) {
@@ -411,29 +421,38 @@ let AppManager = exports.AppManager = {
let app = this._getProjectFront(this.selectedProject);
if (!app.running) {
return app.launch();
} else {
return app.reload();
}
},
+ runtimeCanHandleApps: function() {
+ return !!this._appsFront;
+ },
+
installAndRunProject: function() {
let project = this.selectedProject;
if (!project || (project.type != "packaged" && project.type != "hosted")) {
console.error("Can't install project. Unknown type of project.");
return promise.reject("Can't install");
}
if (!this._listTabsResponse) {
this.reportError("error_cantInstallNotFullyConnected");
return promise.reject("Can't install");
}
+ if (!this._appsFront) {
+ console.error("Runtime doesn't have a webappsActor");
+ return promise.reject("Can't install");
+ }
+
return Task.spawn(function* () {
let self = AppManager;
yield self.validateProject(project);
if (project.errorsCount > 0) {
self.reportError("error_cantInstallValidationErrors");
return;
--- a/browser/devtools/webide/test/test_autoconnect_runtime.html
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -24,17 +24,17 @@
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");
+ is(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
},
get id() {
return "fakeRuntime";
},
--- a/browser/devtools/webide/test/test_runtime.html
+++ b/browser/devtools/webide/test/test_runtime.html
@@ -46,17 +46,17 @@
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
win = yield openWebIDE();
win.AppManager.runtimeList.usb.push({
connect: function(connection) {
- ok(connection, win.AppManager.connection, "connection is valid");
+ is(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
},
get name() {
return "fakeRuntime";
}
@@ -79,16 +79,18 @@
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 waitForUpdate(win, "list-tabs-response");
+
ok(isPlayActive(), "play button is enabled 1");
ok(!isStopActive(), "stop button is disabled 1");
let oldProject = win.AppManager.selectedProject;
win.AppManager.selectedProject = null;
yield nextTick();
ok(!isPlayActive(), "play button is disabled 2");
--- a/browser/extensions/pdfjs/test/browser.ini
+++ b/browser/extensions/pdfjs/test/browser.ini
@@ -4,8 +4,10 @@ support-files = file_pdfjs_test.pdf
[browser_pdfjs_main.js]
skip-if = debug # bug 1058695
[browser_pdfjs_navigation.js]
skip-if = debug # bug 1058695
[browser_pdfjs_savedialog.js]
[browser_pdfjs_views.js]
skip-if = debug # bug 1058695
+[browser_pdfjs_zoom.js]
+skip-if = debug # bug 1058695
new file mode 100644
--- /dev/null
+++ b/browser/extensions/pdfjs/test/browser_pdfjs_zoom.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const RELATIVE_DIR = "browser/extensions/pdfjs/test/";
+const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR;
+
+const TESTS = [
+ {
+ action: {
+ selector: "button#zoomIn",
+ event: "click"
+ },
+ expectedZoom: 1, // 1 - zoom in
+ message: "Zoomed in using the '+' (zoom in) button"
+ },
+
+ {
+ action: {
+ selector: "button#zoomOut",
+ event: "click"
+ },
+ expectedZoom: -1, // -1 - zoom out
+ message: "Zoomed out using the '-' (zoom out) button"
+ },
+
+ {
+ action: {
+ keyboard: true,
+ event: "+"
+ },
+ expectedZoom: 1, // 1 - zoom in
+ message: "Zoomed in using the CTRL++ keys"
+ },
+
+ {
+ action: {
+ keyboard: true,
+ event: "-"
+ },
+ expectedZoom: -1, // -1 - zoom out
+ message: "Zoomed out using the CTRL+- keys"
+ },
+
+ {
+ action: {
+ selector: "select#scaleSelect",
+ index: 5,
+ event: "change"
+ },
+ expectedZoom: -1, // -1 - zoom out
+ message: "Zoomed using the zoom picker"
+ }
+];
+
+let initialWidth; // the initial width of the PDF document
+var previousWidth; // the width of the PDF document at previous step/test
+
+function test() {
+ var tab;
+ let handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ let handlerInfo = mimeService.getFromTypeAndExtension('application/pdf', 'pdf');
+
+ // Make sure pdf.js is the default handler.
+ is(handlerInfo.alwaysAskBeforeHandling, false,
+ 'pdf handler defaults to always-ask is false');
+ is(handlerInfo.preferredAction, Ci.nsIHandlerInfo.handleInternally,
+ 'pdf handler defaults to internal');
+
+ info('Pref action: ' + handlerInfo.preferredAction);
+
+ waitForExplicitFinish();
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(tab);
+ });
+
+ tab = gBrowser.selectedTab = gBrowser.addTab(TESTROOT + "file_pdfjs_test.pdf");
+ var newTabBrowser = gBrowser.getBrowserForTab(tab);
+
+ newTabBrowser.addEventListener("load", function eventHandler() {
+ newTabBrowser.removeEventListener("load", eventHandler, true);
+
+ var document = newTabBrowser.contentDocument,
+ window = newTabBrowser.contentWindow;
+
+ // Runs tests after all 'load' event handlers have fired off
+ window.addEventListener("documentload", function() {
+ initialWidth = parseInt(document.querySelector("div#pageContainer1").style.width);
+ previousWidth = initialWidth;
+ runTests(document, window, finish);
+ }, false, true);
+ }, true);
+}
+
+function runTests(document, window, callback) {
+ // check that PDF is opened with internal viewer
+ ok(document.querySelector('div#viewer'), "document content has viewer UI");
+ ok('PDFJS' in window.wrappedJSObject, "window content has PDFJS object");
+
+ // Start the zooming tests after the document is loaded
+ waitForDocumentLoad(document).then(function () {
+ zoomPDF(document, window, TESTS.shift(), finish);
+ });
+}
+
+function waitForDocumentLoad(document) {
+ var deferred = Promise.defer();
+ var interval = setInterval(function () {
+ if (document.querySelector("div#pageContainer1") != null){
+ clearInterval(interval);
+ deferred.resolve();
+ }
+ }, 500);
+
+ return deferred.promise;
+}
+
+function zoomPDF(document, window, test, endCallback) {
+ var renderedPage;
+
+ document.addEventListener("pagerendered", function onPageRendered(e) {
+ if(e.detail.pageNumber !== 1) {
+ return;
+ }
+
+ document.removeEventListener("pagerendered", onPageRendered, true);
+
+ var pageZoomScale = document.querySelector('select#scaleSelect');
+
+ // The zoom value displayed in the zoom select
+ var zoomValue = pageZoomScale.options[pageZoomScale.selectedIndex].innerHTML;
+
+ let pageContainer = document.querySelector('div#pageContainer1');
+ let actualWidth = parseInt(pageContainer.style.width);
+
+ // the actual zoom of the PDF document
+ let computedZoomValue = parseInt(((actualWidth/initialWidth).toFixed(2))*100) + "%";
+ is(computedZoomValue, zoomValue, "Content has correct zoom");
+
+ // Check that document zooms in the expected way (in/out)
+ let zoom = (actualWidth - previousWidth) * test.expectedZoom;
+ ok(zoom > 0, test.message);
+
+ // Go to next test (if there is any) or finish
+ var nextTest = TESTS.shift();
+ if (nextTest) {
+ previousWidth = actualWidth;
+ zoomPDF(document, window, nextTest, endCallback);
+ }
+ else
+ endCallback();
+ }, true);
+
+ // We zoom using an UI element
+ if (test.action.selector) {
+ // Get the element and trigger the action for changing the zoom
+ var el = document.querySelector(test.action.selector);
+ ok(el, "Element '" + test.action.selector + "' has been found");
+
+ if (test.action.index){
+ el.selectedIndex = test.action.index;
+ }
+
+ // Dispatch the event for changing the zoom
+ el.dispatchEvent(new Event(test.action.event));
+ }
+ // We zoom using keyboard
+ else {
+ // Simulate key press
+ EventUtils.synthesizeKey(test.action.event, { ctrlKey: true });
+ }
+}
--- a/configure.in
+++ b/configure.in
@@ -8984,17 +8984,17 @@ if test "$ACCESSIBILITY" -a "$MOZ_ENABLE
ATK_MAJOR_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $1 }'`
ATK_MINOR_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $2 }'`
ATK_REV_VERSION=`echo ${ATK_FULL_VERSION} | $AWK -F\. '{ print $3 }'`
AC_DEFINE_UNQUOTED(ATK_MAJOR_VERSION, $ATK_MAJOR_VERSION)
AC_DEFINE_UNQUOTED(ATK_MINOR_VERSION, $ATK_MINOR_VERSION)
AC_DEFINE_UNQUOTED(ATK_REV_VERSION, $ATK_REV_VERSION)
fi
-if test -z "$RELEASE_BUILD" -a -z "$NIGHTLY_BUILD"; then
+if test -n "$MOZ_DEV_EDITION"; then
AC_DEFINE(MOZ_DEV_EDITION)
fi
if test "$MOZ_DEBUG"; then
A11Y_LOG=1
fi
case "$MOZ_UPDATE_CHANNEL" in
aurora|beta|release|esr)
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -355,31 +355,37 @@ Promise::MaybeReject(JSContext* aCx,
MaybeRejectInternal(aCx, aValue);
}
void
Promise::MaybeReject(const nsRefPtr<MediaStreamError>& aArg) {
MaybeSomething(aArg, &Promise::MaybeReject);
}
-void
+bool
Promise::PerformMicroTaskCheckpoint()
{
CycleCollectedJSRuntime* runtime = CycleCollectedJSRuntime::Get();
nsTArray<nsRefPtr<nsIRunnable>>& microtaskQueue =
runtime->GetPromiseMicroTaskQueue();
- while (!microtaskQueue.IsEmpty()) {
+ if (microtaskQueue.IsEmpty()) {
+ return false;
+ }
+
+ do {
nsRefPtr<nsIRunnable> runnable = microtaskQueue.ElementAt(0);
MOZ_ASSERT(runnable);
// This function can re-enter, so we remove the element before calling.
microtaskQueue.RemoveElementAt(0);
runnable->Run();
- }
+ } while (!microtaskQueue.IsEmpty());
+
+ return true;
}
/* static */ bool
Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
{
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
JS::Rooted<JS::Value> v(aCx,
--- a/dom/promise/Promise.h
+++ b/dom/promise/Promise.h
@@ -116,17 +116,18 @@ public:
// require use to include DOMError.h either here or in all those translation
// units.
template<typename T>
void MaybeRejectBrokenly(const T& aArg); // Not implemented by default; see
// specializations in the .cpp for
// the T values we support.
// Called by DOM to let us execute our callbacks. May be called recursively.
- static void PerformMicroTaskCheckpoint();
+ // Returns true if at least one microtask was processed.
+ static bool PerformMicroTaskCheckpoint();
// WebIDL
nsIGlobalObject* GetParentObject() const
{
return mGlobal;
}
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -4238,17 +4238,17 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
// Start the periodic GC timer if it is not already running.
SetGCTimerMode(PeriodicTimer);
// Process a single runnable from the main queue.
MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(mThread, false));
// Only perform the Promise microtask checkpoint on the outermost event
// loop. Don't run it, for example, during sync XHR or importScripts.
- Promise::PerformMicroTaskCheckpoint();
+ (void)Promise::PerformMicroTaskCheckpoint();
if (NS_HasPendingEvents(mThread)) {
// Now *might* be a good time to GC. Let the JS engine make the decision.
if (workerCompartment) {
JS_MaybeGC(aCx);
}
}
else {
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -1003,33 +1003,50 @@ nsXPConnect::JSToVariant(JSContext* ctx,
nsRefPtr<XPCVariant> variant = XPCVariant::newVariant(ctx, value);
variant.forget(_retval);
if (!(*_retval))
return NS_ERROR_FAILURE;
return NS_OK;
}
+namespace {
+
+class DummyRunnable : public nsRunnable {
+public:
+ NS_IMETHOD Run() { return NS_OK; }
+};
+
+} // anonymous namespace
+
NS_IMETHODIMP
nsXPConnect::OnProcessNextEvent(nsIThreadInternal *aThread, bool aMayWait,
uint32_t aRecursionDepth)
{
+ MOZ_ASSERT(NS_IsMainThread());
+
// If ProcessNextEvent was called during a Promise "then" callback, we
// must process any pending microtasks before blocking in the event loop,
// otherwise we may deadlock until an event enters the queue later.
if (aMayWait) {
- Promise::PerformMicroTaskCheckpoint();
+ if (Promise::PerformMicroTaskCheckpoint()) {
+ // If any microtask was processed, we post a dummy event in order to
+ // force the ProcessNextEvent call not to block. This is required
+ // to support nested event loops implemented using a pattern like
+ // "while (condition) thread.processNextEvent(true)", in case the
+ // condition is triggered here by a Promise "then" callback.
+ NS_DispatchToMainThread(new DummyRunnable());
+ }
}
// Record this event.
mEventDepth++;
// Push a null JSContext so that we don't see any script during
// event processing.
- MOZ_ASSERT(NS_IsMainThread());
bool ok = PushJSContextNoScriptContext(nullptr);
NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE);
return NS_OK;
}
NS_IMETHODIMP
nsXPConnect::AfterProcessNextEvent(nsIThreadInternal *aThread,
uint32_t aRecursionDepth,
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -898,19 +898,16 @@ OnSharedPreferenceChangeListener
Intent intent = new Intent(ACTION_STUMBLER_UPLOAD_PREF)
.putExtra("pref", PREFS_GEO_REPORTING)
.putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME)
.putExtra("enabled", value)
.putExtra("moz_mozilla_api_key", AppConstants.MOZ_STUMBLER_API_KEY);
if (GeckoAppShell.getGeckoInterface() != null) {
intent.putExtra("user_agent", GeckoAppShell.getGeckoInterface().getDefaultUAString());
}
- if (!AppConstants.MOZILLA_OFFICIAL) {
- intent.putExtra("is_debug", true);
- }
broadcastAction(context, intent);
}
/**
* Broadcast the current value of the
* <code>PREFS_GEO_REPORTING</code> pref.
*/
public static void broadcastStumblerPref(final Context context) {
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -141,8 +141,9 @@ skip-if = android_version == "10"
[testNativeCrypto]
[testSessionHistory]
# testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074
[testSelectionHandler]
skip-if = android_version == "10"
[testStumblerSetting]
+skip-if = android_version == "10"
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
@@ -2,17 +2,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.mozstumbler.service;
import java.util.concurrent.ConcurrentLinkedQueue;
public class AppGlobals {
- public static final String LOG_PREFIX = "Stumbler:";
+ public static final String LOG_PREFIX = "Stumbler_";
/* All intent actions start with this string. Only locally broadcasted. */
public static final String ACTION_NAMESPACE = "org.mozilla.mozstumbler.intent.action";
/* Handle this for logging reporter info. */
public static final String ACTION_GUI_LOG_MESSAGE = AppGlobals.ACTION_NAMESPACE + ".LOG_MESSAGE";
public static final String ACTION_GUI_LOG_MESSAGE_EXTRA = ACTION_GUI_LOG_MESSAGE + ".MESSAGE";
@@ -52,12 +52,20 @@ public class AppGlobals {
if (guiLogMessageBuffer != null) {
if (isBold) {
msg = "<b>" + msg + "</b>";
}
guiLogMessageBuffer.add("<font color='" + color +"'>" + msg + "</font>");
}
}
+ public static String makeLogTag(String name) {
+ final int maxLen = 23 - LOG_PREFIX.length();
+ if (name.length() > maxLen) {
+ name = name.substring(name.length() - maxLen, name.length());
+ }
+ return LOG_PREFIX + name;
+ }
+
public static final String ACTION_TEST_SETTING_ENABLED = "stumbler-test-setting-enabled";
public static final String ACTION_TEST_SETTING_DISABLED = "stumbler-test-setting-disabled";
}
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
@@ -9,17 +9,17 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Build.VERSION;
import android.text.TextUtils;
import android.util.Log;
public final class Prefs {
- private static final String LOG_TAG = Prefs.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(Prefs.class.getSimpleName());
private static final String NICKNAME_PREF = "nickname";
private static final String USER_AGENT_PREF = "user-agent";
private static final String VALUES_VERSION_PREF = "values_version";
private static final String WIFI_ONLY = "wifi_only";
private static final String LAT_PREF = "lat_pref";
private static final String LON_PREF = "lon_pref";
private static final String GEOFENCE_HERE = "geofence_here";
private static final String GEOFENCE_SWITCH = "geofence_switch";
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/PassiveServiceReceiver.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/PassiveServiceReceiver.java
@@ -24,37 +24,41 @@ import org.mozilla.mozstumbler.service.s
* The StumblerService is where the enabled state is checked, and if not enabled, the
* service stops immediately.
*
* 3) Upload notification: onReceive intents are used to tell the StumblerService to check for upload.
* In the Fennec host app use, startup and pause are used as indicators to the StumblerService that now
* is a good time to try upload, as it is likely that the network is in use.
*/
public class PassiveServiceReceiver extends BroadcastReceiver {
- static final String LOG_TAG = AppGlobals.LOG_PREFIX + PassiveServiceReceiver.class.getSimpleName();
+ // This allows global debugging logs to be enabled by doing
+ // |adb shell setprop log.tag.PassiveStumbler DEBUG|
+ static final String LOG_TAG = "PassiveStumbler";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
+ // This value is cached, so if |setprop| is performed (as described on the LOG_TAG above),
+ // then the start/stop intent must be resent by toggling the setting or stopping/starting Fennec.
+ // This does not guard against dumping PII (PII in stumbler is location, wifi BSSID, cell tower details).
+ AppGlobals.isDebug = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
final String action = intent.getAction();
final boolean isIntentFromHostApp = (action != null) && action.contains(".STUMBLER_PREF");
if (!isIntentFromHostApp) {
Log.d(LOG_TAG, "Stumbler: received intent external to host app");
Intent startServiceIntent = new Intent(context, StumblerService.class);
startServiceIntent.putExtra(StumblerService.ACTION_NOT_FROM_HOST_APP, true);
context.startService(startServiceIntent);
return;
}
- if (intent.hasExtra("is_debug")) {
- AppGlobals.isDebug = intent.getBooleanExtra("is_debug", false);
- }
StumblerService.sFirefoxStumblingEnabled.set(intent.getBooleanExtra("enabled", false));
if (!StumblerService.sFirefoxStumblingEnabled.get()) {
// This calls the service's onDestroy(), and the service's onHandleIntent(...) is not called
context.stopService(new Intent(context, StumblerService.class));
// For testing service messages were received
context.sendBroadcast(new Intent(AppGlobals.ACTION_TEST_SETTING_DISABLED));
return;
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
@@ -25,17 +25,17 @@ import org.mozilla.mozstumbler.service.s
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.StumblerBundle;
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellInfo;
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
import org.mozilla.mozstumbler.service.stumblerthread.scanners.GPSScanner;
import org.mozilla.mozstumbler.service.stumblerthread.scanners.WifiScanner;
public final class Reporter extends BroadcastReceiver {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + Reporter.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(Reporter.class.getSimpleName());
public static final String ACTION_FLUSH_TO_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".FLUSH";
private boolean mIsStarted;
/* The maximum number of Wi-Fi access points in a single observation. */
private static final int MAX_WIFIS_PER_LOCATION = 200;
/* The maximum number of cells in a single observation */
private static final int MAX_CELLS_PER_LOCATION = 50;
@@ -190,17 +190,18 @@ public final class Reporter extends Broa
cellCount = mlsObj.getInt(DataStorageContract.ReportsColumns.CELL_COUNT);
} catch (JSONException e) {
Log.w(LOG_TAG, "Failed to convert bundle to JSON: " + e);
return;
}
if (AppGlobals.isDebug) {
- Log.d(LOG_TAG, "Received bundle: " + mlsObj.toString());
+ // PII: do not log the bundle without obfuscating it
+ Log.d(LOG_TAG, "Received bundle");
}
if (wifiCount + cellCount < 1) {
return;
}
try {
DataStorageManager.getInstance().insert(mlsObj.toString(), wifiCount, cellCount);
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
@@ -22,17 +22,17 @@ import org.mozilla.mozstumbler.service.u
import org.mozilla.mozstumbler.service.utils.NetworkUtils;
import org.mozilla.mozstumbler.service.utils.PersistentIntentService;
// In stand-alone service mode (a.k.a passive scanning mode), this is created from PassiveServiceReceiver (by calling startService).
// The StumblerService is a sticky unbound service in this usage.
//
public class StumblerService extends PersistentIntentService
implements DataStorageManager.StorageIsEmptyTracker {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + StumblerService.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(StumblerService.class.getSimpleName());
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE;
public static final String ACTION_START_PASSIVE = ACTION_BASE + ".START_PASSIVE";
public static final String ACTION_EXTRA_MOZ_API_KEY = ACTION_BASE + ".MOZKEY";
public static final String ACTION_EXTRA_USER_AGENT = ACTION_BASE + ".USER_AGENT";
public static final String ACTION_NOT_FROM_HOST_APP = ACTION_BASE + ".NOT_FROM_HOST";
public static final AtomicBoolean sFirefoxStumblingEnabled = new AtomicBoolean();
protected final ScanManager mScanManager = new ScanManager();
protected final Reporter mReporter = new Reporter();
@@ -139,16 +139,18 @@ public class StumblerService extends Per
setIntentRedelivery(true);
}
// Called from the main thread
@Override
public void onDestroy() {
super.onDestroy();
+ UploadAlarmReceiver.cancelAlarm(this, !mScanManager.isPassiveMode());
+
if (!mScanManager.isScanning()) {
return;
}
// Used to move these disk I/O ops off the calling thread. The current operations here are synchronized,
// however instead of creating another thread (if onDestroy grew to have concurrency complications)
// we could be messaging the stumbler thread to perform a shutdown function.
new AsyncTask<Void, Void, Void>() {
@@ -193,17 +195,21 @@ public class StumblerService extends Per
final boolean isScanEnabledInPrefs = Prefs.getInstance().getFirefoxScanEnabled();
if (!isScanEnabledInPrefs && intent.getBooleanExtra(ACTION_NOT_FROM_HOST_APP, false)) {
stopSelf();
return;
}
- if (!DataStorageManager.getInstance().isDirEmpty()) {
+ boolean hasFilesWaiting = !DataStorageManager.getInstance().isDirEmpty();
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Files waiting:" + hasFilesWaiting);
+ }
+ if (hasFilesWaiting) {
// non-empty on startup, schedule an upload
// This is the only upload trigger in Firefox mode
// Firefox triggers this ~4 seconds after startup (after Gecko is loaded), add a small delay to avoid
// clustering with other operations that are triggered at this time.
final long lastAttemptedTime = Prefs.getInstance().getLastAttemptedUploadTime();
final long timeNow = System.currentTimeMillis();
if (timeNow - lastAttemptedTime < PASSIVE_UPLOAD_FREQ_GUARD_MSEC) {
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
@@ -6,17 +6,17 @@ package org.mozilla.mozstumbler.service.
import android.net.wifi.ScanResult;
import android.util.Log;
import org.mozilla.mozstumbler.service.AppGlobals;
import java.util.Locale;
import java.util.regex.Pattern;
public final class BSSIDBlockList {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + BSSIDBlockList.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(BSSIDBlockList.class.getSimpleName());
private static final String NULL_BSSID = "000000000000";
private static final String WILDCARD_BSSID = "ffffffffffff";
private static final Pattern BSSID_PATTERN = Pattern.compile("([0-9a-f]{12})");
private static String[] sOuiList = new String[]{};
private BSSIDBlockList() {
}
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
@@ -33,17 +33,17 @@ import java.util.TimerTask;
*
* If the network is reasonably active, and reporting is slow enough, there is no disk I/O, it all happens
* in-memory.
*
* Also of note: the in-memory buffers (both mCurrentReports and mCurrentReportsSendBuffer) are saved
* when the service is destroyed.
*/
public class DataStorageManager {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + DataStorageManager.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(DataStorageManager.class.getSimpleName());
// The max number of reports stored in the mCurrentReports. Each report is a GPS location plus wifi and cell scan.
// After this size is reached, data is persisted to disk, mCurrentReports is cleared.
private static final int MAX_REPORTS_IN_MEMORY = 50;
// Used to cap the amount of data stored. When this limit is hit, no more data is saved to disk
// until the data is uploaded, or and data exceeds DEFAULT_MAX_WEEKS_DATA_ON_DISK.
private static final long DEFAULT_MAX_BYTES_STORED_ON_DISK = 1024 * 250; // 250 KiB max by default
@@ -196,29 +196,17 @@ public class DataStorageManager {
public final ReportFileList fileList;
}
public interface StorageIsEmptyTracker {
public void notifyStorageStateEmpty(boolean isEmpty);
}
private String getStorageDir(Context c) {
- File dir = null;
- if (AppGlobals.isDebug) {
- // in debug, put files in public location
- dir = c.getExternalFilesDir(null);
- if (dir != null) {
- dir = new File(dir.getAbsolutePath() + "/mozstumbler");
- }
- }
-
- if (dir == null) {
- dir = c.getFilesDir();
- }
-
+ File dir = c.getFilesDir();
if (!dir.exists()) {
boolean ok = dir.mkdirs();
if (!ok) {
Log.d(LOG_TAG, "getStorageDir: error in mkdirs()");
}
}
return dir.getPath();
@@ -409,19 +397,16 @@ public class DataStorageManager {
if (reports != null) {
for(String s: reports) {
sb.append(sep).append(s);
sep = separator;
}
}
final String result = sb.append(kSuffix).toString();
- if (AppGlobals.isDebug) {
- Log.d(LOG_TAG, result);
- }
return result;
}
public synchronized void saveCurrentReportsToDisk() throws IOException {
saveCurrentReportsSendBufferToDisk();
if (mCurrentReports.reports.size() < 1) {
return;
}
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
@@ -29,17 +29,17 @@ public class GPSScanner implements Locat
public static final String ACTION_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
public static final String SUBJECT_NEW_STATUS = "new_status";
public static final String SUBJECT_LOCATION_LOST = "location_lost";
public static final String SUBJECT_NEW_LOCATION = "new_location";
public static final String NEW_STATUS_ARG_FIXES = "fixes";
public static final String NEW_STATUS_ARG_SATS = "sats";
public static final String NEW_LOCATION_ARG_LOCATION = "location";
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + GPSScanner.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(GPSScanner.class.getSimpleName());
private static final int MIN_SAT_USED_IN_FIX = 3;
private static final long ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS = 1000;
private static final float ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M = 10;
private static final long PASSIVE_GPS_MIN_UPDATE_FREQ_MS = 3000;
private static final float PASSIVE_GPS_MOVEMENT_MIN_DELTA_M = 30;
private final LocationBlockList mBlockList = new LocationBlockList();
private final Context mContext;
@@ -186,25 +186,20 @@ public class GPSScanner implements Locat
Date date = new Date(location.getTime());
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String time = formatter.format(date);
logMsg += String.format("%s Coord: %.4f,%.4f, Acc: %.0f, Speed: %.0f, Alt: %.0f, Bearing: %.1f", time, location.getLatitude(),
location.getLongitude(), location.getAccuracy(), location.getSpeed(), location.getAltitude(), location.getBearing());
sendToLogActivity(logMsg);
if (mBlockList.contains(location)) {
- Log.w(LOG_TAG, "Blocked location: " + location);
reportLocationLost();
return;
}
- if (AppGlobals.isDebug) {
- Log.d(LOG_TAG, "New location: " + location);
- }
-
mLocation = location;
if (!mAutoGeofencing) {
reportNewLocationReceived(location);
}
mLocationCount++;
if (mIsPassiveMode) {
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
@@ -5,17 +5,17 @@
package org.mozilla.mozstumbler.service.stumblerthread.scanners;
import android.location.Location;
import android.util.Log;
import org.mozilla.mozstumbler.service.AppGlobals;
import org.mozilla.mozstumbler.service.Prefs;
public final class LocationBlockList {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + LocationBlockList.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(LocationBlockList.class.getSimpleName());
private static final double MAX_ALTITUDE = 8848; // Mount Everest's altitude in meters
private static final double MIN_ALTITUDE = -418; // Dead Sea's altitude in meters
private static final float MAX_SPEED = 340.29f; // Mach 1 in meters/second
private static final float MIN_ACCURACY = 500; // meter radius
private static final long MIN_TIMESTAMP = 946684801; // 2000-01-01 00:00:01
private static final double GEOFENCE_RADIUS = 0.01; // .01 degrees is approximately 1km
private static final long MILLISECONDS_PER_DAY = 86400000;
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
@@ -18,17 +18,17 @@ import org.mozilla.mozstumbler.service.s
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class ScanManager {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + ScanManager.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(ScanManager.class.getSimpleName());
private Timer mPassiveModeFlushTimer;
private Context mContext;
private boolean mIsScanning;
private GPSScanner mGPSScanner;
private WifiScanner mWifiScanner;
private CellScanner mCellScanner;
private ActiveOrPassiveStumbling mStumblingMode = ActiveOrPassiveStumbling.ACTIVE_STUMBLING;
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
@@ -35,17 +35,17 @@ public class WifiScanner extends Broadca
public static final String ACTION_WIFIS_SCANNED = ACTION_BASE + "WIFIS_SCANNED";
public static final String ACTION_WIFIS_SCANNED_ARG_RESULTS = "scan_results";
public static final String ACTION_WIFIS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
public static final int STATUS_IDLE = 0;
public static final int STATUS_ACTIVE = 1;
public static final int STATUS_WIFI_DISABLED = -1;
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + WifiScanner.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(WifiScanner.class.getSimpleName());
private static final long WIFI_MIN_UPDATE_TIME = 5000; // milliseconds
private boolean mStarted;
private final Context mContext;
private WifiLock mWifiLock;
private Timer mWifiScanTimer;
private final Set<String> mAPs = Collections.synchronizedSet(new HashSet<String>());
private final AtomicInteger mVisibleAPs = new AtomicInteger();
@@ -189,21 +189,19 @@ public class WifiScanner extends Broadca
mWifiScanTimer.cancel();
mWifiScanTimer = null;
mVisibleAPs.set(0);
}
public static boolean shouldLog(ScanResult scanResult) {
if (BSSIDBlockList.contains(scanResult)) {
- Log.w(LOG_TAG, "Blocked BSSID: " + scanResult);
return false;
}
if (SSIDBlockList.contains(scanResult)) {
- Log.w(LOG_TAG, "Blocked SSID: " + scanResult);
return false;
}
return true;
}
private WifiManager getWifiManager() {
return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
}
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
@@ -14,17 +14,17 @@ import android.telephony.cdma.CdmaCellLo
import android.telephony.gsm.GsmCellLocation;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.mozstumbler.service.AppGlobals;
public class CellInfo implements Parcelable {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + CellInfo.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(CellInfo.class.getSimpleName());
public static final String RADIO_GSM = "gsm";
public static final String RADIO_CDMA = "cdma";
public static final String RADIO_WCDMA = "wcdma";
public static final String CELL_RADIO_GSM = "gsm";
public static final String CELL_RADIO_UMTS = "umts";
public static final String CELL_RADIO_CDMA = "cdma";
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
@@ -21,17 +21,17 @@ import org.mozilla.mozstumbler.service.A
public class CellScanner {
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".CellScanner.";
public static final String ACTION_CELLS_SCANNED = ACTION_BASE + "CELLS_SCANNED";
public static final String ACTION_CELLS_SCANNED_ARG_CELLS = "cells";
public static final String ACTION_CELLS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + CellScanner.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(CellScanner.class.getSimpleName());
private static final long CELL_MIN_UPDATE_TIME = 1000; // milliseconds
private final Context mContext;
private static CellScannerImpl sImpl;
private Timer mCellScanTimer;
private final Set<String> mCells = new HashSet<String>();
private int mCurrentCellInfoCount;
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerNoWCDMA.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerNoWCDMA.java
@@ -26,17 +26,17 @@ import org.mozilla.mozstumbler.service.A
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/* Fennec does not yet support the api level for WCDMA import */
public class CellScannerNoWCDMA implements CellScanner.CellScannerImpl {
- protected static String LOG_TAG = AppGlobals.LOG_PREFIX + CellScannerNoWCDMA.class.getSimpleName();
+ protected static String LOG_TAG = AppGlobals.makeLogTag(CellScannerNoWCDMA.class.getSimpleName());
protected GetAllCellInfoScannerImpl mGetAllInfoCellScanner;
protected TelephonyManager mTelephonyManager;
protected boolean mIsStarted;
protected int mPhoneType;
protected final Context mContext;
protected volatile int mSignalStrength;
protected volatile int mCdmaDbm;
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
@@ -19,17 +19,17 @@ import org.mozilla.mozstumbler.service.u
/* Only one at a time may be uploading. If executed while another upload is in progress
* it will return immediately, and SyncResult is null.
*
* Threading:
* Uploads on a separate thread. ONLY DataStorageManager is thread-safe, do not call
* preferences, do not call any code that isn't thread-safe. You will cause suffering.
* An exception is made for AppGlobals.isDebug, a false reading is of no consequence. */
public class AsyncUploader extends AsyncTask<Void, Void, SyncSummary> {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + AsyncUploader.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(AsyncUploader.class.getSimpleName());
private final UploadSettings mSettings;
private final Object mListenerLock = new Object();
private AsyncUploaderListener mListener;
private static final AtomicBoolean sIsUploading = new AtomicBoolean();
private String mNickname;
public interface AsyncUploaderListener {
public void onUploadComplete(SyncSummary result);
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
@@ -25,17 +25,17 @@ import org.mozilla.mozstumbler.service.u
// 2) Changing the pref in Fennec to stumble or not.
// 3) Boot intent (and SD card app available intent).
//
// Threading:
// - scheduled from the stumbler thread
// - triggered from the main thread
// - actual work is done the upload thread (AsyncUploader)
public class UploadAlarmReceiver extends BroadcastReceiver {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + UploadAlarmReceiver.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(UploadAlarmReceiver.class.getSimpleName());
private static final String EXTRA_IS_REPEATING = "is_repeating";
private static boolean sIsAlreadyScheduled;
public UploadAlarmReceiver() {}
public static class UploadAlarmService extends IntentService {
public UploadAlarmService(String name) {
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
@@ -15,17 +15,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public abstract class AbstractCommunicator {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + AbstractCommunicator.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(AbstractCommunicator.class.getSimpleName());
private static final String NICKNAME_HEADER = "X-Nickname";
private static final String USER_AGENT_HEADER = "User-Agent";
private HttpURLConnection mHttpURLConnection;
private final String mUserAgent;
private static int sBytesSentTotal = 0;
private static String sMozApiKey;
public abstract String getUrlString();
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
@@ -6,17 +6,17 @@ package org.mozilla.mozstumbler.service.
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import org.mozilla.mozstumbler.service.AppGlobals;
public final class NetworkUtils {
- private static final String LOG_TAG = AppGlobals.LOG_PREFIX + NetworkUtils.class.getSimpleName();
+ private static final String LOG_TAG = AppGlobals.makeLogTag(NetworkUtils.class.getSimpleName());
ConnectivityManager mConnectivityManager;
static NetworkUtils sInstance;
/* Created at startup by app, or service, using a context. */
static public void createGlobalInstance(Context context) {
sInstance = new NetworkUtils();
sInstance.mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5851,16 +5851,21 @@
"kind": "boolean",
"description": "How many times has the devtool's JS Profiler been opened?"
},
"DEVTOOLS_NETMONITOR_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's Network Monitor been opened?"
},
+ "DEVTOOLS_STORAGE_OPENED_BOOLEAN": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "How many times has the Storage Inspector been opened?"
+ },
"DEVTOOLS_PAINTFLASHING_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's Paint Flashing been opened via the toolbox button?"
},
"DEVTOOLS_TILT_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
@@ -5971,16 +5976,21 @@
"kind": "flag",
"description": "How many users have opened the devtool's JS Profiler?"
},
"DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Network Monitor?"
},
+ "DEVTOOLS_STORAGE_OPENED_PER_USER_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "How many users have opened the devtool's Storage Inspector?"
+ },
"DEVTOOLS_PAINTFLASHING_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Paint Flashing been opened via the toolbox button?"
},
"DEVTOOLS_TILT_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
@@ -6125,16 +6135,23 @@
},
"DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000000",
"n_buckets": 100,
"description": "How long has the network monitor been active (seconds)"
},
+ "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": "10000000",
+ "n_buckets": 100,
+ "description": "How long has the storage inspector been active (seconds)"
+ },
"DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000000",
"n_buckets": 100,
"description": "How long has paint flashing been active (seconds)"
},
"DEVTOOLS_TILT_TIME_ACTIVE_SECONDS": {