diff --git a/CLOBBER b/CLOBBER index 3688cb19466b..a7f86f6bbbce 100644 --- a/CLOBBER +++ b/CLOBBER @@ -22,4 +22,4 @@ # 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. diff --git a/browser/components/loop/content/shared/css/conversation.css b/browser/components/loop/content/shared/css/conversation.css index b5b5dab0eb98..f48f534fd58b 100644 --- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -697,6 +697,38 @@ html, .fx-embedded, #main, 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. */ @@ -745,7 +777,7 @@ html, .fx-embedded, #main, .standalone .room-inner-info-area { position: absolute; - top: 35%; + top: 50%; left: 0; right: 25%; z-index: 1000; @@ -767,6 +799,7 @@ html, .fx-embedded, #main, padding: .5em 3em .3em 3em; border-radius: 3px; font-weight: normal; + max-width: 400px; } .standalone .room-conversation h2.room-name { @@ -796,7 +829,7 @@ html, .fx-embedded, #main, .standalone .room-conversation .conversation-toolbar { background: #000; - border-top: none; + border: none; } .standalone .room-conversation .conversation-toolbar .btn-hangup-entry { diff --git a/browser/components/loop/content/shared/js/activeRoomStore.js b/browser/components/loop/content/shared/js/activeRoomStore.js index 1e074b18cb23..9af874261b19 100644 --- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -10,6 +10,15 @@ 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 @@ -84,7 +93,8 @@ loop.store.ActiveRoomStore = (function() { this._storeState = { roomState: ROOM_STATES.INIT, audioMuted: false, - videoMuted: false + videoMuted: false, + failureReason: undefined }; } @@ -112,13 +122,24 @@ loop.store.ActiveRoomStore = (function() { * @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 }); }, @@ -228,6 +249,11 @@ loop.store.ActiveRoomStore = (function() { * 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) { @@ -275,11 +301,17 @@ loop.store.ActiveRoomStore = (function() { /** * 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); }, diff --git a/browser/components/loop/content/shared/js/otSdkDriver.js b/browser/components/loop/content/shared/js/otSdkDriver.js index 5811d697788e..8a6b87fbe80a 100644 --- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -8,6 +8,7 @@ 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 @@ -47,8 +48,11 @@ loop.OTSdkDriver = (function() { // 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)); }, /** @@ -96,16 +100,12 @@ loop.OTSdkDriver = (function() { */ 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; } @@ -126,7 +126,7 @@ loop.OTSdkDriver = (function() { if (error) { console.error("Failed to complete connection", error); this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "couldNotConnect" + reason: FAILURE_REASONS.COULD_NOT_CONNECT })); return; } @@ -159,7 +159,7 @@ loop.OTSdkDriver = (function() { // We only need to worry about the network disconnected reason here. if (event.reason === "networkDisconnected") { this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "networkDisconnected" + reason: FAILURE_REASONS.NETWORK_DISCONNECTED })); } }, @@ -188,24 +188,42 @@ loop.OTSdkDriver = (function() { } }, + /** + * 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. diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js index c33975b1d099..81f2acf876ac 100644 --- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -17,6 +17,14 @@ loop.shared.utils = (function(mozL10n) { 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. * @@ -110,6 +118,7 @@ loop.shared.utils = (function(mozL10n) { return { CALL_TYPES: CALL_TYPES, + FAILURE_REASONS: FAILURE_REASONS, Helper: Helper, composeCallUrlEmail: composeCallUrlEmail, formatDate: formatDate, diff --git a/browser/components/loop/standalone/content/css/webapp.css b/browser/components/loop/standalone/content/css/webapp.css index b2698f0c4158..26954f70d396 100644 --- a/browser/components/loop/standalone/content/css/webapp.css +++ b/browser/components/loop/standalone/content/css/webapp.css @@ -18,6 +18,13 @@ body, 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; diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index d182bdc37854..52072680dda6 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -11,8 +11,10 @@ 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', @@ -39,6 +41,20 @@ loop.standaloneRoomViews = (function(mozL10n) { ); }, + /** + * @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: @@ -67,6 +83,12 @@ loop.standaloneRoomViews = (function(mozL10n) { React.DOM.p(null, this._renderCallToActionLink()) ) ); + case ROOM_STATES.FAILED: + return ( + React.DOM.p({className: "failed-room-message"}, + this._getFailureString() + ) + ); default: return null; } @@ -81,6 +103,43 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + 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], @@ -144,6 +203,11 @@ loop.standaloneRoomViews = (function(mozL10n) { }; }, + 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); }, @@ -207,7 +271,9 @@ loop.standaloneRoomViews = (function(mozL10n) { 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"}, @@ -229,7 +295,8 @@ loop.standaloneRoomViews = (function(mozL10n) { hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), enableHangup: this._roomIsActive()}) ) - ) + ), + StandaloneRoomFooter(null) ) ); } diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index dd40191bb432..965306a156ab 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -11,8 +11,10 @@ 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({ @@ -39,6 +41,20 @@ loop.standaloneRoomViews = (function(mozL10n) { ); }, + /** + * @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: @@ -67,6 +83,12 @@ loop.standaloneRoomViews = (function(mozL10n) {

