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, [
[