author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Fri, 14 Nov 2014 13:13:42 +0100 | |
changeset 215759 | 64206634959a2e84eefec40d1da0122c7a63bc20 |
parent 215740 | bbb68df450c2bc464c9e89686aea1be674b04b9e (current diff) |
parent 215758 | b1a9e41d3f4b1d2720a6f95b84a7f2f8aa9feb48 (diff) |
child 215760 | da57927b609dc71e13b14fd1d4701cb2d59f0dfd |
child 215830 | b85cb9cd8d3602139b4158c1a0eaa99674522c2f |
child 215843 | 91fa670781830e500dbd503405381c2d316ceddc |
push id | 51845 |
push user | cbook@mozilla.com |
push date | Fri, 14 Nov 2014 12:23:21 +0000 |
treeherder | mozilla-inbound@da57927b609d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 36.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/CLOBBER +++ b/CLOBBER @@ -17,9 +17,9 @@ # # Modifying this file will now automatically clobber the buildbot machines \o/ # # Are you updating CLOBBER because you think it's needed for your WebIDL # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Bug 1095234 - Bug 1091260 stopped packaging a devtools file with EXTRA_JS_MODULES while making it require pre-processing. +Bug 1084498 - Android build tools dependency.
--- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -692,16 +692,48 @@ html, .fx-embedded, #main, * Rooms */ .room-conversation-wrapper { position: relative; height: 100%; } +.standalone .room-conversation-wrapper { + height: calc(100% - 50px - 60px); + background: #000; +} + +.room-conversation-wrapper header { + background: #000; + height: 50px; + text-align: left; +} + +.room-conversation-wrapper header h1 { + font-size: 1.5em; + color: #fff; + line-height: 50px; + text-indent: 50px; + background-image: url("../img/firefox-logo.png"); + background-size: 30px; + background-position: 10px; + background-repeat: no-repeat; +} + +.room-conversation-wrapper footer { + background: #000; + height: 60px; + margin-top: -12px; +} + +.room-conversation-wrapper footer a { + color: #555; +} + /** * Hides the hangup button for room conversations. */ .room-conversation .conversation-toolbar .btn-hangup-entry { display: none; } .room-invitation-overlay { @@ -740,17 +772,17 @@ html, .fx-embedded, #main, /* Standalone rooms */ .standalone .room-conversation-wrapper { position: relative; } .standalone .room-inner-info-area { position: absolute; - top: 35%; + top: 50%; left: 0; right: 25%; z-index: 1000; margin: 0 auto; width: 50%; color: #fff; font-weight: bold; font-size: 1.1em; @@ -762,16 +794,17 @@ html, .fx-embedded, #main, padding: .2em 1.2em; cursor: pointer; } .standalone .room-inner-info-area a.btn { padding: .5em 3em .3em 3em; border-radius: 3px; font-weight: normal; + max-width: 400px; } .standalone .room-conversation h2.room-name { position: absolute; display: inline-block; top: 0; right: 0; color: #fff; @@ -791,14 +824,14 @@ html, .fx-embedded, #main, .standalone .room-conversation .local-stream { width: 33%; height: 26.5%; } .standalone .room-conversation .conversation-toolbar { background: #000; - border-top: none; + border: none; } .standalone .room-conversation .conversation-toolbar .btn-hangup-entry { display: block; }
--- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -5,16 +5,25 @@ /* global loop:true */ var loop = loop || {}; loop.store = loop.store || {}; loop.store.ActiveRoomStore = (function() { "use strict"; var sharedActions = loop.shared.actions; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; + + // Error numbers taken from + // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json + var SERVER_CODES = loop.store.SERVER_CODES = { + INVALID_TOKEN: 105, + EXPIRED: 111, + ROOM_FULL: 202 + }; var ROOM_STATES = loop.store.ROOM_STATES = { // 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", @@ -79,17 +88,18 @@ loop.store.ActiveRoomStore = (function() * @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, audioMuted: false, - videoMuted: false + videoMuted: false, + failureReason: undefined }; } 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%. */ @@ -107,23 +117,34 @@ loop.store.ActiveRoomStore = (function() }, /** * Handles a room failure. * * @param {sharedActions.RoomFailure} actionData */ roomFailure: function(actionData) { + function getReason(serverCode) { + switch (serverCode) { + case SERVER_CODES.INVALID_TOKEN: + case SERVER_CODES.EXPIRED: + return FAILURE_REASONS.EXPIRED_OR_INVALID; + default: + return FAILURE_REASONS.UNKNOWN; + } + } + console.error("Error in state `" + this._storeState.roomState + "`:", actionData.error); this.setStoreState({ error: actionData.error, - roomState: actionData.error.errno === 202 ? ROOM_STATES.FULL - : ROOM_STATES.FAILED + failureReason: getReason(actionData.error.errno), + roomState: actionData.error.errno === SERVER_CODES.ROOM_FULL ? + ROOM_STATES.FULL : ROOM_STATES.FAILED }); }, /** * Registers the actions with the dispatcher that this store is interested * in. */ _registerActions: function() { @@ -223,16 +244,21 @@ loop.store.ActiveRoomStore = (function() roomUrl: actionData.roomUrl }); }, /** * Handles the action to join to a room. */ joinRoom: function() { + // Reset the failure reason if necessary. + if (this.getStoreState().failureReason) { + this.setStoreState({failureReason: undefined}); + } + this._mozLoop.rooms.join(this._storeState.roomToken, function(error, responseData) { if (error) { this._dispatcher.dispatch( new sharedActions.RoomFailure({error: error})); return; } @@ -270,21 +296,27 @@ loop.store.ActiveRoomStore = (function() connectedToSdkServers: function() { this.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED }); }, /** * Handles disconnection of this local client from the sdk servers. + * + * @param {sharedActions.ConnectionFailure} actionData */ - connectionFailure: function() { + connectionFailure: function(actionData) { // Treat all reasons as something failed. In theory, clientDisconnected // could be a success case, but there's no way we should be intentionally // sending that and still have the window open. + this.setStoreState({ + failureReason: actionData.reason + }); + this._leaveRoom(ROOM_STATES.FAILED); }, /** * Records the mute state for the stream. * * @param {sharedActions.setMute} actionData The mute state for the stream type. */
--- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -3,16 +3,17 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true */ var loop = loop || {}; loop.OTSdkDriver = (function() { var sharedActions = loop.shared.actions; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; /** * This is a wrapper for the OT sdk. It is used to translate the SDK events into * actions, and instruct the SDK what to do as a result of actions. */ var OTSdkDriver = function(options) { if (!options.dispatcher) { throw new Error("Missing option dispatcher"); @@ -42,18 +43,21 @@ loop.OTSdkDriver = (function() { this.getLocalElement = actionData.getLocalElementFunc; this.getRemoteElement = actionData.getRemoteElementFunc; this.publisherConfig = actionData.publisherConfig; // At this state we init the publisher, even though we might be waiting for // the initial connect of the session. This saves time when setting up // the media. this.publisher = this.sdk.initPublisher(this.getLocalElement(), - this.publisherConfig, - this._onPublishComplete.bind(this)); + this.publisherConfig); + this.publisher.on("accessAllowed", this._onPublishComplete.bind(this)); + this.publisher.on("accessDenied", this._onPublishDenied.bind(this)); + this.publisher.on("accessDialogOpened", + this._onAccessDialogOpened.bind(this)); }, /** * Handles the setMute action. Informs the published stream to mute * or unmute audio as appropriate. * * @param {sharedActions.SetMute} actionData The data associated with the * action. See action.js. @@ -91,26 +95,22 @@ loop.OTSdkDriver = (function() { this._onConnectionComplete.bind(this)); }, /** * Disconnects the sdk session. */ disconnectSession: function() { if (this.session) { - this.session.off("streamCreated", this._onRemoteStreamCreated.bind(this)); - this.session.off("connectionDestroyed", - this._onConnectionDestroyed.bind(this)); - this.session.off("sessionDisconnected", - this._onSessionDisconnected.bind(this)); - + this.session.off("streamCreated connectionDestroyed sessionDisconnected"); this.session.disconnect(); delete this.session; } if (this.publisher) { + this.publisher.off("accessAllowed accessDenied accessDialogOpened"); this.publisher.destroy(); delete this.publisher; } // Also, tidy these variables ready for next time. delete this._sessionConnected; delete this._publisherReady; delete this._publishedLocalStream; @@ -121,17 +121,17 @@ loop.OTSdkDriver = (function() { * Called once the session has finished connecting. * * @param {Error} error An OT error object, null if there was no error. */ _onConnectionComplete: function(error) { if (error) { console.error("Failed to complete connection", error); this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "couldNotConnect" + reason: FAILURE_REASONS.COULD_NOT_CONNECT })); return; } this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers()); this._sessionConnected = true; this._maybePublishLocalStream(); }, @@ -154,17 +154,17 @@ loop.OTSdkDriver = (function() { * * @param {SessionDisconnectEvent} event The event details: * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html */ _onSessionDisconnected: function(event) { // We only need to worry about the network disconnected reason here. if (event.reason === "networkDisconnected") { this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "networkDisconnected" + reason: FAILURE_REASONS.NETWORK_DISCONNECTED })); } }, _onConnectionCreated: function(event) { if (this.session.connection.id === event.connection.id) { return; } @@ -184,34 +184,52 @@ loop.OTSdkDriver = (function() { this._subscribedRemoteStream = true; if (this._checkAllStreamsConnected()) { this.dispatcher.dispatch(new sharedActions.MediaConnected()); } }, /** + * Called from the sdk when the media access dialog is opened. + * Prevents the default action, to prevent the SDK's "allow access" + * dialog from being shown. + * + * @param {OT.Event} event + */ + _onAccessDialogOpened: function(event) { + event.preventDefault(); + }, + + /** * Handles the publishing being complete. * - * @param {Error} error An OT error object, null if there was no error. + * @param {OT.Event} event */ - _onPublishComplete: function(error) { - if (error) { - console.error("Failed to initialize publisher", error); - this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "noMedia" - })); - return; - } - + _onPublishComplete: function(event) { + event.preventDefault(); this._publisherReady = true; this._maybePublishLocalStream(); }, /** + * Handles publishing of media being denied. + * + * @param {OT.Event} event + */ + _onPublishDenied: function(event) { + // This prevents the SDK's "access denied" dialog showing. + event.preventDefault(); + + this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ + reason: FAILURE_REASONS.MEDIA_DENIED + })); + }, + + /** * Publishes the local stream if the session is connected * and the publisher is ready. */ _maybePublishLocalStream: function() { if (this._sessionConnected && this._publisherReady) { // We are clear to publish the stream to the session. this.session.publish(this.publisher);
--- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -12,16 +12,24 @@ loop.shared.utils = (function(mozL10n) { /** * Call types used for determining if a call is audio/video or audio-only. */ var CALL_TYPES = { AUDIO_VIDEO: "audio-video", AUDIO_ONLY: "audio" }; + var FAILURE_REASONS = { + MEDIA_DENIED: "reason-media-denied", + COULD_NOT_CONNECT: "reason-could-not-connect", + NETWORK_DISCONNECTED: "reason-network-disconnected", + EXPIRED_OR_INVALID: "reason-expired-or-invalid", + UNKNOWN: "reason-unknown" + }; + /** * Format a given date into an l10n-friendly string. * * @param {Integer} The timestamp in seconds to format. * @return {String} The formatted string. */ function formatDate(timestamp) { var date = (new Date(timestamp * 1000)); @@ -105,14 +113,15 @@ loop.shared.utils = (function(mozL10n) { learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }), recipient ); } return { CALL_TYPES: CALL_TYPES, + FAILURE_REASONS: FAILURE_REASONS, Helper: Helper, composeCallUrlEmail: composeCallUrlEmail, formatDate: formatDate, getBoolPreference: getBoolPreference }; })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/standalone/content/css/webapp.css +++ b/browser/components/loop/standalone/content/css/webapp.css @@ -13,16 +13,23 @@ body, .standalone { width: 100%; background: #fbfbfb; color: #666; text-align: center; font-family: Open Sans,sans-serif; } +/** + * Note: the is-standalone-room class is dynamically set by the StandaloneRoomView. + */ +.standalone.is-standalone-room { + background-color: #000; +} + .standalone-header { border-radius: 4px; background: #fff; border: 1px solid #E7E7E7; box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.03); background-image: url("../shared/img/beta-ribbon.svg#beta-ribbon"); background-size: 5rem 5rem; background-repeat: no-repeat;
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -6,18 +6,20 @@ /* global loop:true, React */ /* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.standaloneRoomViews = (function(mozL10n) { "use strict"; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var ROOM_STATES = loop.store.ROOM_STATES; var sharedActions = loop.shared.actions; + var sharedMixins = loop.shared.mixins; var sharedViews = loop.shared.views; var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea', propTypes: { helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired }, _renderCallToActionLink: function() { @@ -34,16 +36,30 @@ loop.standaloneRoomViews = (function(moz React.DOM.a({href: loop.config.brandWebsiteUrl, className: "btn btn-info"}, mozL10n.get("rooms_room_full_call_to_action_nonFx_label", { brandShortname: mozL10n.get("brandShortname") }) ) ); }, + /** + * @return String An appropriate string according to the failureReason. + */ + _getFailureString: function() { + switch(this.props.failureReason) { + case FAILURE_REASONS.MEDIA_DENIED: + return mozL10n.get("rooms_media_denied_message"); + case FAILURE_REASONS.EXPIRED_OR_INVALID: + return mozL10n.get("rooms_unavailable_notification_message"); + default: + return mozL10n.get("status_error"); + }; + }, + _renderContent: function() { switch(this.props.roomState) { case ROOM_STATES.INIT: case ROOM_STATES.READY: { return ( React.DOM.button({className: "btn btn-join btn-info", onClick: this.props.joinRoom}, mozL10n.get("rooms_room_join_label") @@ -62,30 +78,73 @@ loop.standaloneRoomViews = (function(moz return ( React.DOM.div(null, React.DOM.p({className: "full-room-message"}, mozL10n.get("rooms_room_full_label") ), React.DOM.p(null, this._renderCallToActionLink()) ) ); + case ROOM_STATES.FAILED: + return ( + React.DOM.p({className: "failed-room-message"}, + this._getFailureString() + ) + ); default: return null; } }, render: function() { return ( React.DOM.div({className: "room-inner-info-area"}, this._renderContent() ) ); } }); + var StandaloneRoomHeader = React.createClass({displayName: 'StandaloneRoomHeader', + render: function() { + return ( + React.DOM.header(null, + React.DOM.h1(null, mozL10n.get("clientShortname2")) + ) + ); + } + }); + + var StandaloneRoomFooter = React.createClass({displayName: 'StandaloneRoomFooter', + _getContent: function() { + return mozL10n.get("legal_text_and_links", { + "clientShortname": mozL10n.get("clientShortname2"), + "terms_of_use_url": React.renderComponentToStaticMarkup( + React.DOM.a({href: loop.config.legalWebsiteUrl, target: "_blank"}, + mozL10n.get("terms_of_use_link_text") + ) + ), + "privacy_notice_url": React.renderComponentToStaticMarkup( + React.DOM.a({href: loop.config.privacyWebsiteUrl, target: "_blank"}, + mozL10n.get("privacy_notice_link_text") + ) + ), + }); + }, + + render: function() { + return ( + React.DOM.footer(null, + React.DOM.p({dangerouslySetInnerHTML: {__html: this._getContent()}}), + React.DOM.div({className: "footer-logo"}) + ) + ); + } + }); + var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView', mixins: [Backbone.Events], propTypes: { activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired @@ -139,16 +198,21 @@ loop.standaloneRoomViews = (function(moz bugDisplayMode: "off", buttonDisplayMode: "off", nameDisplayMode: "off", videoDisabledDisplayMode: "off" } }; }, + componentDidMount: function() { + // Adding a class to the document body element from here to ease styling it. + document.body.classList.add("is-standalone-room"); + }, + componentWillUnmount: function() { this.stopListening(this.props.activeRoomStore); }, /** * Watches for when we transition from READY to JOINED room state, so we can * request user media access. * @param {Object} nextProps (Unused) @@ -202,17 +266,19 @@ loop.standaloneRoomViews = (function(moz hide: !this._roomIsActive(), local: true, "local-stream": true, "local-stream-audio": false }); return ( React.DOM.div({className: "room-conversation-wrapper"}, + StandaloneRoomHeader(null), StandaloneRoomInfoArea({roomState: this.state.roomState, + failureReason: this.state.failureReason, joinRoom: this.joinRoom, helper: this.props.helper}), React.DOM.div({className: "video-layout-wrapper"}, React.DOM.div({className: "conversation room-conversation"}, React.DOM.h2({className: "room-name"}, this.state.roomName), React.DOM.div({className: "media nested"}, React.DOM.div({className: "video_wrapper remote_wrapper"}, React.DOM.div({className: "video_inner remote"}) @@ -224,17 +290,18 @@ loop.standaloneRoomViews = (function(moz visible: this._roomIsActive()}, audio: {enabled: !this.state.audioMuted, visible: this._roomIsActive()}, publishStream: this.publishStream, hangup: this.leaveRoom, hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), enableHangup: this._roomIsActive()}) ) - ) + ), + StandaloneRoomFooter(null) ) ); } }); return { StandaloneRoomView: StandaloneRoomView };
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -6,18 +6,20 @@ /* global loop:true, React */ /* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.standaloneRoomViews = (function(mozL10n) { "use strict"; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var ROOM_STATES = loop.store.ROOM_STATES; var sharedActions = loop.shared.actions; + var sharedMixins = loop.shared.mixins; var sharedViews = loop.shared.views; var StandaloneRoomInfoArea = React.createClass({ propTypes: { helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired }, _renderCallToActionLink: function() { @@ -34,16 +36,30 @@ loop.standaloneRoomViews = (function(moz <a href={loop.config.brandWebsiteUrl} className="btn btn-info"> {mozL10n.get("rooms_room_full_call_to_action_nonFx_label", { brandShortname: mozL10n.get("brandShortname") })} </a> ); }, + /** + * @return String An appropriate string according to the failureReason. + */ + _getFailureString: function() { + switch(this.props.failureReason) { + case FAILURE_REASONS.MEDIA_DENIED: + return mozL10n.get("rooms_media_denied_message"); + case FAILURE_REASONS.EXPIRED_OR_INVALID: + return mozL10n.get("rooms_unavailable_notification_message"); + default: + return mozL10n.get("status_error"); + }; + }, + _renderContent: function() { switch(this.props.roomState) { case ROOM_STATES.INIT: case ROOM_STATES.READY: { return ( <button className="btn btn-join btn-info" onClick={this.props.joinRoom}> {mozL10n.get("rooms_room_join_label")} @@ -62,30 +78,73 @@ loop.standaloneRoomViews = (function(moz return ( <div> <p className="full-room-message"> {mozL10n.get("rooms_room_full_label")} </p> <p>{this._renderCallToActionLink()}</p> </div> ); + case ROOM_STATES.FAILED: + return ( + <p className="failed-room-message"> + {this._getFailureString()} + </p> + ); default: return null; } }, render: function() { return ( <div className="room-inner-info-area"> {this._renderContent()} </div> ); } }); + var StandaloneRoomHeader = React.createClass({ + render: function() { + return ( + <header> + <h1>{mozL10n.get("clientShortname2")}</h1> + </header> + ); + } + }); + + var StandaloneRoomFooter = React.createClass({ + _getContent: function() { + return mozL10n.get("legal_text_and_links", { + "clientShortname": mozL10n.get("clientShortname2"), + "terms_of_use_url": React.renderComponentToStaticMarkup( + <a href={loop.config.legalWebsiteUrl} target="_blank"> + {mozL10n.get("terms_of_use_link_text")} + </a> + ), + "privacy_notice_url": React.renderComponentToStaticMarkup( + <a href={loop.config.privacyWebsiteUrl} target="_blank"> + {mozL10n.get("privacy_notice_link_text")} + </a> + ), + }); + }, + + render: function() { + return ( + <footer> + <p dangerouslySetInnerHTML={{__html: this._getContent()}}></p> + <div className="footer-logo" /> + </footer> + ); + } + }); + var StandaloneRoomView = React.createClass({ mixins: [Backbone.Events], propTypes: { activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired @@ -139,16 +198,21 @@ loop.standaloneRoomViews = (function(moz bugDisplayMode: "off", buttonDisplayMode: "off", nameDisplayMode: "off", videoDisabledDisplayMode: "off" } }; }, + componentDidMount: function() { + // Adding a class to the document body element from here to ease styling it. + document.body.classList.add("is-standalone-room"); + }, + componentWillUnmount: function() { this.stopListening(this.props.activeRoomStore); }, /** * Watches for when we transition from READY to JOINED room state, so we can * request user media access. * @param {Object} nextProps (Unused) @@ -202,17 +266,19 @@ loop.standaloneRoomViews = (function(moz hide: !this._roomIsActive(), local: true, "local-stream": true, "local-stream-audio": false }); return ( <div className="room-conversation-wrapper"> + <StandaloneRoomHeader /> <StandaloneRoomInfoArea roomState={this.state.roomState} + failureReason={this.state.failureReason} joinRoom={this.joinRoom} helper={this.props.helper} /> <div className="video-layout-wrapper"> <div className="conversation room-conversation"> <h2 className="room-name">{this.state.roomName}</h2> <div className="media nested"> <div className="video_wrapper remote_wrapper"> <div className="video_inner remote"></div> @@ -225,16 +291,17 @@ loop.standaloneRoomViews = (function(moz audio={{enabled: !this.state.audioMuted, visible: this._roomIsActive()}} publishStream={this.publishStream} hangup={this.leaveRoom} hangupButtonLabel={mozL10n.get("rooms_leave_button_label")} enableHangup={this._roomIsActive()} /> </div> </div> + <StandaloneRoomFooter /> </div> ); } }); return { StandaloneRoomView: StandaloneRoomView };
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties +++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties @@ -111,16 +111,18 @@ rooms_new_room_button_label=Start a conv rooms_only_occupant_label=You're the first one here. rooms_panel_title=Choose a conversation or start a new one rooms_room_full_label=There are already two people in this conversation. rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} » rooms_room_joined_label=Someone has joined the conversation! rooms_room_join_label=Join the conversation rooms_display_name_guest=Guest +rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid. +rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again. ## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be ## replaced by the brand name and {{currentStatus}} will be replaced ## by the current call status (Connecting, Ringing, etc.) standalone_title_with_status={{clientShortname}} — {{currentStatus}} status_in_conversation=In conversation status_conversation_ended=Conversation ended status_error=Something went wrong
--- a/browser/components/loop/test/desktop-local/roomViews_test.js +++ b/browser/components/loop/test/desktop-local/roomViews_test.js @@ -60,16 +60,17 @@ describe("loop.roomViews", function () { var testView = TestUtils.renderIntoDocument(TestView({ roomStore: roomStore })); expect(testView.state).eql({ roomState: ROOM_STATES.INIT, audioMuted: false, videoMuted: false, + failureReason: undefined, foo: "bar" }); }); it("should listen to store changes", function() { var TestView = React.createClass({ mixins: [loop.roomViews.ActiveRoomStoreMixin], render: function() { return React.DOM.div(); }
--- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -1,17 +1,19 @@ /* global chai, loop */ var expect = chai.expect; var sharedActions = loop.shared.actions; describe("loop.store.ActiveRoomStore", function () { "use strict"; + var SERVER_CODES = loop.store.SERVER_CODES; var ROOM_STATES = loop.store.ROOM_STATES; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver; var fakeMultiplexGum; beforeEach(function() { sandbox = sinon.sandbox.create(); sandbox.useFakeTimers(); dispatcher = new loop.Dispatcher(); @@ -86,29 +88,50 @@ describe("loop.store.ActiveRoomStore", f it("should log the error", function() { store.roomFailure({error: fakeError}); sinon.assert.calledOnce(console.error); sinon.assert.calledWith(console.error, sinon.match(ROOM_STATES.READY), fakeError); }); - it("should set the state to `FULL` on server errno 202", function() { - fakeError.errno = 202; + it("should set the state to `FULL` on server error room full", function() { + fakeError.errno = SERVER_CODES.ROOM_FULL; store.roomFailure({error: fakeError}); expect(store._storeState.roomState).eql(ROOM_STATES.FULL); }); it("should set the state to `FAILED` on generic error", function() { store.roomFailure({error: fakeError}); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.UNKNOWN); }); + + it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + + "invalid token", function() { + fakeError.errno = SERVER_CODES.INVALID_TOKEN; + + store.roomFailure({error: fakeError}); + + expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID); + }); + + it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + + "expired", function() { + fakeError.errno = SERVER_CODES.EXPIRED; + + store.roomFailure({error: fakeError}); + + expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID); + }); }); describe("#setupWindowData", function() { var fakeToken, fakeRoomData; beforeEach(function() { fakeToken = "337-ff-54"; fakeRoomData = { @@ -239,16 +262,24 @@ describe("loop.store.ActiveRoomStore", f }); }); describe("#joinRoom", function() { beforeEach(function() { store.setStoreState({roomToken: "tokenFake"}); }); + it("should reset failureReason", function() { + store.setStoreState({failureReason: "Test"}); + + store.joinRoom(); + + expect(store.getStoreState().failureReason).eql(undefined); + }); + it("should call rooms.join on mozLoop", function() { store.joinRoom(); sinon.assert.calledOnce(fakeMozLoop.rooms.join); sinon.assert.calledWith(fakeMozLoop.rooms.join, "tokenFake"); }); it("should dispatch `JoinedRoom` on success", function() { @@ -375,55 +406,67 @@ describe("loop.store.ActiveRoomStore", f 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() { + var connectionFailureAction; + beforeEach(function() { store.setStoreState({ roomState: ROOM_STATES.JOINED, roomToken: "fakeToken", sessionToken: "1627384950" }); + + connectionFailureAction = new sharedActions.ConnectionFailure({ + reason: "FAIL" + }); + }); + + it("should store the failure reason", function() { + store.connectionFailure(connectionFailureAction); + + expect(store.getStoreState().failureReason).eql("FAIL"); }); it("should reset the multiplexGum", function() { - store.leaveRoom(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeMultiplexGum.reset); }); it("should disconnect from the servers via the sdk", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeSdkDriver.disconnectSession); }); it("should clear any existing timeout", function() { sandbox.stub(window, "clearTimeout"); store._timeout = {}; - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(clearTimeout); }); it("should call mozLoop.rooms.leave", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeMozLoop.rooms.leave); sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave, "fakeToken", "1627384950"); }); it("should set the state to `FAILED`", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); 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});
--- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -2,26 +2,29 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ var expect = chai.expect; describe("loop.OTSdkDriver", function () { "use strict"; var sharedActions = loop.shared.actions; - + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var sandbox; var dispatcher, driver, publisher, sdk, session, sessionData; - var fakeLocalElement, fakeRemoteElement, publisherConfig; + var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent; beforeEach(function() { sandbox = sinon.sandbox.create(); fakeLocalElement = {fake: 1}; fakeRemoteElement = {fake: 2}; + fakeEvent = { + preventDefault: sinon.stub() + }; publisherConfig = { fake: "config" }; sessionData = { apiKey: "1234567890", sessionId: "3216549870", sessionToken: "1357924680" }; @@ -29,24 +32,24 @@ describe("loop.OTSdkDriver", function () dispatcher = new loop.Dispatcher(); session = _.extend({ connect: sinon.stub(), disconnect: sinon.stub(), publish: sinon.stub(), subscribe: sinon.stub() }, Backbone.Events); - publisher = { + publisher = _.extend({ destroy: sinon.stub(), publishAudio: sinon.stub(), publishVideo: sinon.stub() - }; + }, Backbone.Events); sdk = { - initPublisher: sinon.stub(), + initPublisher: sinon.stub().returns(publisher), initSession: sinon.stub().returns(session) }; driver = new loop.OTSdkDriver({ dispatcher: dispatcher, sdk: sdk }); }); @@ -75,61 +78,16 @@ describe("loop.OTSdkDriver", function () getLocalElementFunc: function() {return fakeLocalElement;}, getRemoteElementFunc: function() {return fakeRemoteElement;}, publisherConfig: publisherConfig })); sinon.assert.calledOnce(sdk.initPublisher); sinon.assert.calledWith(sdk.initPublisher, fakeLocalElement, publisherConfig); }); - - describe("On Publisher Complete", function() { - it("should publish the stream if the connection is ready", function() { - sdk.initPublisher.callsArgWith(2, null); - - driver.session = session; - driver._sessionConnected = true; - - dispatcher.dispatch(new sharedActions.SetupStreamElements({ - getLocalElementFunc: function() {return fakeLocalElement;}, - getRemoteElementFunc: function() {return fakeRemoteElement;}, - publisherConfig: publisherConfig - })); - - sinon.assert.calledOnce(session.publish); - }); - - it("should dispatch connectionFailure if connecting failed", function() { - sdk.initPublisher.callsArgWith(2, new Error("Failure")); - - // Special stub, as we want to use the dispatcher, but also know that - // we've been called correctly for the second dispatch. - var dispatchStub = (function() { - var originalDispatch = dispatcher.dispatch.bind(dispatcher); - return sandbox.stub(dispatcher, "dispatch", function(action) { - originalDispatch(action); - }); - }()); - - driver.session = session; - driver._sessionConnected = true; - - dispatcher.dispatch(new sharedActions.SetupStreamElements({ - getLocalElementFunc: function() {return fakeLocalElement;}, - getRemoteElementFunc: function() {return fakeRemoteElement;}, - publisherConfig: publisherConfig - })); - - sinon.assert.called(dispatcher.dispatch); - sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("name", "connectionFailure")); - sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("reason", "noMedia")); - }); - }); }); describe("#setMute", function() { beforeEach(function() { sdk.initPublisher.returns(publisher); dispatcher.dispatch(new sharedActions.SetupStreamElements({ getLocalElementFunc: function() {return fakeLocalElement;}, @@ -189,17 +147,17 @@ describe("loop.OTSdkDriver", function () sandbox.stub(dispatcher, "dispatch"); driver.connectSession(sessionData); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithMatch(dispatcher.dispatch, sinon.match.hasOwn("name", "connectionFailure")); sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("reason", "couldNotConnect")); + sinon.match.hasOwn("reason", FAILURE_REASONS.COULD_NOT_CONNECT)); }); }); }); describe("#disconnectionSession", function() { it("should disconnect the session", function() { driver.session = session; @@ -264,17 +222,17 @@ describe("loop.OTSdkDriver", function () session.trigger("sessionDisconnected", { 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", "networkDisconnected")); + sinon.match.hasOwn("reason", FAILURE_REASONS.NETWORK_DISCONNECTED)); }); }); describe("streamCreated", function() { var fakeStream; beforeEach(function() { fakeStream = { @@ -323,10 +281,46 @@ describe("loop.OTSdkDriver", function () function() { session.trigger("connectionCreated", { connection: {id: "localUser"} }); sinon.assert.notCalled(dispatcher.dispatch); }); }); + + describe("accessAllowed", function() { + it("should publish the stream if the connection is ready", function() { + driver._sessionConnected = true; + + publisher.trigger("accessAllowed", fakeEvent); + + sinon.assert.calledOnce(session.publish); + }); + }); + + describe("accessDenied", function() { + it("should prevent the default event behavior", function() { + publisher.trigger("accessDenied", fakeEvent); + + sinon.assert.calledOnce(fakeEvent.preventDefault); + }); + + it("should dispatch connectionFailure", function() { + publisher.trigger("accessDenied", fakeEvent); + + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithMatch(dispatcher.dispatch, + sinon.match.hasOwn("name", "connectionFailure")); + sinon.assert.calledWithMatch(dispatcher.dispatch, + sinon.match.hasOwn("reason", FAILURE_REASONS.MEDIA_DENIED)); + }); + }); + + describe("accessDialogOpened", function() { + it("should prevent the default event behavior", function() { + publisher.trigger("accessDialogOpened", fakeEvent); + + sinon.assert.calledOnce(fakeEvent.preventDefault); + }); + }); }); });
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -134,16 +134,26 @@ describe("loop.standaloneRoomViews", fun function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL}); expect(view.getDOMNode().querySelector(".full-room-message")) .not.eql(null); }); }); + describe("Failed room message", function() { + it("should display a failed room message on FAILED", + function() { + activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED}); + + expect(view.getDOMNode().querySelector(".failed-room-message")) + .not.eql(null); + }); + }); + describe("Join button", function() { function getJoinButton(view) { return view.getDOMNode().querySelector(".btn-join"); } it("should render the Join button when room isn't active", function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
--- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -603,16 +603,26 @@ Example({summary: "Standalone room conversation (full - non FFx user)"}, React.DOM.div({className: "standalone"}, StandaloneRoomView({ dispatcher: dispatcher, activeRoomStore: activeRoomStore, roomState: ROOM_STATES.FULL, helper: {isFirefox: returnFalse}}) ) + ), + + Example({summary: "Standalone room conversation (failed)"}, + React.DOM.div({className: "standalone"}, + StandaloneRoomView({ + dispatcher: dispatcher, + activeRoomStore: activeRoomStore, + roomState: ROOM_STATES.FAILED, + helper: {isFirefox: returnFalse}}) + ) ) ), 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 @@ -604,16 +604,26 @@ <div className="standalone"> <StandaloneRoomView dispatcher={dispatcher} activeRoomStore={activeRoomStore} roomState={ROOM_STATES.FULL} helper={{isFirefox: returnFalse}} /> </div> </Example> + + <Example summary="Standalone room conversation (failed)"> + <div className="standalone"> + <StandaloneRoomView + dispatcher={dispatcher} + activeRoomStore={activeRoomStore} + roomState={ROOM_STATES.FAILED} + helper={{isFirefox: returnFalse}} /> + </div> + </Example> </Section> <Section name="SVG icons preview"> <Example summary="16x16"> <SVGIcons /> </Example> </Section>
--- a/browser/components/sessionstore/test/browser_586068-reload.js +++ b/browser/components/sessionstore/test/browser_586068-reload.js @@ -4,137 +4,51 @@ const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand"; function test() { TestRunner.run(); } function runTests() { - // Request a longer timeout because the test takes quite a while - // to complete on slow Windows debug machines and we would otherwise - // see a lot of (not so) intermittent test failures. - requestLongerTimeout(2); - Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true); registerCleanupFunction(function () { Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND); }); let state = { windows: [{ tabs: [ { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } }, { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } } ], selected: 1 }] }; let loadCount = 0; - gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) { - loadCount++; - is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url, - "load " + loadCount + " - browser loaded correct url"); + gBrowser.tabContainer.addEventListener("SSTabRestored", function onRestored(event) { + let tab = event.target; + let browser = tab.linkedBrowser; + let tabData = state.windows[0].tabs[loadCount++]; - if (loadCount <= state.windows[0].tabs.length) { - // double check that this tab was the right one - let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq; - let tab; - for (let i = 0; i < window.gBrowser.tabs.length; i++) { - if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser) - tab = window.gBrowser.tabs[i]; - } - is(ss.getTabValue(tab, "uniq"), expectedData, - "load " + loadCount + " - correct tab was restored"); + // double check that this tab was the right one + is(browser.currentURI.spec, tabData.entries[0].url, + "load " + loadCount + " - browser loaded correct url"); + is(ss.getTabValue(tab, "uniq"), tabData.extData.uniq, + "load " + loadCount + " - correct tab was restored"); - if (loadCount == state.windows[0].tabs.length) { - gProgressListener.unsetCallback(); - executeSoon(function () { - reloadAllTabs(state, function () { - waitForBrowserState(TestRunner.backupState, testCascade); - }); - }); - } else { - // reload the next tab - window.gBrowser.reloadTab(window.gBrowser.tabs[loadCount]); - } + if (loadCount == state.windows[0].tabs.length) { + gBrowser.tabContainer.removeEventListener("SSTabRestored", onRestored); + + executeSoon(function () { + waitForBrowserState(TestRunner.backupState, finish); + }); + } else { + // reload the next tab + gBrowser.browsers[loadCount].reload(); } }); yield ss.setBrowserState(JSON.stringify(state)); } - -function testCascade() { - Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false); - - let state = { windows: [{ tabs: [ - { entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } }, - { entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } } - ] }] }; - - let loadCount = 0; - gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) { - if (++loadCount < state.windows[0].tabs.length) { - return; - } - - gProgressListener.unsetCallback(); - executeSoon(function () { - reloadAllTabs(state, next); - }); - }); - - ss.setBrowserState(JSON.stringify(state)); -} - -function reloadAllTabs(aState, aCallback) { - // Simulate a left mouse button click with no modifiers, which is what - // Command-R, or clicking reload does. - let fakeEvent = { - button: 0, - metaKey: false, - altKey: false, - ctrlKey: false, - shiftKey: false - }; - - let loadCount = 0; - gWebProgressListener.setCallback(function (aBrowser) { - if (++loadCount <= aState.windows[0].tabs.length) { - // double check that this tab was the right one - let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq; - let tab; - for (let i = 0; i < window.gBrowser.tabs.length; i++) { - if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser) - tab = window.gBrowser.tabs[i]; - } - is(ss.getTabValue(tab, "uniq"), expectedData, - "load " + loadCount + " - correct tab was reloaded"); - - if (loadCount == aState.windows[0].tabs.length) { - gWebProgressListener.unsetCallback(); - executeSoon(aCallback); - } else { - // reload the next tab - window.gBrowser.selectTabAtIndex(loadCount); - BrowserReloadOrDuplicate(fakeEvent); - } - } - }); - - BrowserReloadOrDuplicate(fakeEvent); -}
--- a/browser/experiments/Makefile.in +++ b/browser/experiments/Makefile.in @@ -3,15 +3,14 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. include $(topsrcdir)/config/rules.mk # This is so hacky. Waiting on bug 988938. addondir = $(srcdir)/test/addons testdir = $(abspath $(DEPTH)/_tests/xpcshell/browser/experiments/test/xpcshell) -libs:: +misc:: $(call mkdir_deps,$(testdir)) $(EXIT_ON_ERROR) \ - $(NSINSTALL) -D $(testdir); \ for dir in $(addondir)/*; do \ base=`basename $$dir`; \ (cd $$dir && zip -qr $(testdir)/$$base.xpi *); \ done
--- a/browser/experiments/moz.build +++ b/browser/experiments/moz.build @@ -1,12 +1,14 @@ # 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/. +HAS_MISC_RULE = True + EXTRA_COMPONENTS += [ 'Experiments.manifest', 'ExperimentsService.js', ] EXTRA_JS_MODULES.experiments += [ 'Experiments.jsm', ]
--- a/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.properties +++ b/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.properties @@ -1,2 +1,6 @@ -title=You're browsing privately -title.normal=Open a private window? \ No newline at end of file +# 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/. + +title=You're browsing privately +title.normal=Open a private window?
--- a/build/mobile/robocop/FennecNativeActions.java +++ b/build/mobile/robocop/FennecNativeActions.java @@ -36,17 +36,17 @@ public class FennecNativeActions impleme mSolo = robocop; mInstr = instrumentation; mAsserter = asserter; GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath()); } class GeckoEventExpecter implements RepeatedEventExpecter { - private static final int MAX_WAIT_MS = 90000; + private static final int MAX_WAIT_MS = 180000; private volatile boolean mIsRegistered; private final String mGeckoEvent; private final GeckoEventListener mListener; private volatile boolean mEventEverReceived; private String mEventData;
--- a/layout/style/test/Makefile.in +++ b/layout/style/test/Makefile.in @@ -18,10 +18,11 @@ ifdef COMPILE_ENVIRONMENT css_properties.js: host_ListCSSProperties$(HOST_BIN_SUFFIX) css_properties_like_longhand.js Makefile $(RM) $@ ./host_ListCSSProperties$(HOST_BIN_SUFFIX) > $@ cat $(srcdir)/css_properties_like_longhand.js >> $@ GARBAGE += css_properties.js TEST_FILES := css_properties.js TEST_DEST = $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) +TEST_TARGET := misc INSTALL_TARGETS += TEST endif
--- a/layout/style/test/moz.build +++ b/layout/style/test/moz.build @@ -1,14 +1,16 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +HAS_MISC_RULE = True + HostSimplePrograms([ 'host_ListCSSProperties', ]) MOCHITEST_MANIFESTS += [ 'chrome/mochitest.ini', 'css-visited/mochitest.ini', 'mochitest.ini',
--- a/mobile/android/base/SharedPreferencesHelper.java +++ b/mobile/android/base/SharedPreferencesHelper.java @@ -22,16 +22,20 @@ import java.util.HashMap; /** * Helper class to get, set, and observe Android Shared Preferences. */ public final class SharedPreferencesHelper implements GeckoEventListener { public static final String LOGTAG = "GeckoAndSharedPrefs"; + // Calculate this once, at initialization. isLoggable is too expensive to + // have in-line in each log call. + private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); + private enum Scope { APP("app"), PROFILE("profile"), GLOBAL("global"); public final String key; private Scope(String key) { @@ -209,17 +213,17 @@ public final class SharedPreferencesHelp public ChangeListener(final Scope scope, final String branch, final String profileName) { this.scope = scope; this.branch = branch; this.profileName = profileName; } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got onSharedPreferenceChanged"); } try { final JSONObject msg = new JSONObject(); msg.put("scope", this.scope.key); msg.put("branch", this.branch); msg.put("profileName", this.profileName); msg.put("key", key); @@ -274,29 +278,29 @@ public final class SharedPreferencesHelp } @Override public void handleMessage(String event, JSONObject message) { // Everything here is synchronous and serial, so we need not worry about // overwriting an in-progress response. try { if (event.equals("SharedPreferences:Set")) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Set message."); } handleSet(message); } else if (event.equals("SharedPreferences:Get")) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Get message."); } JSONObject obj = new JSONObject(); obj.put("values", handleGet(message)); EventDispatcher.sendResponse(message, obj); } else if (event.equals("SharedPreferences:Observe")) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Observe message."); } handleObserve(message); } else { Log.e(LOGTAG, "SharedPreferencesHelper got unexpected message " + event); return; } } catch (JSONException e) {
--- a/mobile/android/base/distribution/Distribution.java +++ b/mobile/android/base/distribution/Distribution.java @@ -19,17 +19,16 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Queue; -import java.util.Scanner; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.net.ssl.SSLException; @@ -37,16 +36,17 @@ import org.apache.http.protocol.HTTP; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.mozglue.RobocopTarget; +import org.mozilla.gecko.util.FileUtils; import org.mozilla.gecko.util.ThreadUtils; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.os.SystemClock; import android.util.Log; @@ -281,17 +281,17 @@ public class Distribution { public DistributionDescriptor getDescriptor() { File descFile = getDistributionFile("preferences.json"); if (descFile == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { - JSONObject all = new JSONObject(getFileContents(descFile)); + JSONObject all = new JSONObject(FileUtils.getFileContents(descFile)); if (!all.has("Global")) { Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); return null; } return new DistributionDescriptor(all.getJSONObject("Global")); @@ -309,17 +309,17 @@ public class Distribution { public JSONArray getBookmarks() { File bookmarks = getDistributionFile("bookmarks.json"); if (bookmarks == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { - return new JSONArray(getFileContents(bookmarks)); + return new JSONArray(FileUtils.getFileContents(bookmarks)); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing bookmarks.json", e); Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; @@ -734,29 +734,16 @@ public class Distribution { } File system = getSystemDistributionDir(); if (system.exists()) { return this.distributionDir = system; } return null; } - // Shortcut to slurp a file without messing around with streams. - private String getFileContents(File file) throws IOException { - Scanner scanner = null; - try { - scanner = new Scanner(file, "UTF-8"); - return scanner.useDelimiter("\\A").next(); - } finally { - if (scanner != null) { - scanner.close(); - } - } - } - private String getDataDir() { return context.getApplicationInfo().dataDir; } private File getSystemDistributionDir() { return new File("/system/" + context.getPackageName() + "/distribution"); }
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java @@ -33,16 +33,17 @@ import android.os.Handler; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; import android.text.TextUtils; +import android.text.format.DateUtils; /** * A fragment that displays the status of an AndroidFxAccount. * <p> * The owning activity is responsible for providing an AndroidFxAccount at * appropriate times. */ public class FxAccountStatusFragment @@ -81,16 +82,17 @@ public class FxAccountStatusFragment protected CheckBoxPreference bookmarksPreference; protected CheckBoxPreference historyPreference; protected CheckBoxPreference tabsPreference; protected CheckBoxPreference passwordsPreference; protected EditTextPreference deviceNamePreference; protected Preference syncServerPreference; protected Preference morePreference; + protected Preference syncNowPreference; protected volatile AndroidFxAccount fxAccount; // The contract is: when fxAccount is non-null, then clientsDataDelegate is // non-null. If violated then an IllegalStateException is thrown. protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate; // Used to post delayed sync requests. protected Handler handler; @@ -162,16 +164,20 @@ public class FxAccountStatusFragment deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name"); deviceNamePreference.setOnPreferenceChangeListener(this); syncServerPreference = ensureFindPreference("sync_server"); morePreference = ensureFindPreference("more"); morePreference.setOnPreferenceClickListener(this); + syncNowPreference = ensureFindPreference("sync_now"); + syncNowPreference.setEnabled(true); + syncNowPreference.setOnPreferenceClickListener(this); + if (HardwareUtils.hasMenuButton()) { syncCategory.removePreference(morePreference); } } /** * We intentionally don't refresh here. Our owning activity is responsible for * providing an AndroidFxAccount to our refresh method in its onResume method. @@ -224,16 +230,23 @@ public class FxAccountStatusFragment return true; } if (preference == morePreference) { getActivity().openOptionsMenu(); return true; } + if (preference == syncNowPreference) { + if (fxAccount != null) { + FirefoxAccounts.requestSync(fxAccount.getAndroidAccount(), FirefoxAccounts.FORCE, null, null); + } + return true; + } + return false; } protected Bundle getExtrasForAccount() { final Bundle extras = new Bundle(); final ExtendedJSONObject o = new ExtendedJSONObject(); o.put(FxAccountAbstractSetupActivity.JSON_KEY_AUTH, fxAccount.getAccountServerURI()); final ExtendedJSONObject services = new ExtendedJSONObject(); @@ -245,16 +258,17 @@ public class FxAccountStatusFragment protected void setCheckboxesEnabled(boolean enabled) { bookmarksPreference.setEnabled(enabled); historyPreference.setEnabled(enabled); tabsPreference.setEnabled(enabled); passwordsPreference.setEnabled(enabled); // Since we can't sync, we can't update our remote client record. deviceNamePreference.setEnabled(enabled); + syncNowPreference.setEnabled(enabled); } /** * Show at most one error preference, hiding all others. * * @param errorPreferenceToShow * single error preference to show; if null, hide all error preferences */ @@ -465,16 +479,36 @@ public class FxAccountStatusFragment } finally { // No matter our state, we should update the checkboxes. updateSelectedEngines(); } final String clientName = clientsDataDelegate.getClientName(); deviceNamePreference.setSummary(clientName); deviceNamePreference.setText(clientName); + + updateSyncNowPreference(); + } + + // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span. + private String getLastSyncedString(final long startTime) { + final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime); + return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString); + } + + protected void updateSyncNowPreference() { + final boolean currentlySyncing = fxAccount.isCurrentlySyncing(); + syncNowPreference.setEnabled(!currentlySyncing); + if (currentlySyncing) { + syncNowPreference.setTitle(R.string.fxaccount_status_syncing); + } else { + syncNowPreference.setTitle(R.string.fxaccount_status_sync_now); + } + final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp()); + syncNowPreference.setSummary(lastSynced); } protected void updateAuthServerPreference() { final String authServer = fxAccount.getAccountServerURI(); final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer); final boolean currentlyShown = null != findPreference(authServerPreference.getKey()); if (currentlyShown != shouldBeShown) { if (shouldBeShown) {
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -60,16 +60,18 @@ public class AndroidFxAccount { public static final int CURRENT_BUNDLE_VERSION = 2; public static final String BUNDLE_KEY_BUNDLE_VERSION = "version"; public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel"; public static final String BUNDLE_KEY_STATE = "state"; protected static final List<String> ANDROID_AUTHORITIES = Collections.unmodifiableList(Arrays.asList(BrowserContract.AUTHORITY)); + private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp"; + protected final Context context; protected final AccountManager accountManager; protected final Account account; /** * Create an Android Firefox Account instance backed by an Android Account * instance. * <p> @@ -560,9 +562,27 @@ public class AndroidFxAccount { public static Intent makeDeletedAccountIntent(final Context context, final Account account) { final Intent intent = new Intent(FxAccountConstants.ACCOUNT_DELETED_ACTION); intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION)); intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name); return intent; } + + public void setLastSyncedTimestamp(long now) { + try { + getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit(); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e); + } + } + + public long getLastSyncedTimestamp() { + final long neverSynced = -1L; + try { + return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e); + return neverSynced; + } + } }
--- a/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java +++ b/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java @@ -100,16 +100,17 @@ public class FxAccountSchedulePolicy imp this.context.getContentResolver(); Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + "."); ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds); POLL_INTERVAL_CURRENT_SEC = intervalSeconds; } @Override public void onSuccessfulSync(int otherClientsCount) { + this.account.setLastSyncedTimestamp(System.currentTimeMillis()); // This undoes the change made in observeBackoffMillis -- once we hit backoff we'll // periodically sync at the backoff duration, but as soon as we succeed we'll switch // into the client-count-dependent interval. long interval = (otherClientsCount > 0) ? POLL_INTERVAL_MULTI_DEVICE_SEC : POLL_INTERVAL_SINGLE_DEVICE_SEC; requestPeriodicSync(interval); } @Override
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -429,38 +429,41 @@ public class FxAccountSyncAdapter extend * This should be replaced with a full {@link FxAccountAuthenticator}-based * token implementation. */ @Override public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); Logger.resetLogging(); + final Context context = getContext(); + final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); + Logger.info(LOG_TAG, "Syncing FxAccount" + " account named like " + Utils.obfuscateEmail(account.name) + " for authority " + authority + " with instance " + this + "."); + Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp()); + + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + fxAccount.dump(); + } + final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); FirefoxAccounts.logSyncHints(syncHints); // This applies even to forced syncs, but only on success. if (this.lastSyncRealtimeMillis > 0L && (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) { Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + ": minimum interval not met."); return; } - final Context context = getContext(); - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { - fxAccount.dump(); - } - // Pickle in a background thread to avoid strict mode warnings. ThreadPool.run(new Runnable() { @Override public void run() { try { AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); } catch (Exception e) { // Should never happen, but we really don't want to die in a background thread.
--- a/mobile/android/base/locales/en-US/sync_strings.dtd +++ b/mobile/android/base/locales/en-US/sync_strings.dtd @@ -182,16 +182,18 @@ <!ENTITY fxaccount_update_credentials_header 'Sign in'> <!ENTITY fxaccount_update_credentials_button_label 'Sign in'> <!ENTITY fxaccount_update_credentials_unknown_error 'Could not sign in'> <!ENTITY fxaccount_status_header2 'Firefox Account'> <!ENTITY fxaccount_status_signed_in_as 'Signed in as'> <!ENTITY fxaccount_status_auth_server 'Account server'> +<!ENTITY fxaccount_status_sync_now 'Sync now'> +<!ENTITY fxaccount_status_syncing 'Syncing...'> <!ENTITY fxaccount_status_device_name 'Device name'> <!ENTITY fxaccount_status_sync_server 'Sync server'> <!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'> <!ENTITY fxaccount_status_sync_enabled '&syncBrand.shortName.label;: enabled'> <!ENTITY fxaccount_status_needs_verification2 'Your account needs to be verified. Tap to resend verification email.'> <!ENTITY fxaccount_status_needs_credentials 'Cannot connect. Tap to sign in.'> <!ENTITY fxaccount_status_needs_upgrade 'You need to upgrade &brandShortName; to sign in.'> <!ENTITY fxaccount_status_needs_master_sync_automatically_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data” in Android Settings > Data Usage.'>
--- a/mobile/android/base/resources/layout/fxaccount_confirm_account.xml +++ b/mobile/android/base/resources/layout/fxaccount_confirm_account.xml @@ -40,17 +40,17 @@ style="@style/FxAccountButton" android:text="@string/fxaccount_back_to_browsing" /> <TextView android:id="@+id/resend_confirmation_email_link" style="@style/FxAccountLinkItem" android:text="@string/fxaccount_confirm_account_resend_email" /> - <TextView + <TextView android:id="@+id/change_confirmation_email_link" style="@style/FxAccountLinkItem" android:text="@string/fxaccount_confirm_account_change_email" /> <LinearLayout style="@style/FxAccountSpacer" /> <ImageView style="@style/FxAccountIcon"
--- a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml +++ b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml @@ -50,16 +50,24 @@ <Preference android:editable="false" android:icon="@drawable/fxaccount_sync_error" android:key="needs_account_enabled" android:layout="@layout/fxaccount_status_error_preference" android:persistent="false" android:title="@string/fxaccount_status_needs_account_enabled" /> + <Preference + android:editable="false" + android:key="sync_now" + android:defaultValue="" + android:persistent="false" + android:title="Sync now" + android:summary="" /> + <CheckBoxPreference android:key="bookmarks" android:persistent="false" android:title="@string/fxaccount_status_bookmarks" /> <CheckBoxPreference android:key="history" android:persistent="false" android:title="@string/fxaccount_status_history" />
--- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -91,16 +91,17 @@ <string name="history_week_section">&history_week_section2;</string> <string name="history_older_section">&history_older_section2;</string> <string name="share">&share;</string> <string name="share_title">&share_title;</string> <string name="share_image_failed">&share_image_failed;</string> <string name="save_as_pdf">&save_as_pdf;</string> <string name="find_in_page">&find_in_page;</string> + <string name="find_matchcase">&find_matchcase;</string> <string name="desktop_mode">&desktop_mode;</string> <string name="page">&page;</string> <string name="tools">&tools;</string> <string name="find_text">&find_text;</string> <string name="find_prev">&find_prev;</string> <string name="find_next">&find_next;</string> <string name="find_close">&find_close;</string>
--- a/mobile/android/base/sync/SyncConfiguration.java +++ b/mobile/android/base/sync/SyncConfiguration.java @@ -32,17 +32,16 @@ public class SyncConfiguration { public EditorBranch(SyncConfiguration config, String prefix) { if (!prefix.endsWith(".")) { throw new IllegalArgumentException("No trailing period in prefix."); } this.prefix = prefix; this.editor = config.getEditor(); } - @Override public void apply() { // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding. this.editor.commit(); } @Override public Editor clear() { this.editor = this.editor.clear(); @@ -81,17 +80,16 @@ public class SyncConfiguration { @Override public Editor putString(String key, String value) { this.editor = this.editor.putString(prefix + key, value); return this; } // Not marking as Override, because Android <= 10 doesn't have // putStringSet. Neither can we implement it. - @Override public Editor putStringSet(String key, Set<String> value) { throw new RuntimeException("putStringSet not available."); } @Override public Editor remove(String key) { this.editor = this.editor.remove(prefix + key); return this; @@ -157,17 +155,16 @@ public class SyncConfiguration { @Override public String getString(String key, String defValue) { return config.getPrefs().getString(prefix + key, defValue); } // Not marking as Override, because Android <= 10 doesn't have // getStringSet. Neither can we implement it. - @Override public Set<String> getStringSet(String key, Set<String> defValue) { throw new RuntimeException("getStringSet not available."); } @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { config.getPrefs().registerOnSharedPreferenceChangeListener(listener); }
--- a/mobile/android/base/sync/net/BaseResource.java +++ b/mobile/android/base/sync/net/BaseResource.java @@ -60,17 +60,17 @@ import ch.boye.httpclientandroidlib.util public class BaseResource implements Resource { private static final String ANDROID_LOOPBACK_IP = "10.0.2.2"; private static final int MAX_TOTAL_CONNECTIONS = 20; private static final int MAX_CONNECTIONS_PER_ROUTE = 10; private boolean retryOnFailedRequest = true; - public static final boolean rewriteLocalhost = true; + public static boolean rewriteLocalhost = true; private static final String LOG_TAG = "BaseResource"; protected final URI uri; protected BasicHttpContext context; protected DefaultHttpClient client; public ResourceDelegate delegate; protected HttpRequestBase request;
--- a/mobile/android/base/tests/JavascriptTest.java +++ b/mobile/android/base/tests/JavascriptTest.java @@ -6,16 +6,21 @@ import org.mozilla.gecko.tests.helpers.J import org.mozilla.gecko.tests.helpers.JavascriptMessageParser; import android.util.Log; public class JavascriptTest extends BaseTest { private static final String LOGTAG = "JavascriptTest"; private static final String EVENT_TYPE = JavascriptBridge.EVENT_TYPE; + // Calculate these once, at initialization. isLoggable is too expensive to + // have in-line in each log call. + private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); + private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); + private final String javascriptUrl; public JavascriptTest(String javascriptUrl) { super(); this.javascriptUrl = javascriptUrl; } public void testJavascript() throws Exception { @@ -34,38 +39,38 @@ public class JavascriptTest extends Base final String url = getAbsoluteUrl(StringHelper.getHarnessUrlForJavascript(javascriptUrl)); mAsserter.dumpLog("Loading JavaScript test from " + url); loadUrl(url); final JavascriptMessageParser testMessageParser = new JavascriptMessageParser(mAsserter, false); try { while (!testMessageParser.isTestFinished()) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Waiting for " + EVENT_TYPE); } String data = expecter.blockForEventData(); - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got event with data '" + data + "'"); } JSONObject o = new JSONObject(data); String innerType = o.getString("innerType"); if (!"progress".equals(innerType)) { throw new Exception("Unexpected event innerType " + innerType); } String message = o.getString("message"); if (message == null) { throw new Exception("Progress message must not be null"); } testMessageParser.logMessage(message); } - if (Log.isLoggable(LOGTAG, Log.DEBUG)) { + if (logDebug) { Log.d(LOGTAG, "Got test finished message"); } } finally { expecter.unregisterListener(); mAsserter.dumpLog("Unregistered listener for " + EVENT_TYPE); } } }
--- a/mobile/android/base/util/FileUtils.java +++ b/mobile/android/base/util/FileUtils.java @@ -4,16 +4,17 @@ package org.mozilla.gecko.util; import android.util.Log; import java.io.File; import java.io.IOException; import java.io.FilenameFilter; +import java.util.Scanner; import org.mozilla.gecko.mozglue.RobocopTarget; public class FileUtils { private static final String LOGTAG= "GeckoFileUtils"; /* * A basic Filter for checking a filename and age. **/ @@ -76,9 +77,22 @@ public class FileUtils { Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex); } } } // Even if this is a dir, it should now be empty and delete should work return file.delete(); } + + // Shortcut to slurp a file without messing around with streams. + public static String getFileContents(File file) throws IOException { + Scanner scanner = null; + try { + scanner = new Scanner(file, "UTF-8"); + return scanner.useDelimiter("\\A").next(); + } finally { + if (scanner != null) { + scanner.close(); + } + } + } }
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6902,17 +6902,17 @@ OverscrollController.prototype = { }; var SearchEngines = { _contextMenuId: null, PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled", PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted", // Shared preference key used for search activity default engine. - PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.default", + PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname", init: function init() { Services.obs.addObserver(this, "SearchEngines:Add", false); Services.obs.addObserver(this, "SearchEngines:GetVisible", false); Services.obs.addObserver(this, "SearchEngines:Remove", false); Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false); Services.obs.addObserver(this, "SearchEngines:SetDefault", false); Services.obs.addObserver(this, "browser-search-engine-modified", false); @@ -7042,44 +7042,17 @@ var SearchEngines = { }, migrateSearchActivityDefaultPref: function migrateSearchActivityDefaultPref() { Services.search.init(() => this._setSearchActivityDefaultPref(Services.search.defaultEngine)); }, // Updates the search activity pref when the default engine changes. _setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) { - // Helper function copied from nsSearchService.js. This is the logic that is used - // to create file names for search plugin XML serialized to disk. - function sanitizeName(aName) { - const maxLength = 60; - const minLength = 1; - let name = aName.toLowerCase(); - name = name.replace(/\s+/g, "-"); - name = name.replace(/[^-a-z0-9]/g, ""); - - if (name.length < minLength) { - // Well, in this case, we're kinda screwed. In this case, the search service - // generates a random file name, so to do this the right way, we'd need - // to open up search.json and see what file name is stored. - Cu.reportError("Couldn't create search plugin file name from engine name: " + aName); - return null; - } - - // Force max length. - return name.substring(0, maxLength); - } - - let identifier = engine.identifier; - if (identifier === null) { - // The identifier will be null for non-built-in engines. In this case, we need to - // figure out an identifier to store from the engine name. - identifier = sanitizeName(engine.name); - } - SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, identifier); + SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name); }, // Display context menu listing names of the search engines available to be added. displaySearchEnginesList: function displaySearchEnginesList(aData) { let data = JSON.parse(aData); let tab = BrowserApp.getTabForId(data.tabId); if (!tab)
--- a/mobile/android/config/mozconfigs/common +++ b/mobile/android/config/mozconfigs/common @@ -10,17 +10,17 @@ # much slower and we didn't want to slow down developers builds. # Has no effect when MOZ_ENABLE_SZIP is not set in mobile/android/confvars.sh. MOZ_SZIP_FLAGS="-D auto -f auto" ac_add_options --enable-elf-hack ANDROID_NDK_VERSION="r8e" ANDROID_NDK_VERSION_32BIT="r8c" -ANDROID_SDK_VERSION="20" +ANDROID_SDK_VERSION="21" # Build Fennec ac_add_options --enable-application=mobile/android if test `uname -m` = 'x86_64'; then ac_add_options --with-android-ndk="$topsrcdir/android-ndk" ac_add_options --with-android-sdk="$topsrcdir/android-sdk-linux/platforms/android-$ANDROID_SDK_VERSION" else
--- a/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest @@ -1,18 +1,18 @@ [ { "size": 78706854, "digest": "8ff42509ecebfd7e20f8fac9987ed2b2c04942641eead674ee66f74014c5153f1c20080cd3ccb243af76ca7432df3c3f5b5ae08a478fd2817e62661a4edb437c", "algorithm": "sha512", "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, { "size": 336, "digest": "3336af2e7106654be09ae10b19f981162584ede0888abe295c45fe6e10f52d460a0a94b8bef0518f1419fcd82939b723123e122897599edce9cc240757424890", "algorithm": "sha512", "filename": "setup.sh"
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest @@ -1,18 +1,18 @@ [ { "size": 80729933, "digest": "ccd13527c0ba3979f4030eae713e6529e916a701b9b16a371256d92618e914fb0fd2ba70547efca0e93d02010c89eeb7c222d40a9b6f26fab80911386cdfecc7", "algorithm": "sha512", "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, { "size": 336, "digest": "3336af2e7106654be09ae10b19f981162584ede0888abe295c45fe6e10f52d460a0a94b8bef0518f1419fcd82939b723123e122897599edce9cc240757424890", "algorithm": "sha512", "filename": "setup.sh"
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android/releng.manifest @@ -1,18 +1,18 @@ [ { "size": 78706854, "digest": "8ff42509ecebfd7e20f8fac9987ed2b2c04942641eead674ee66f74014c5153f1c20080cd3ccb243af76ca7432df3c3f5b5ae08a478fd2817e62661a4edb437c", "algorithm": "sha512", "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, { "size": 336, "digest": "3336af2e7106654be09ae10b19f981162584ede0888abe295c45fe6e10f52d460a0a94b8bef0518f1419fcd82939b723123e122897599edce9cc240757424890", "algorithm": "sha512", "filename": "setup.sh"
--- a/mobile/android/gradle/Makefile.in +++ b/mobile/android/gradle/Makefile.in @@ -1,19 +1,21 @@ # 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/. gradle := \ + local.properties.in \ gradle.properties.in \ $(NULL) gradle_PATH := $(CURDIR) gradle_FLAGS += -Dtopsrcdir=$(abspath $(topsrcdir)) gradle_FLAGS += -Dtopobjdir=$(abspath $(DEPTH)) +gradle_FLAGS += -DANDROID_SDK_ROOT=$(ANDROID_SDK_ROOT) gradle_KEEP_PATH := 1 PP_TARGETS += gradle wrapper_FILES := \ build.gradle \ settings.gradle \ gradle/wrapper/gradle-wrapper.jar \ gradle/wrapper/gradle-wrapper.properties \
--- a/mobile/android/gradle/build.gradle +++ b/mobile/android/gradle/build.gradle @@ -9,15 +9,15 @@ ext { } buildscript { repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:0.14.1' } } repositories { jcenter() }
index 8c0fb64a8698b08ecc4158d828ca593c4928e9dd..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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
--- a/mobile/android/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Thu Nov 13 14:57:51 PST 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip
new file mode 100644 --- /dev/null +++ b/mobile/android/gradle/local.properties.in @@ -0,0 +1,2 @@ +#filter substitution +sdk.dir=@ANDROID_SDK_ROOT@
--- a/mobile/android/search/java/org/mozilla/search/Constants.java +++ b/mobile/android/search/java/org/mozilla/search/Constants.java @@ -12,14 +12,9 @@ package org.mozilla.search; /** * Key should not be stored here. For more info on storing keys, see * https://github.com/ericedens/FirefoxSearch/issues/3 */ public class Constants { public static final String ABOUT_BLANK = "about:blank"; - - // TODO: Localize this with region.properties (or a similar solution). See bug 1065306. - public static final String DEFAULT_ENGINE_IDENTIFIER = "yahoo"; - - public static final String PREF_SEARCH_ENGINE_KEY = "search.engines.default"; }
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java @@ -5,16 +5,17 @@ package org.mozilla.search; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.LocaleAware; import org.mozilla.gecko.R; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.BrowserContract.SearchHistory; +import org.mozilla.gecko.distribution.Distribution; import org.mozilla.gecko.health.BrowserHealthRecorder; import org.mozilla.search.autocomplete.SearchBar; import org.mozilla.search.autocomplete.SuggestionsFragment; import org.mozilla.search.providers.SearchEngine; import org.mozilla.search.providers.SearchEngineManager; import org.mozilla.search.providers.SearchEngineManager.SearchEngineCallback; import android.content.AsyncQueryHandler; @@ -96,17 +97,17 @@ public class SearchActivity extends Loca GeckoAppShell.ensureCrashHandling(); super.onCreate(savedInstanceState); setContentView(R.layout.search_activity_main); suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions); postSearchFragment = (PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch); - searchEngineManager = new SearchEngineManager(this); + searchEngineManager = new SearchEngineManager(this, Distribution.init(this)); searchEngineManager.setChangeCallback(this); // Initialize the fragments with the selected search engine. searchEngineManager.getEngine(this); queryHandler = new AsyncQueryHandler(getContentResolver()) {}; searchBar = (SearchBar) findViewById(R.id.search_bar); @@ -274,30 +275,37 @@ public class SearchActivity extends Loca return; } // engine will only be null if startSearch is called before the getEngine // call in onCreate is completed. searchEngineManager.getEngine(new SearchEngineCallback() { @Override public void execute(SearchEngine engine) { - postSearchFragment.startSearch(engine, query); + // TODO: If engine is null, we should show an error message. + if (engine != null) { + postSearchFragment.startSearch(engine, query); + } } }); } /** * This method is called when we fetch the current engine in onCreate, * as well as whenever the current engine changes. This method will only * ever be called on the main thread. * * @param engine The current search engine. */ @Override public void execute(SearchEngine engine) { + // TODO: If engine is null, we should show an error message. + if (engine == null) { + return; + } this.engine = engine; suggestionsFragment.setEngine(engine); searchBar.setEngine(engine); } /** * Animates search suggestion item to fill the results view area. *
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java @@ -47,17 +47,19 @@ public class SearchEngine { // head of a web page. The actual CSS is inserted at `%s`. private static final String STYLE_INJECTION_SCRIPT = "javascript:(function(){" + "var tag=document.createElement('style');" + "tag.type='text/css';" + "document.getElementsByTagName('head')[0].appendChild(tag);" + "tag.innerText='%s'})();"; + // The Gecko search identifier. This will be null for engines that don't ship with the locale. private final String identifier; + private String shortName; private String iconURL; // Ordered list of preferred results URIs. private final List<Uri> resultsUris = new ArrayList<Uri>(); private Uri suggestUri; /** @@ -184,17 +186,19 @@ public class SearchEngine { * HACKS! We'll need to replace this with endpoints that return the correct content. * * Retrieve a JS snippet, in bookmarklet style, that can be used * to modify the results page. */ public String getInjectableJs() { final String css; - if (identifier.equals("bing")) { + if (identifier == null) { + css = ""; + } else if (identifier.equals("bing")) { css = "#mHeader{display:none}#contentWrapper{margin-top:0}"; } else if (identifier.equals("google")) { css = "#sfcnt,#top_nav{display:none}"; } else if (identifier.equals("yahoo")) { css = "#nav,#header{display:none}"; } else { css = ""; } @@ -242,31 +246,31 @@ public class SearchEngine { /** * Create a uri string that can be used to fetch the results page. * * @param query The user's query. This method will escape and encode the query. */ public String resultsUriForQuery(String query) { final Uri resultsUri = getResultsUri(); if (resultsUri == null) { - Log.e(LOG_TAG, "No results URL for search engine: " + identifier); + Log.e(LOG_TAG, "No results URL for search engine: " + shortName); return ""; } final String template = Uri.decode(resultsUri.toString()); return paramSubstitution(template, Uri.encode(query)); } /** * Create a uri string to fetch autocomplete suggestions. * * @param query The user's query. This method will escape and encode the query. */ public String getSuggestionTemplate(String query) { if (suggestUri == null) { - Log.e(LOG_TAG, "No suggestions template for search engine: " + identifier); + Log.e(LOG_TAG, "No suggestions template for search engine: " + shortName); return ""; } final String template = Uri.decode(suggestUri.toString()); return paramSubstitution(template, Uri.encode(query)); } /** * @return Preferred results URI.
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java @@ -1,211 +1,420 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.search.providers; import android.content.Context; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.FileUtils; import org.mozilla.gecko.util.GeckoJarReader; +import org.mozilla.gecko.util.RawResource; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.distribution.Distribution; import org.mozilla.search.Constants; import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String LOG_TAG = "SearchEngineManager"; + private static final String LOG_TAG = "GeckoSearchEngineManager"; + + // Gecko pref that defines the name of the default search engine. + private static final String PREF_GECKO_DEFAULT_ENGINE = "browser.search.defaultenginename"; + + // Key for shared preference that stores default engine name. + private static final String PREF_DEFAULT_ENGINE_KEY = "search.engines.defaultname"; private Context context; + private Distribution distribution; private SearchEngineCallback changeCallback; private SearchEngine engine; public static interface SearchEngineCallback { public void execute(SearchEngine engine); } - public SearchEngineManager(Context context) { + public SearchEngineManager(Context context, Distribution distribution) { this.context = context; + this.distribution = distribution; GeckoSharedPrefs.forApp(context).registerOnSharedPreferenceChangeListener(this); } + /** + * Sets a callback to be called when the default engine changes. + * + * @param callback SearchEngineCallback to be called after the search engine + * changed. This will run on the UI thread. + * Note: callback may be called with null engine. + */ public void setChangeCallback(SearchEngineCallback changeCallback) { this.changeCallback = changeCallback; } /** * Perform an action with the user's default search engine. * * @param callback The callback to be used with the user's default search engine. The call * may be sync or async; if the call is async, it will be called on the * ui thread. */ public void getEngine(SearchEngineCallback callback) { if (engine != null) { callback.execute(engine); } else { - getEngineFromPrefs(callback); + getDefaultEngine(callback); } } public void destroy() { GeckoSharedPrefs.forApp(context).unregisterOnSharedPreferenceChangeListener(this); context = null; + distribution = null; changeCallback = null; engine = null; } + private int ignorePreferenceChange = 0; + @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - if (!TextUtils.equals(Constants.PREF_SEARCH_ENGINE_KEY, key)) { + if (!TextUtils.equals(PREF_DEFAULT_ENGINE_KEY, key)) { + return; + } + + if (ignorePreferenceChange > 0) { + ignorePreferenceChange--; return; } - getEngineFromPrefs(changeCallback); + + getDefaultEngine(changeCallback); + } + + /** + * Runs a SearchEngineCallback on the main thread. + */ + private void runCallback(final SearchEngine engine, final SearchEngineCallback callback) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + // Cache engine for future calls to getEngine. + SearchEngineManager.this.engine = engine; + callback.execute(engine); + } + }); + } + + /** + * This method finds and creates the default search engine. It will first look for + * the default engine name, then create the engine from that name. + * + * To find the default engine name, we first look in shared preferences, then + * the distribution (if one exists), and finally fall back to the localized default. + * + * @param callback SearchEngineCallback to be called after successfully looking + * up the search engine. This will run on the UI thread. + * Note: callback may be called with null engine. + */ + private void getDefaultEngine(final SearchEngineCallback callback) { + // This runnable is posted to the background thread. + distribution.addOnDistributionReadyCallback(new Runnable() { + @Override + public void run() { + // First look for a default name stored in shared preferences. + String name = GeckoSharedPrefs.forApp(context).getString(PREF_DEFAULT_ENGINE_KEY, null); + + if (name != null) { + Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name); + } else { + // First, look for the default search engine in a distribution. + name = getDefaultEngineNameFromDistribution(); + if (name == null) { + // Otherwise, get the default engine that we ship. + name = getDefaultEngineNameFromLocale(); + } + + // Store the default engine name for the future. + // Increment an 'ignore' counter so that this preference change + // won'tcause getDefaultEngine to be called again. + ignorePreferenceChange++; + GeckoSharedPrefs.forApp(context) + .edit() + .putString(PREF_DEFAULT_ENGINE_KEY, name) + .apply(); + } + + final SearchEngine engine = createEngineFromName(name); + runCallback(engine, callback); + } + }); } /** - * Look up the current search engine in shared preferences. - * Creates a SearchEngine instance and caches it for use on the main thread. + * Looks for a default search engine included in a distribution. + * This method must be called after the distribution is ready. * - * @param callback a SearchEngineCallback to be called after successfully looking - * up the search engine. This will run on the UI thread. + * @return search engine name. */ - private void getEngineFromPrefs(final SearchEngineCallback callback) { - final AsyncTask<Void, Void, SearchEngine> task = new AsyncTask<Void, Void, SearchEngine>() { - @Override - protected SearchEngine doInBackground(Void... params) { - String identifier = GeckoSharedPrefs.forApp(context).getString(Constants.PREF_SEARCH_ENGINE_KEY, null); - if (!TextUtils.isEmpty(identifier)) { - try { - return createEngine(identifier); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "Exception creating search engine from pref. Falling back to default engine.", e); - } + private String getDefaultEngineNameFromDistribution() { + if (!distribution.exists()) { + return null; + } + + final File prefFile = distribution.getDistributionFile("preferences.json"); + if (prefFile == null) { + return null; + } + + try { + final JSONObject all = new JSONObject(FileUtils.getFileContents(prefFile)); + + // First, check to see if there's a locale-specific override. + final String languageTag = BrowserLocaleManager.getLanguageTag(Locale.getDefault()); + final String overridesKey = "LocalizablePreferences." + languageTag; + if (all.has(overridesKey)) { + final JSONObject overridePrefs = all.getJSONObject(overridesKey); + if (overridePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) { + Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences override."); + return overridePrefs.getString(PREF_GECKO_DEFAULT_ENGINE); } - - try { - return createEngine(Constants.DEFAULT_ENGINE_IDENTIFIER); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "Exception creating search engine from default identifier. " + - "This will happen if the locale doesn't contain the default search plugin.", e); - } - - return null; } - @Override - protected void onPostExecute(SearchEngine engine) { - if (engine != null) { - // Only touch engine on the main thread. - SearchEngineManager.this.engine = engine; - if (callback != null) { - callback.execute(engine); - } + // Next, check to see if there's a non-override default pref. + if (all.has("LocalizablePreferences")) { + final JSONObject localizablePrefs = all.getJSONObject("LocalizablePreferences"); + if (localizablePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) { + Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences."); + return localizablePrefs.getString(PREF_GECKO_DEFAULT_ENGINE); } } - }; - task.execute(); + } catch (IOException e) { + Log.e(LOG_TAG, "Error getting search engine name from preferences.json", e); + } catch (JSONException e) { + Log.e(LOG_TAG, "Error parsing preferences.json", e); + } + return null; + } + + /** + * Looks for the default search engine shipped in the locale. + * + * @return search engine name. + */ + private String getDefaultEngineNameFromLocale() { + try { + final JSONObject browsersearch = new JSONObject(RawResource.getAsString(context, R.raw.browsersearch)); + if (browsersearch.has("default")) { + Log.d(LOG_TAG, "Found default engine name in browsersearch.json."); + return browsersearch.getString("default"); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Error getting search engine name from browsersearch.json", e); + } catch (JSONException e) { + Log.e(LOG_TAG, "Error parsing browsersearch.json", e); + } + return null; } /** - * Creates a list of SearchEngine instances from all available open search plugins. - * This method does disk I/O, call it from a background thread. + * Creates a SearchEngine instance from an engine name. * - * @return List of SearchEngine instances + * To create the engine, we first try to find the search plugin in the distribution + * (if one exists), followed by the localized plugins we ship with the browser, and + * then finally third-party plugins that are installed in the profile directory. + * + * This method must be called after the distribution is ready. + * + * @param name The search engine name (e.g. "Google" or "Amazon.com") + * @return SearchEngine instance for name. */ - public List<SearchEngine> getAllEngines() { - // First try to read the engine list from the jar. - InputStream in = getInputStreamFromJar("list.txt"); + private SearchEngine createEngineFromName(String name) { + // First, look in the distribution. + SearchEngine engine = createEngineFromDistribution(name); + + // Second, look in the jar for plugins shipped with the locale. + if (engine == null) { + engine = createEngineFromLocale(name); + } + + // Finally, look in the profile for third-party plugins. + if (engine == null) { + engine = createEngineFromProfile(name); + } + + if (engine == null) { + Log.e(LOG_TAG, "Could not create search engine from name: " + name); + } + + return engine; + } - final List<SearchEngine> list = new ArrayList<SearchEngine>(); + /** + * Creates a SearchEngine instance for a distribution search plugin. + * + * This method iterates through the distribution searchplugins directory, + * creating SearchEngine instances until it finds one with the right name. + * + * This method must be called after the distribution is ready. + * + * @param name Search engine name. + * @return SearchEngine instance for name. + */ + private SearchEngine createEngineFromDistribution(String name) { + if (!distribution.exists()) { + return null; + } + + final File pluginsDir = distribution.getDistributionFile("searchplugins"); + if (pluginsDir == null) { + return null; + } + + final File[] files = (new File(pluginsDir, "common")).listFiles(); + return createEngineFromFileList(files, name); + } + + /** + * Creates a SearchEngine instance for a search plugin shipped in the locale. + * + * This method reads the list of search plugin file names from list.txt, then + * iterates through the files, creating SearchEngine instances until it finds one + * with the right name. Unfortunately, we need to do this because there is no + * other way to map the search engine "name" to the file for the search plugin. + * + * @param name Search engine name. + * @return SearchEngine instance for name. + */ + private SearchEngine createEngineFromLocale(String name) { + final InputStream in = getInputStreamFromSearchPluginsJar("list.txt"); InputStreamReader isr = null; try { isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); String identifier; while ((identifier = br.readLine()) != null) { - list.add(createEngine(identifier)); + final InputStream pluginIn = getInputStreamFromSearchPluginsJar(identifier + ".xml"); + final SearchEngine engine = createEngineFromInputStream(identifier, pluginIn); + if (engine != null && engine.getName().equals(name)) { + return engine; + } } } catch (IOException e) { - throw new IllegalStateException("Error creating all search engines from list.txt"); + Log.e(LOG_TAG, "Error creating shipped search engine from name: " + name, e); } finally { if (isr != null) { try { isr.close(); } catch (IOException e) { // Ignore. } } try { in.close(); } catch (IOException e) { // Ignore. } } - return list; + return null; + } + + /** + * Creates a SearchEngine instance for a search plugin in the profile directory. + * + * This method iterates through the profile searchplugins directory, creating + * SearchEngine instances until it finds one with the right name. + * + * @param name Search engine name. + * @return SearchEngine instance for name. + */ + private SearchEngine createEngineFromProfile(String name) { + final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins"); + if (pluginsDir == null) { + return null; + } + + final File[] files = pluginsDir.listFiles(); + return createEngineFromFileList(files, name); } /** - * Creates a SearchEngine instance from an open search plugin. - * This method does disk I/O, call it from a background thread. + * This method iterates through an array of search plugin files, creating + * SearchEngine instances until it finds one with the right name. * - * @param identifier search engine identifier (e.g. "google") - * @return SearchEngine instance for identifier + * @param files Array of search plugin files. + * @param name Search engine name. + * @return SearchEngine instance for name. */ - private SearchEngine createEngine(String identifier) { - InputStream in = getInputStreamFromJar(identifier + ".xml"); + private SearchEngine createEngineFromFileList(File[] files, String name) { + for (int i = 0; i < files.length; i++) { + try { + final FileInputStream fis = new FileInputStream(files[i]); + final SearchEngine engine = createEngineFromInputStream(null, fis); + if (engine != null && engine.getName().equals(name)) { + return engine; + } + } catch (IOException e) { + Log.e(LOG_TAG, "Error creating earch engine from name: " + name, e); + } + } + return null; + } - if (in == null) { - in = getEngineFromProfile(identifier); - } - - if (in == null) { - throw new IllegalArgumentException("Couldn't find search engine for identifier: " + identifier); - } - + /** + * Creates a SearchEngine instance from an InputStream. + * + * This method closes the stream after it is done reading it. + * + * @param identifier Seach engine identifier. This only exists for search engines that + * ship with the default set of engines in the locale. + * @param in InputStream for search plugin XML file. + * @return SearchEngine instance. + */ + private SearchEngine createEngineFromInputStream(String identifier, InputStream in) { try { try { return new SearchEngine(identifier, in); } finally { in.close(); } } catch (IOException | XmlPullParserException e) { Log.e(LOG_TAG, "Exception creating search engine", e); } return null; } /** - * Reads a file from the searchplugins directory in the Gecko jar. This will only work - * if the search activity is built as part of mozilla-central. + * Reads a file from the searchplugins directory in the Gecko jar. * - * @param fileName name of the file to read - * @return InputStream for file + * @param fileName name of the file to read. + * @return InputStream for file. */ - private InputStream getInputStreamFromJar(String fileName) { + private InputStream getInputStreamFromSearchPluginsJar(String fileName) { final Locale locale = Locale.getDefault(); // First, try a file path for the full locale. final String languageTag = BrowserLocaleManager.getLanguageTag(locale); String url = getSearchPluginsJarURL(languageTag, fileName); InputStream in = GeckoJarReader.getStream(url); if (in != null) { @@ -223,37 +432,19 @@ public class SearchEngineManager impleme } // Finally, fall back to en-US. url = getSearchPluginsJarURL("en-US", fileName); return GeckoJarReader.getStream(url); } /** - * Gets the jar URL for a file in the searchplugins directory + * Gets the jar URL for a file in the searchplugins directory. * - * @param locale String representing the Gecko locale (e.g. "en-US") - * @param fileName name of the file to read - * @return URL for jar file + * @param locale String representing the Gecko locale (e.g. "en-US"). + * @param fileName The name of the file to read. + * @return URL for jar file. */ private String getSearchPluginsJarURL(String locale, String fileName) { final String path = "!/chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName; return "jar:jar:file://" + context.getPackageResourcePath() + "!/" + AppConstants.OMNIJAR_NAME + path; } - - /** - * Opens the search plugin XML file from the searchplugins directory in the Gecko profile. - * - * @param identifier - * @return InputStream for search plugin file - */ - private InputStream getEngineFromProfile(String identifier) { - final File f = GeckoProfile.get(context).getFile("searchplugins/" + identifier + ".xml"); - if (f.exists()) { - try { - return new FileInputStream(f); - } catch (FileNotFoundException e) { - Log.e(LOG_TAG, "Exception getting search engine from profile", e); - } - } - return null; - } }
--- a/mobile/android/services/strings.xml.in +++ b/mobile/android/services/strings.xml.in @@ -170,16 +170,19 @@ <string name="fxaccount_update_credentials_header">&fxaccount_update_credentials_header;</string> <string name="fxaccount_update_credentials_button_label">&fxaccount_update_credentials_button_label;</string> <string name="fxaccount_update_credentials_unknown_error">&fxaccount_update_credentials_unknown_error;</string> <string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string> <string name="fxaccount_status_header">&fxaccount_status_header2;</string> <string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string> <string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string> +<string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string> +<string name="fxaccount_status_syncing">&fxaccount_status_syncing;</string> +<string name="fxaccount_status_last_synced">&remote_tabs_last_synced;</string> <string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string> <string name="fxaccount_status_sync_server">&fxaccount_status_sync_server;</string> <string name="fxaccount_status_sync">&fxaccount_status_sync;</string> <string name="fxaccount_status_sync_enabled">&fxaccount_status_sync_enabled;</string> <string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string> <string name="fxaccount_status_needs_credentials">&fxaccount_status_needs_credentials;</string> <string name="fxaccount_status_needs_upgrade">&fxaccount_status_needs_upgrade;</string> <string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string> @@ -211,11 +214,8 @@ <string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string> <string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string> <!-- Remove Account --> <string name="fxaccount_remove_account_dialog_title">&fxaccount_remove_account_dialog_title;</string> <string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string> <string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string> <string name="fxaccount_remove_account_menu_item">&fxaccount_remove_account_menu_item;</string> - -<!-- Find-In-Page strings --> -<string name="find_matchcase">&find_matchcase;</string>
--- a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java +++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java @@ -269,17 +269,19 @@ public class Hex implements BinaryEncode * @throws EncoderException * Thrown if the given object is not a String or byte[] * @see #encodeHex(byte[]) */ public Object encode(Object object) throws EncoderException { try { byte[] byteArray = object instanceof String ? ((String) object).getBytes(getCharsetName()) : (byte[]) object; return encodeHex(byteArray); - } catch (ClassCastException | UnsupportedEncodingException e) { + } catch (ClassCastException e) { + throw new EncoderException(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { throw new EncoderException(e.getMessage(), e); } } /** * Gets the charset name. * * @return the charset name.
--- a/python/mozbuild/mozbuild/frontend/context.py +++ b/python/mozbuild/mozbuild/frontend/context.py @@ -496,16 +496,32 @@ VARIABLES = { and read the frontend file there. If there is no frontend file, an error is raised. Values are relative paths. They can be multiple directory levels above or below. Use ``..`` for parent directories and ``/`` for path delimiters. """, None), + 'HAS_MISC_RULE': (bool, bool, + """Whether this directory should be traversed in the ``misc`` tier. + + Many ``libs`` rules still exist in Makefile.in files. We highly prefer + that these rules exist in the ``misc`` tier/target so that they can be + executed concurrently during tier traversal (the ``misc`` tier is + fully concurrent). + + Presence of this variable indicates that this directory should be + traversed by the ``misc`` tier. + + Please note that converting ``libs`` rules to the ``misc`` tier must + be done with care, as there are many implicit dependencies that can + break the build in subtle ways. + """, 'misc'), + 'FINAL_TARGET_FILES': (HierarchicalStringList, list, """List of files to be installed into the application directory. ``FINAL_TARGET_FILES`` will copy (or symlink, if the platform supports it) the contents of its files to the directory specified by ``FINAL_TARGET`` (typically ``dist/bin``). Files that are destined for a subdirectory can be specified by accessing a field, or as a dict access. For example, to export ``foo.png`` to the top-level directory and
--- a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build @@ -0,0 +1,1 @@ +HAS_MISC_RULE = True
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py +++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py @@ -93,16 +93,18 @@ class TestEmitterBasic(unittest.TestCase self.assertIsInstance(o, DirectoryTraversal) self.assertEqual(o.test_dirs, []) self.assertTrue(os.path.isabs(o.context_main_path)) self.assertEqual(len(o.context_all_paths), 1) reldirs = [o.relativedir for o in objs] self.assertEqual(reldirs, ['', 'foo', 'foo/biz', 'bar']) + self.assertEqual(objs[3].affected_tiers, {'misc'}) + dirs = [o.dirs for o in objs] self.assertEqual(dirs, [ [ mozpath.join(reader.config.topsrcdir, 'foo'), mozpath.join(reader.config.topsrcdir, 'bar') ], [ mozpath.join(reader.config.topsrcdir, 'foo', 'biz') ], [], []])