{this._renderCallToActionLink()}

); + case ROOM_STATES.FAILED: + return ( +

+ {this._getFailureString()} +

+ ); default: return null; } @@ -81,6 +103,43 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + var StandaloneRoomHeader = React.createClass({ + render: function() { + return ( +
+

{mozL10n.get("clientShortname2")}

+
+ ); + } + }); + + var StandaloneRoomFooter = React.createClass({ + _getContent: function() { + return mozL10n.get("legal_text_and_links", { + "clientShortname": mozL10n.get("clientShortname2"), + "terms_of_use_url": React.renderComponentToStaticMarkup( + + {mozL10n.get("terms_of_use_link_text")} + + ), + "privacy_notice_url": React.renderComponentToStaticMarkup( + + {mozL10n.get("privacy_notice_link_text")} + + ), + }); + }, + + render: function() { + return ( + + ); + } + }); + var StandaloneRoomView = React.createClass({ mixins: [Backbone.Events], @@ -144,6 +203,11 @@ loop.standaloneRoomViews = (function(mozL10n) { }; }, + 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); }, @@ -207,7 +271,9 @@ loop.standaloneRoomViews = (function(mozL10n) { return (
+
@@ -230,6 +296,7 @@ loop.standaloneRoomViews = (function(mozL10n) { enableHangup={this._roomIsActive()} />
+ ); } diff --git a/browser/components/loop/standalone/content/l10n/loop.en-US.properties b/browser/components/loop/standalone/content/l10n/loop.en-US.properties index 1669f7b2ffc7..38398480fbf2 100644 --- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties +++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties @@ -116,6 +116,8 @@ 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 diff --git a/browser/components/loop/test/desktop-local/roomViews_test.js b/browser/components/loop/test/desktop-local/roomViews_test.js index c007c85bf99c..c67a34a812c1 100644 --- a/browser/components/loop/test/desktop-local/roomViews_test.js +++ b/browser/components/loop/test/desktop-local/roomViews_test.js @@ -65,6 +65,7 @@ describe("loop.roomViews", function () { roomState: ROOM_STATES.INIT, audioMuted: false, videoMuted: false, + failureReason: undefined, foo: "bar" }); }); diff --git a/browser/components/loop/test/shared/activeRoomStore_test.js b/browser/components/loop/test/shared/activeRoomStore_test.js index 304406825577..4233372d4454 100644 --- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -6,7 +6,9 @@ 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; @@ -91,8 +93,8 @@ describe("loop.store.ActiveRoomStore", function () { 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}); @@ -103,7 +105,28 @@ describe("loop.store.ActiveRoomStore", 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() { @@ -244,6 +267,14 @@ describe("loop.store.ActiveRoomStore", 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(); @@ -380,22 +411,34 @@ describe("loop.store.ActiveRoomStore", function () { }); 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); }); @@ -404,13 +447,13 @@ describe("loop.store.ActiveRoomStore", 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, @@ -418,7 +461,7 @@ describe("loop.store.ActiveRoomStore", function () { }); it("should set the state to `FAILED`", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); expect(store.getStoreState().roomState).eql(ROOM_STATES.FAILED); }); diff --git a/browser/components/loop/test/shared/otSdkDriver_test.js b/browser/components/loop/test/shared/otSdkDriver_test.js index eb75c32ed52f..06c607a2f7a7 100644 --- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -7,16 +7,19 @@ 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" }; @@ -34,14 +37,14 @@ describe("loop.OTSdkDriver", function () { 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) }; @@ -80,51 +83,6 @@ describe("loop.OTSdkDriver", function () { 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() { @@ -194,7 +152,7 @@ describe("loop.OTSdkDriver", function () { 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)); }); }); }); @@ -269,7 +227,7 @@ describe("loop.OTSdkDriver", function () { 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)); }); }); @@ -328,5 +286,41 @@ describe("loop.OTSdkDriver", function () { 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); + }); + }); }); }); diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index 9c36bc646a44..d63c6ecdc07e 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -139,6 +139,16 @@ describe("loop.standaloneRoomViews", function() { }); }); + 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"); diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index e974a50265e1..5e6310135aa5 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -608,6 +608,16 @@ 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}}) + ) ) ), diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index e1c016d09b81..20dd786f78b8 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -609,6 +609,16 @@ helper={{isFirefox: returnFalse}} /> + + +
+ +
+
diff --git a/browser/components/sessionstore/test/browser_586068-reload.js b/browser/components/sessionstore/test/browser_586068-reload.js index e79835f6e252..655431594068 100644 --- a/browser/components/sessionstore/test/browser_586068-reload.js +++ b/browser/components/sessionstore/test/browser_586068-reload.js @@ -9,11 +9,6 @@ function test() { } 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); @@ -29,112 +24,31 @@ function runTests() { { 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, + gBrowser.tabContainer.addEventListener("SSTabRestored", function onRestored(event) { + let tab = event.target; + let browser = tab.linkedBrowser; + let tabData = state.windows[0].tabs[loadCount++]; + + // 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) { - // 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"); + if (loadCount == state.windows[0].tabs.length) { + gBrowser.tabContainer.removeEventListener("SSTabRestored", onRestored); - 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]); - } + 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); -} diff --git a/browser/experiments/Makefile.in b/browser/experiments/Makefile.in index 6025f7624331..831d9ef209f7 100644 --- a/browser/experiments/Makefile.in +++ b/browser/experiments/Makefile.in @@ -8,9 +8,8 @@ include $(topsrcdir)/config/rules.mk 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 *); \ diff --git a/browser/experiments/moz.build b/browser/experiments/moz.build index 2bd9f455da5e..a11e4b7258b5 100644 --- a/browser/experiments/moz.build +++ b/browser/experiments/moz.build @@ -2,6 +2,8 @@ # 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', diff --git a/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.properties b/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.properties index 5284c84a04f5..b5dc075c55e9 100644 --- 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? diff --git a/build/mobile/robocop/FennecNativeActions.java b/build/mobile/robocop/FennecNativeActions.java index 3f4634c05717..2870ca25fcd4 100644 --- a/build/mobile/robocop/FennecNativeActions.java +++ b/build/mobile/robocop/FennecNativeActions.java @@ -41,7 +41,7 @@ public class FennecNativeActions implements Actions { } class GeckoEventExpecter implements RepeatedEventExpecter { - private static final int MAX_WAIT_MS = 90000; + private static final int MAX_WAIT_MS = 180000; private volatile boolean mIsRegistered; diff --git a/layout/style/test/Makefile.in b/layout/style/test/Makefile.in index febcea4c4986..7c8b782bfe70 100644 --- a/layout/style/test/Makefile.in +++ b/layout/style/test/Makefile.in @@ -23,5 +23,6 @@ css_properties.js: host_ListCSSProperties$(HOST_BIN_SUFFIX) css_properties_like_ GARBAGE += css_properties.js TEST_FILES := css_properties.js TEST_DEST = $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) +TEST_TARGET := misc INSTALL_TARGETS += TEST endif diff --git a/layout/style/test/moz.build b/layout/style/test/moz.build index 9f9b80fc883c..cae600df1db8 100644 --- a/layout/style/test/moz.build +++ b/layout/style/test/moz.build @@ -4,6 +4,8 @@ # 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', ]) diff --git a/mobile/android/base/SharedPreferencesHelper.java b/mobile/android/base/SharedPreferencesHelper.java index cc91efeaa535..eebc1d210013 100644 --- a/mobile/android/base/SharedPreferencesHelper.java +++ b/mobile/android/base/SharedPreferencesHelper.java @@ -27,6 +27,10 @@ public final class SharedPreferencesHelper { 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"), @@ -214,7 +218,7 @@ public final class SharedPreferencesHelper @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (Log.isLoggable(LOGTAG, Log.VERBOSE)) { + if (logVerbose) { Log.v(LOGTAG, "Got onSharedPreferenceChanged"); } try { @@ -279,19 +283,19 @@ public final class SharedPreferencesHelper // 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); diff --git a/mobile/android/base/distribution/Distribution.java b/mobile/android/base/distribution/Distribution.java index 7b108845a159..ee5ea2870c3e 100644 --- a/mobile/android/base/distribution/Distribution.java +++ b/mobile/android/base/distribution/Distribution.java @@ -24,7 +24,6 @@ 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; @@ -42,6 +41,7 @@ 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; @@ -286,7 +286,7 @@ public class Distribution { } 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!"); @@ -314,7 +314,7 @@ public class Distribution { } 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); @@ -739,19 +739,6 @@ public class Distribution { 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; } diff --git a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java index a6ed9d569703..0258eaef0d2f 100644 --- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java @@ -38,6 +38,7 @@ 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. @@ -86,6 +87,7 @@ public class FxAccountStatusFragment 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 @@ -167,6 +169,10 @@ public class FxAccountStatusFragment morePreference = ensureFindPreference("more"); morePreference.setOnPreferenceClickListener(this); + syncNowPreference = ensureFindPreference("sync_now"); + syncNowPreference.setEnabled(true); + syncNowPreference.setOnPreferenceClickListener(this); + if (HardwareUtils.hasMenuButton()) { syncCategory.removePreference(morePreference); } @@ -229,6 +235,13 @@ public class FxAccountStatusFragment return true; } + if (preference == syncNowPreference) { + if (fxAccount != null) { + FirefoxAccounts.requestSync(fxAccount.getAndroidAccount(), FirefoxAccounts.FORCE, null, null); + } + return true; + } + return false; } @@ -250,6 +263,7 @@ public class FxAccountStatusFragment passwordsPreference.setEnabled(enabled); // Since we can't sync, we can't update our remote client record. deviceNamePreference.setEnabled(enabled); + syncNowPreference.setEnabled(enabled); } /** @@ -470,6 +484,26 @@ public class FxAccountStatusFragment 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() { diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java index d32b787800bb..9a0a840d95f5 100644 --- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -65,6 +65,8 @@ public class AndroidFxAccount { protected static final List 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; @@ -565,4 +567,22 @@ public class AndroidFxAccount { 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; + } + } } diff --git a/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java b/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java index 26c856eaf2d3..5d63b30a429f 100644 --- a/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java +++ b/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java @@ -105,6 +105,7 @@ public class FxAccountSchedulePolicy implements SchedulePolicy { @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. diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index b13cd0c26a6b..c9f03a08532f 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -434,11 +434,20 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { 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 syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); FirefoxAccounts.logSyncHints(syncHints); @@ -450,12 +459,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { 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 diff --git a/mobile/android/base/locales/en-US/sync_strings.dtd b/mobile/android/base/locales/en-US/sync_strings.dtd index 9ef4d9b9c4e6..7e8058ae0371 100644 --- a/mobile/android/base/locales/en-US/sync_strings.dtd +++ b/mobile/android/base/locales/en-US/sync_strings.dtd @@ -187,6 +187,8 @@ + + diff --git a/mobile/android/base/resources/layout/fxaccount_confirm_account.xml b/mobile/android/base/resources/layout/fxaccount_confirm_account.xml index 96d460e95e87..6df9f3190477 100644 --- a/mobile/android/base/resources/layout/fxaccount_confirm_account.xml +++ b/mobile/android/base/resources/layout/fxaccount_confirm_account.xml @@ -45,7 +45,7 @@ style="@style/FxAccountLinkItem" android:text="@string/fxaccount_confirm_account_resend_email" /> - diff --git a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml index b38e26f2390c..12fde5c4cc23 100644 --- a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml +++ b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml @@ -55,6 +55,14 @@ android:persistent="false" android:title="@string/fxaccount_status_needs_account_enabled" /> + + &share_image_failed; &save_as_pdf; &find_in_page; + &find_matchcase; &desktop_mode; &page; &tools; diff --git a/mobile/android/base/sync/SyncConfiguration.java b/mobile/android/base/sync/SyncConfiguration.java index b5b5064a3669..3f23dec8896d 100644 --- a/mobile/android/base/sync/SyncConfiguration.java +++ b/mobile/android/base/sync/SyncConfiguration.java @@ -37,7 +37,6 @@ public class SyncConfiguration { this.editor = config.getEditor(); } - @Override public void apply() { // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding. this.editor.commit(); @@ -86,7 +85,6 @@ public class SyncConfiguration { // Not marking as Override, because Android <= 10 doesn't have // putStringSet. Neither can we implement it. - @Override public Editor putStringSet(String key, Set value) { throw new RuntimeException("putStringSet not available."); } @@ -162,7 +160,6 @@ public class SyncConfiguration { // Not marking as Override, because Android <= 10 doesn't have // getStringSet. Neither can we implement it. - @Override public Set getStringSet(String key, Set defValue) { throw new RuntimeException("getStringSet not available."); } diff --git a/mobile/android/base/sync/net/BaseResource.java b/mobile/android/base/sync/net/BaseResource.java index ebe4785ceaa1..c1834725dacf 100644 --- a/mobile/android/base/sync/net/BaseResource.java +++ b/mobile/android/base/sync/net/BaseResource.java @@ -65,7 +65,7 @@ public class BaseResource implements Resource { private boolean retryOnFailedRequest = true; - public static final boolean rewriteLocalhost = true; + public static boolean rewriteLocalhost = true; private static final String LOG_TAG = "BaseResource"; diff --git a/mobile/android/base/tests/JavascriptTest.java b/mobile/android/base/tests/JavascriptTest.java index 35a642bbe1a5..09877833566f 100644 --- a/mobile/android/base/tests/JavascriptTest.java +++ b/mobile/android/base/tests/JavascriptTest.java @@ -11,6 +11,11 @@ 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) { @@ -39,11 +44,11 @@ public class JavascriptTest extends BaseTest { 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 + "'"); } @@ -60,7 +65,7 @@ public class JavascriptTest extends BaseTest { testMessageParser.logMessage(message); } - if (Log.isLoggable(LOGTAG, Log.DEBUG)) { + if (logDebug) { Log.d(LOGTAG, "Got test finished message"); } } finally { diff --git a/mobile/android/base/util/FileUtils.java b/mobile/android/base/util/FileUtils.java index a89ac90b4965..202696338d30 100644 --- a/mobile/android/base/util/FileUtils.java +++ b/mobile/android/base/util/FileUtils.java @@ -9,6 +9,7 @@ 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; @@ -81,4 +82,17 @@ public class FileUtils { // 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(); + } + } + } } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 724cf7899959..1122e0cb5c1f 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6907,7 +6907,7 @@ var SearchEngines = { 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); @@ -7047,34 +7047,7 @@ var SearchEngines = { // 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. diff --git a/mobile/android/config/mozconfigs/common b/mobile/android/config/mozconfigs/common index 246a49b27d9b..219bb027ac47 100644 --- a/mobile/android/config/mozconfigs/common +++ b/mobile/android/config/mozconfigs/common @@ -15,7 +15,7 @@ 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 diff --git a/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest b/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest index d2f3e4c44d10..003255a8ed7b 100644 --- a/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest @@ -6,8 +6,8 @@ "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, diff --git a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest index 256d5715911f..a8d8ce5224d8 100644 --- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest @@ -6,8 +6,8 @@ "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, diff --git a/mobile/android/config/tooltool-manifests/android/releng.manifest b/mobile/android/config/tooltool-manifests/android/releng.manifest index d2f3e4c44d10..003255a8ed7b 100644 --- a/mobile/android/config/tooltool-manifests/android/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android/releng.manifest @@ -6,8 +6,8 @@ "filename": "android-ndk.tar.bz2" }, { -"size": 207966812, -"digest": "9f6d50e5e67e6a6784dea3b5573178a869b017d2d0c7588af9eb53fdd23c7d1bd6f775f07a9ad1510ba36bf1608d21baa4e26e92afe61e190429870a6371b97d", +"size": 227988048, +"digest": "c84db0abd1f4fda1bb38292ef561e211e1e6b99586764fd8cf0829fa4d0c6a605eb21e1eb5462465fcca64749d48e22ac1b13029e2623bbdfe103801f5ef1411", "algorithm": "sha512", "filename": "android-sdk.tar.xz" }, diff --git a/mobile/android/gradle/Makefile.in b/mobile/android/gradle/Makefile.in index e695d24dc05a..ce217a748ece 100644 --- a/mobile/android/gradle/Makefile.in +++ b/mobile/android/gradle/Makefile.in @@ -3,12 +3,14 @@ # 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 diff --git a/mobile/android/gradle/build.gradle b/mobile/android/gradle/build.gradle index 577a0e7ded92..9cfdaeaa4535 100644 --- a/mobile/android/gradle/build.gradle +++ b/mobile/android/gradle/build.gradle @@ -14,7 +14,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:0.14.1' } } diff --git a/mobile/android/gradle/gradle/wrapper/gradle-wrapper.jar b/mobile/android/gradle/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a8698..3d0dee6e8edf 100644 Binary files a/mobile/android/gradle/gradle/wrapper/gradle-wrapper.jar and b/mobile/android/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mobile/android/gradle/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/gradle/wrapper/gradle-wrapper.properties index 1e61d1fd3a9b..fccfe002227a 100644 --- 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 diff --git a/mobile/android/gradle/local.properties.in b/mobile/android/gradle/local.properties.in new file mode 100644 index 000000000000..0c64cd2341f9 --- /dev/null +++ b/mobile/android/gradle/local.properties.in @@ -0,0 +1,2 @@ +#filter substitution +sdk.dir=@ANDROID_SDK_ROOT@ diff --git a/mobile/android/search/java/org/mozilla/search/Constants.java b/mobile/android/search/java/org/mozilla/search/Constants.java index 55fb50ef5a79..8e8a1760055c 100644 --- a/mobile/android/search/java/org/mozilla/search/Constants.java +++ b/mobile/android/search/java/org/mozilla/search/Constants.java @@ -17,9 +17,4 @@ package org.mozilla.search; 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"; } diff --git a/mobile/android/search/java/org/mozilla/search/SearchActivity.java b/mobile/android/search/java/org/mozilla/search/SearchActivity.java index ceb03e765774..3c90db24dbcf 100644 --- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java @@ -10,6 +10,7 @@ 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; @@ -101,7 +102,7 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity 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. @@ -279,7 +280,10 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity 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); + } } }); } @@ -293,6 +297,10 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity */ @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); diff --git a/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java b/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java index f4969a9f4eb1..dca6285f4d10 100644 --- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java @@ -52,7 +52,9 @@ public class SearchEngine { "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; @@ -189,7 +191,9 @@ public class SearchEngine { 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}"; @@ -247,7 +251,7 @@ public class SearchEngine { 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()); @@ -261,7 +265,7 @@ public class SearchEngine { */ 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()); diff --git a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java index aacd5139089f..51d067154ffe 100644 --- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java @@ -6,15 +6,21 @@ 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; @@ -30,9 +36,16 @@ 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; @@ -40,11 +53,19 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference 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; } @@ -60,80 +81,230 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference 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; } - getEngineFromPrefs(changeCallback); + + if (ignorePreferenceChange > 0) { + ignorePreferenceChange--; + return; + } + + getDefaultEngine(changeCallback); } /** - * Look up the current search engine in shared preferences. - * Creates a SearchEngine instance and caches it for use on the main thread. + * 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. * - * @param callback a SearchEngineCallback to be called after successfully looking + * 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 getEngineFromPrefs(final SearchEngineCallback callback) { - final AsyncTask task = new AsyncTask() { + private void getDefaultEngine(final SearchEngineCallback callback) { + // This runnable is posted to the background thread. + distribution.addOnDistributionReadyCallback(new Runnable() { @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); + 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(); } - 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; + final SearchEngine engine = createEngineFromName(name); + runCallback(engine, callback); } - - @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); - } - } - } - }; - task.execute(); + }); } /** - * Creates a list of SearchEngine instances from all available open search plugins. - * This method does disk I/O, call it from a background thread. + * Looks for a default search engine included in a distribution. + * This method must be called after the distribution is ready. * - * @return List of SearchEngine instances + * @return search engine name. */ - public List getAllEngines() { - // First try to read the engine list from the jar. - InputStream in = getInputStreamFromJar("list.txt"); + private String getDefaultEngineNameFromDistribution() { + if (!distribution.exists()) { + return null; + } - final List list = new ArrayList(); + 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); + } + } + + // 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); + } + } + } 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 SearchEngine instance from an engine name. + * + * 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. + */ + 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; + } + + /** + * 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 { @@ -141,10 +312,14 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference 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 { @@ -159,27 +334,62 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference // Ignore. } } - return list; + return null; } /** - * Creates a SearchEngine instance from an open search plugin. - * This method does disk I/O, call it from a background thread. + * Creates a SearchEngine instance for a search plugin in the profile directory. * - * @param identifier search engine identifier (e.g. "google") - * @return SearchEngine instance for identifier + * 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 createEngine(String identifier) { - InputStream in = getInputStreamFromJar(identifier + ".xml"); - - if (in == null) { - in = getEngineFromProfile(identifier); + private SearchEngine createEngineFromProfile(String name) { + final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins"); + if (pluginsDir == null) { + return null; } - if (in == null) { - throw new IllegalArgumentException("Couldn't find search engine for identifier: " + identifier); - } + final File[] files = pluginsDir.listFiles(); + return createEngineFromFileList(files, name); + } + /** + * This method iterates through an array of search plugin files, creating + * SearchEngine instances until it finds one with the right name. + * + * @param files Array of search plugin files. + * @param name Search engine name. + * @return SearchEngine instance for name. + */ + 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; + } + + /** + * 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); @@ -194,13 +404,12 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference } /** - * 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. @@ -228,32 +437,14 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference } /** - * 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; - } } diff --git a/mobile/android/services/strings.xml.in b/mobile/android/services/strings.xml.in index 739f9e6a2b55..98ba82ebd50f 100644 --- a/mobile/android/services/strings.xml.in +++ b/mobile/android/services/strings.xml.in @@ -175,6 +175,9 @@ &fxaccount_status_header2; &fxaccount_status_signed_in_as; &fxaccount_status_auth_server; +&fxaccount_status_sync_now; +&fxaccount_status_syncing; +&remote_tabs_last_synced; &fxaccount_status_device_name; &fxaccount_status_sync_server; &fxaccount_status_sync; @@ -216,6 +219,3 @@ &fxaccount_remove_account_dialog_message; &fxaccount_remove_account_toast; &fxaccount_remove_account_menu_item; - - -&find_matchcase; diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java index 50feca0025f4..a2e34fe34c28 100644 --- a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java +++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java @@ -274,7 +274,9 @@ public class Hex implements BinaryEncoder, BinaryDecoder { 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); } } diff --git a/python/mozbuild/mozbuild/frontend/context.py b/python/mozbuild/mozbuild/frontend/context.py index aea5e6a2b2e4..e40a109b8d13 100644 --- a/python/mozbuild/mozbuild/frontend/context.py +++ b/python/mozbuild/mozbuild/frontend/context.py @@ -501,6 +501,22 @@ VARIABLES = { 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. diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build index e69de29bb2d1..578799f986a8 100644 --- 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 @@ +HAS_MISC_RULE = True diff --git a/python/mozbuild/mozbuild/test/frontend/test_emitter.py b/python/mozbuild/mozbuild/test/frontend/test_emitter.py index 6b388c49f1d1..1400a8048725 100644 --- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py +++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py @@ -98,6 +98,8 @@ class TestEmitterBasic(unittest.TestCase): 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, [ [