author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Wed, 10 Dec 2014 11:55:54 +0100 | |
changeset 218985 | 5b01216f97f863236eccee5227abbc18319d4ab1 |
parent 218955 | 551c3cd74dbdcacb83803ded241c657917da94a5 (current diff) |
parent 218984 | 219d81afdd503110fd28def92d7a2076b715f78c (diff) |
child 219001 | be1f49e80d2da65d16134390ab8b8e8c36a883ab |
push id | 27950 |
push user | cbook@mozilla.com |
push date | Wed, 10 Dec 2014 10:58:50 +0000 |
treeherder | autoland@5b01216f97f8 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 37.0a1 |
first release with | nightly linux32
5b01216f97f8
/
37.0a1
/
20141210030207
/
files
nightly linux64
5b01216f97f8
/
37.0a1
/
20141210030207
/
files
nightly mac
5b01216f97f8
/
37.0a1
/
20141210030207
/
files
nightly win32
5b01216f97f8
/
37.0a1
/
20141210030207
/
files
nightly win64
5b01216f97f8
/
37.0a1
/
20141210030207
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
37.0a1
/
20141210030207
/
pushlog to previous
nightly linux64
37.0a1
/
20141210030207
/
pushlog to previous
nightly mac
37.0a1
/
20141210030207
/
pushlog to previous
nightly win32
37.0a1
/
20141210030207
/
pushlog to previous
nightly win64
37.0a1
/
20141210030207
/
pushlog to previous
|
js/src/jscompartment.cpp | file | annotate | diff | comparison | revisions | |
mobile/android/base/LocaleAware.java | file | annotate | diff | comparison | revisions | |
mobile/android/config/proguard.cfg | file | annotate | diff | comparison | revisions |
--- a/browser/base/content/abouthome/aboutHome.xhtml +++ b/browser/base/content/abouthome/aboutHome.xhtml @@ -59,17 +59,17 @@ </div> </div> <div class="spacer"/> <div id="launcher"> <button class="launchButton" id="downloads">&abouthome.downloadsButton.label;</button> <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button> <button class="launchButton" id="history">&abouthome.historyButton.label;</button> - <button class="launchButton" id="apps" hidden="true">&abouthome.appsButton.label;</button> + <button class="launchButton" id="apps">&abouthome.appsButton.label;</button> <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button> <button class="launchButton" id="sync">&abouthome.syncButton.label;</button> #ifdef XP_WIN <button class="launchButton" id="settings">&abouthome.preferencesButtonWin.label;</button> #else <button class="launchButton" id="settings">&abouthome.preferencesButtonUnix.label;</button> #endif <div id="restorePreviousSessionSeparator"/>
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2545,34 +2545,34 @@ let gMenuButtonUpdateBadge = { PanelUI.menuButton.setAttribute("badge", "\u2605"); let brandBundle = document.getElementById("bundle_brand"); let brandShortName = brandBundle.getString("brandShortName"); stringId = "appmenu.restartNeeded.description"; updateButtonText = gNavigatorBundle.getFormattedString(stringId, [brandShortName]); - updateButton.label = updateButtonText; + updateButton.setAttribute("label", updateButtonText); + updateButton.setAttribute("update-status", "succeeded"); updateButton.hidden = false; - updateButton.setAttribute("update-status", "succeeded"); PanelUI.panel.addEventListener("popupshowing", this, true); break; case STATE_FAILED: // Background update has failed, let's show the UI responsible for // prompting the user to update manually. PanelUI.menuButton.setAttribute("badge", "!"); stringId = "appmenu.updateFailed.description"; updateButtonText = gNavigatorBundle.getString(stringId); - updateButton.label = updateButtonText; + updateButton.setAttribute("label", updateButtonText); + updateButton.setAttribute("update-status", "failed"); updateButton.hidden = false; - updateButton.setAttribute("update-status", "failed"); PanelUI.panel.addEventListener("popupshowing", this, true); break; case STATE_DOWNLOADING: // We've fallen back to downloading the full update because the partial // update failed to get staged in the background. Therefore we need to keep // our observer.
--- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -322,22 +322,16 @@ let AboutHomeListener = { addMessageListener("AboutHome:FocusInput", this); addEventListener("click", this, true); addEventListener("pagehide", this, true); if (!Services.prefs.getBoolPref("browser.search.showOneOffButtons")) { doc.documentElement.setAttribute("searchUIConfiguration", "oldsearchui"); } - // XXX bug 738646 - when Marketplace is launched, remove this statement and - // the hidden attribute set on the apps button in aboutHome.xhtml - if (Services.prefs.getPrefType("browser.aboutHome.apps") == Services.prefs.PREF_BOOL && - Services.prefs.getBoolPref("browser.aboutHome.apps")) - doc.getElementById("apps").removeAttribute("hidden"); - sendAsyncMessage("AboutHome:RequestUpdate"); doc.addEventListener("AboutHomeSearchEvent", this, true, true); doc.addEventListener("AboutHomeSearchPanel", this, true, true); }, onClick: function(aEvent) { if (!aEvent.isTrusted || // Don't trust synthetic events aEvent.button == 2 || aEvent.target.localName != "button") {
--- a/browser/components/loop/content/js/contacts.js +++ b/browser/components/loop/content/js/contacts.js @@ -253,17 +253,20 @@ loop.contacts = (function(_, mozL10n) { : null ) ); } }); const ContactsList = React.createClass({displayName: 'ContactsList', - mixins: [React.addons.LinkedStateMixin], + mixins: [ + React.addons.LinkedStateMixin, + loop.shared.mixins.WindowCloseMixin + ], /** * Contacts collection object */ contacts: null, /** * User profile @@ -430,21 +433,23 @@ loop.contacts = (function(_, mozL10n) { if (err) { throw err; } }); break; case "video-call": if (!contact.blocked) { navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO); + this.closeWindow(); } break; case "audio-call": if (!contact.blocked) { navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY); + this.closeWindow(); } break; default: console.error("Unrecognized action: " + actionName); break; } },
--- a/browser/components/loop/content/js/contacts.jsx +++ b/browser/components/loop/content/js/contacts.jsx @@ -253,17 +253,20 @@ loop.contacts = (function(_, mozL10n) { : null } </li> ); } }); const ContactsList = React.createClass({ - mixins: [React.addons.LinkedStateMixin], + mixins: [ + React.addons.LinkedStateMixin, + loop.shared.mixins.WindowCloseMixin + ], /** * Contacts collection object */ contacts: null, /** * User profile @@ -430,21 +433,23 @@ loop.contacts = (function(_, mozL10n) { if (err) { throw err; } }); break; case "video-call": if (!contact.blocked) { navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO); + this.closeWindow(); } break; case "audio-call": if (!contact.blocked) { navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY); + this.closeWindow(); } break; default: console.error("Unrecognized action: " + actionName); break; } },
--- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -218,17 +218,17 @@ loop.conversation = (function(mozL10n) { /** * This view manages the incoming conversation views - from * call initiation through to the actual conversation and call end. * * At the moment, it does more than that, these parts need refactoring out. */ var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView', - mixins: [sharedMixins.AudioMixin], + mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], propTypes: { client: React.PropTypes.instanceOf(loop.Client).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, sdk: React.PropTypes.object.isRequired, conversationAppStore: React.PropTypes.instanceOf( loop.store.ConversationAppStore).isRequired, @@ -310,17 +310,17 @@ loop.conversation = (function(mozL10n) { return ( sharedViews.FeedbackView({ feedbackStore: this.props.feedbackStore, onAfterFeedbackReceived: this.closeWindow.bind(this)} ) ); } case "close": { - window.close(); + this.closeWindow(); return (React.DOM.div(null)); } } }, /** * Notify the user that the connection was not possible * @param {{code: number, message: string}} error @@ -454,20 +454,16 @@ loop.conversation = (function(mozL10n) { */ _abortIncomingCall: function() { this._websocket.close(); // Having a timeout here lets the logging for the websocket complete and be // displayed on the console if both are on. setTimeout(this.closeWindow, 0); }, - closeWindow: function() { - window.close(); - }, - /** * Accepts an incoming call. */ accept: function() { navigator.mozLoop.stopAlerting(); this._websocket.accept(); this.props.conversation.accepted(); }, @@ -536,17 +532,17 @@ loop.conversation = (function(mozL10n) { }, }); /** * Master controller view for handling if incoming or outgoing calls are * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({displayName: 'AppControllerView', - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { // XXX Old types required for incoming call view. client: React.PropTypes.instanceOf(loop.Client).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -570,20 +566,16 @@ loop.conversation = (function(mozL10n) { this.setState(this.props.conversationAppStore.getStoreState()); }, this); }, componentWillUnmount: function() { this.stopListening(this.props.conversationAppStore); }, - closeWindow: function() { - window.close(); - }, - render: function() { switch(this.state.windowType) { case "incoming": { return (IncomingConversationView({ client: this.props.client, conversation: this.props.conversation, sdk: this.props.sdk, conversationAppStore: this.props.conversationAppStore,
--- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -218,17 +218,17 @@ loop.conversation = (function(mozL10n) { /** * This view manages the incoming conversation views - from * call initiation through to the actual conversation and call end. * * At the moment, it does more than that, these parts need refactoring out. */ var IncomingConversationView = React.createClass({ - mixins: [sharedMixins.AudioMixin], + mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin], propTypes: { client: React.PropTypes.instanceOf(loop.Client).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, sdk: React.PropTypes.object.isRequired, conversationAppStore: React.PropTypes.instanceOf( loop.store.ConversationAppStore).isRequired, @@ -310,17 +310,17 @@ loop.conversation = (function(mozL10n) { return ( <sharedViews.FeedbackView feedbackStore={this.props.feedbackStore} onAfterFeedbackReceived={this.closeWindow.bind(this)} /> ); } case "close": { - window.close(); + this.closeWindow(); return (<div/>); } } }, /** * Notify the user that the connection was not possible * @param {{code: number, message: string}} error @@ -454,20 +454,16 @@ loop.conversation = (function(mozL10n) { */ _abortIncomingCall: function() { this._websocket.close(); // Having a timeout here lets the logging for the websocket complete and be // displayed on the console if both are on. setTimeout(this.closeWindow, 0); }, - closeWindow: function() { - window.close(); - }, - /** * Accepts an incoming call. */ accept: function() { navigator.mozLoop.stopAlerting(); this._websocket.accept(); this.props.conversation.accepted(); }, @@ -536,17 +532,17 @@ loop.conversation = (function(mozL10n) { }, }); /** * Master controller view for handling if incoming or outgoing calls are * in progress, and hence, which view to display. */ var AppControllerView = React.createClass({ - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { // XXX Old types required for incoming call view. client: React.PropTypes.instanceOf(loop.Client).isRequired, conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, sdk: React.PropTypes.object.isRequired, @@ -570,20 +566,16 @@ loop.conversation = (function(mozL10n) { this.setState(this.props.conversationAppStore.getStoreState()); }, this); }, componentWillUnmount: function() { this.stopListening(this.props.conversationAppStore); }, - closeWindow: function() { - window.close(); - }, - render: function() { switch(this.state.windowType) { case "incoming": { return (<IncomingConversationView client={this.props.client} conversation={this.props.conversation} sdk={this.props.sdk} conversationAppStore={this.props.conversationAppStore}
--- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -188,17 +188,21 @@ loop.conversationViews = (function(mozL1 ); } }); /** * Call failed view. Displayed when a call fails. */ var CallFailedView = React.createClass({displayName: 'CallFailedView', - mixins: [Backbone.Events, sharedMixins.AudioMixin], + mixins: [ + Backbone.Events, + sharedMixins.AudioMixin, + sharedMixins.WindowCloseMixin + ], propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, store: React.PropTypes.instanceOf( loop.store.ConversationStore).isRequired, contact: React.PropTypes.object.isRequired, // This is used by the UI showcase. emailLinkError: React.PropTypes.bool, @@ -222,17 +226,17 @@ loop.conversationViews = (function(mozL1 componentWillUnmount: function() { this.stopListening(this.props.store); }, _onEmailLinkReceived: function() { var emailLink = this.props.store.getStoreState("emailLink"); var contactEmail = _getPreferredEmail(this.props.contact).value; sharedUtils.composeCallUrlEmail(emailLink, contactEmail); - window.close(); + this.closeWindow(); }, _onEmailLinkError: function() { this.setState({ emailLinkError: true, emailLinkButtonDisabled: false }); },
--- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -188,17 +188,21 @@ loop.conversationViews = (function(mozL1 ); } }); /** * Call failed view. Displayed when a call fails. */ var CallFailedView = React.createClass({ - mixins: [Backbone.Events, sharedMixins.AudioMixin], + mixins: [ + Backbone.Events, + sharedMixins.AudioMixin, + sharedMixins.WindowCloseMixin + ], propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, store: React.PropTypes.instanceOf( loop.store.ConversationStore).isRequired, contact: React.PropTypes.object.isRequired, // This is used by the UI showcase. emailLinkError: React.PropTypes.bool, @@ -222,17 +226,17 @@ loop.conversationViews = (function(mozL1 componentWillUnmount: function() { this.stopListening(this.props.store); }, _onEmailLinkReceived: function() { var emailLink = this.props.store.getStoreState("emailLink"); var contactEmail = _getPreferredEmail(this.props.contact).value; sharedUtils.composeCallUrlEmail(emailLink, contactEmail); - window.close(); + this.closeWindow(); }, _onEmailLinkError: function() { this.setState({ emailLinkError: true, emailLinkButtonDisabled: false }); },
--- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -590,30 +590,33 @@ loop.panel = (function(_, mozL10n) { * Room list entry. */ var RoomEntry = React.createClass({displayName: 'RoomEntry', propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, room: React.PropTypes.instanceOf(loop.store.Room).isRequired }, + mixins: [loop.shared.mixins.WindowCloseMixin], + getInitialState: function() { return { urlCopied: false }; }, shouldComponentUpdate: function(nextProps, nextState) { return (nextProps.room.ctime > this.props.room.ctime) || (nextState.urlCopied !== this.state.urlCopied); }, handleClickEntry: function(event) { event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: this.props.room.roomToken })); + this.closeWindow(); }, handleCopyButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({ roomUrl: this.props.room.roomUrl }));
--- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -590,30 +590,33 @@ loop.panel = (function(_, mozL10n) { * Room list entry. */ var RoomEntry = React.createClass({ propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, room: React.PropTypes.instanceOf(loop.store.Room).isRequired }, + mixins: [loop.shared.mixins.WindowCloseMixin], + getInitialState: function() { return { urlCopied: false }; }, shouldComponentUpdate: function(nextProps, nextState) { return (nextProps.room.ctime > this.props.room.ctime) || (nextState.urlCopied !== this.state.urlCopied); }, handleClickEntry: function(event) { event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: this.props.room.roomToken })); + this.closeWindow(); }, handleCopyButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({ roomUrl: this.props.room.roomUrl }));
--- a/browser/components/loop/content/shared/js/mixins.js +++ b/browser/components/loop/content/shared/js/mixins.js @@ -11,23 +11,25 @@ loop.shared.mixins = (function() { /** * Root object, by default set to window. * @type {DOMWindow|Object} */ var rootObject = window; /** - * Sets a new root object. This is useful for testing native DOM events so we - * can fake them. + * Sets a new root object. This is useful for testing native DOM events so we + * can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to + * substitute a fake window, and in afterEach(), the real window object is + * replaced. * * @param {Object} */ function setRootObject(obj) { - console.info("loop.shared.mixins: rootObject set to " + obj); + console.log("loop.shared.mixins: rootObject set to " + obj); rootObject = obj; } /** * window.location mixin. Handles changes in the call url. * Forces a reload of the page to ensure proper state of the webapp * * @type {Object} @@ -60,16 +62,31 @@ loop.shared.mixins = (function() { */ var DocumentTitleMixin = { setTitle: function(newTitle) { rootObject.document.title = newTitle; } }; /** + * Window close mixin, for more testable closing of windows. Instead of + * calling window.close() directly, use this mixin and call + * this.closeWindow from your component. + * + * @type {Object} + * + * @see setRootObject for info on how to unit test code that uses this mixin + */ + var WindowCloseMixin = { + closeWindow: function() { + rootObject.close(); + } + }; + + /** * Dropdown menu mixin. * @type {Object} */ var DropdownMenuMixin = { get documentBody() { return rootObject.document.body; }, @@ -286,11 +303,12 @@ loop.shared.mixins = (function() { return { AudioMixin: AudioMixin, RoomsAudioMixin: RoomsAudioMixin, setRootObject: setRootObject, DropdownMenuMixin: DropdownMenuMixin, DocumentVisibilityMixin: DocumentVisibilityMixin, DocumentLocationMixin: DocumentLocationMixin, DocumentTitleMixin: DocumentTitleMixin, - UrlHashChangeMixin: UrlHashChangeMixin + UrlHashChangeMixin: UrlHashChangeMixin, + WindowCloseMixin: WindowCloseMixin }; })();
--- a/browser/components/loop/test/desktop-local/contacts_test.js +++ b/browser/components/loop/test/desktop-local/contacts_test.js @@ -9,37 +9,87 @@ var expect = chai.expect; var TestUtils = React.addons.TestUtils; describe("loop.contacts", function() { "use strict"; var fakeAddContactButtonText = "Fake Add Contact"; var fakeEditContactButtonText = "Fake Edit Contact"; var fakeDoneButtonText = "Fake Done"; + var sandbox; + var fakeWindow; beforeEach(function(done) { + sandbox = sinon.sandbox.create(); navigator.mozLoop = { getStrings: function(entityName) { var textContentValue = "fakeText"; if (entityName == "add_contact_button") { textContentValue = fakeAddContactButtonText; } else if (entityName == "edit_contact_title") { textContentValue = fakeEditContactButtonText; } else if (entityName == "edit_contact_done_button") { textContentValue = fakeDoneButtonText; } return JSON.stringify({textContent: textContentValue}); }, }; + fakeWindow = { + close: sandbox.stub(), + }; + loop.shared.mixins.setRootObject(fakeWindow); + document.mozL10n.initialize(navigator.mozLoop); // XXX prevent a race whenever mozL10n hasn't been initialized yet setTimeout(done, 0); }); + afterEach(function() { + loop.shared.mixins.setRootObject(window); + sandbox.restore(); + }); + + + describe("ContactsList", function () { + var listView; + + beforeEach(function() { + navigator.mozLoop.calls = { + startDirectCall: sandbox.stub(), + clearCallInProgress: sandbox.stub() + }; + navigator.mozLoop.contacts = {getAll: sandbox.stub()}; + + listView = TestUtils.renderIntoDocument(loop.contacts.ContactsList()); + }); + + afterEach(function() { + listView = null; + delete navigator.mozLoop.calls; + delete navigator.mozLoop.contacts; + }); + + describe("#handleContactAction", function() { + it("should call window.close when called with 'video-call' action", + function() { + listView.handleContactAction({}, "video-call"); + + sinon.assert.calledOnce(fakeWindow.close); + }); + + it("should call window.close when called with 'audio-call' action", + function() { + listView.handleContactAction({}, "audio-call"); + + sinon.assert.calledOnce(fakeWindow.close); + }); + }); + }); + describe("ContactDetailsForm", function() { describe("#render", function() { describe("add mode", function() { it("should render 'add' header", function() { var view = TestUtils.renderIntoDocument( loop.contacts.ContactDetailsForm({mode: "add"})); var header = view.getDOMNode().querySelector("header");
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -3,17 +3,17 @@ var expect = chai.expect; describe("loop.conversationViews", function () { "use strict"; var sharedUtils = loop.shared.utils; var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR; - var fakeMozLoop; + var fakeMozLoop, fakeWindow; var CALL_STATES = loop.store.CALL_STATES; beforeEach(function() { sandbox = sinon.sandbox.create(); oldTitle = document.title; sandbox.stub(document.mozL10n, "get", function(x) { @@ -53,19 +53,27 @@ describe("loop.conversationViews", funct channel: "test", platform: "test" }; }, getAudioBlob: sinon.spy(function(name, callback) { callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"})); }) }; + + fakeWindow = { + navigator: { mozLoop: fakeMozLoop }, + close: sandbox.stub(), + }; + loop.shared.mixins.setRootObject(fakeWindow); + }); afterEach(function() { + loop.shared.mixins.setRootObject(window); document.title = oldTitle; view = undefined; delete navigator.mozLoop; sandbox.restore(); }); describe("CallIdentifierView", function() { function mountTestComponent(props) { @@ -311,22 +319,21 @@ describe("loop.conversationViews", funct sinon.assert.calledOnce(composeCallUrlEmail); sinon.assert.calledWithExactly(composeCallUrlEmail, "http://fake.invalid/", "test@test.tld"); }); it("should close the conversation window once the email link is received", function() { - sandbox.stub(window, "close"); view = mountTestComponent(); store.setStoreState({emailLink: "http://fake.invalid/"}); - sinon.assert.calledOnce(window.close); + sinon.assert.calledOnce(fakeWindow.close); }); it("should display an error message in case email link retrieval failed", function() { view = mountTestComponent(); store.trigger("error:emailLink");
--- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -6,16 +6,17 @@ var expect = chai.expect; describe("loop.conversation", function() { "use strict"; var sharedModels = loop.shared.models, sharedView = loop.shared.views, + fakeWindow, sandbox; // XXX refactor to Just Work with "sandbox.stubComponent" or else // just pass in the sandbox and put somewhere generally usable function stubComponent(obj, component, mockTagName){ var reactClass = React.createClass({ render: function() { @@ -63,25 +64,32 @@ describe("loop.conversation", function() platform: "test" }; }, getAudioBlob: sinon.spy(function(name, callback) { callback(null, new Blob([new ArrayBuffer(10)], {type: 'audio/ogg'})); }) }; + fakeWindow = { + navigator: { mozLoop: navigator.mozLoop }, + close: sandbox.stub(), + }; + loop.shared.mixins.setRootObject(fakeWindow); + // XXX These stubs should be hoisted in a common file // Bug 1040968 sandbox.stub(document.mozL10n, "get", function(x) { return x; }); document.mozL10n.initialize(navigator.mozLoop); }); afterEach(function() { + loop.shared.mixins.setRootObject(window); delete navigator.mozLoop; sandbox.restore(); }); describe("#init", function() { beforeEach(function() { sandbox.stub(React, "renderComponent"); sandbox.stub(document.mozL10n, "initialize"); @@ -403,17 +411,16 @@ describe("loop.conversation", function() // setup functions icView = mountTestComponent(); promise = new Promise(function(resolve, reject) { resolve(); }); sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise); sandbox.stub(loop.CallConnectionWebSocket.prototype, "close"); - sandbox.stub(window, "close"); }); describe("progress - terminated (previousState = alerting)", function() { it("should stop alerting", function(done) { promise.then(function() { icView._websocket.trigger("progress", { state: "terminated", reason: "timeout" @@ -440,22 +447,23 @@ describe("loop.conversation", function() promise.then(function() { icView._websocket.trigger("progress", { state: "terminated", reason: "answered-elsewhere" }, "alerting"); sandbox.clock.tick(1); - sinon.assert.calledOnce(window.close); + sinon.assert.calledOnce(fakeWindow.close); done(); }); }); }); + describe("progress - terminated (previousState not init" + " nor alerting)", function() { it("should set the state to end", function(done) { promise.then(function() { icView._websocket.trigger("progress", { state: "terminated", reason: "media-fail" @@ -516,17 +524,16 @@ describe("loop.conversation", function() sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); }); }); describe("#decline", function() { beforeEach(function() { icView = mountTestComponent(); - sandbox.stub(window, "close"); icView._websocket = { decline: sinon.stub(), close: sinon.stub() }; conversation.set({ windowId: "8699" }); conversation.setIncomingSessionData({ @@ -534,17 +541,17 @@ describe("loop.conversation", function() }); }); it("should close the window", function() { icView.decline(); sandbox.clock.tick(1); - sinon.assert.calledOnce(window.close); + sinon.assert.calledOnce(fakeWindow.close); }); it("should stop alerting", function() { icView.decline(); sinon.assert.calledOnce(navigator.mozLoop.stopAlerting); }); @@ -562,17 +569,16 @@ describe("loop.conversation", function() beforeEach(function() { icView = mountTestComponent(); icView._websocket = { decline: sinon.spy(), close: sinon.stub() }; - sandbox.stub(window, "close"); mozLoop = { LOOP_SESSION_TYPE: { GUEST: 1, FXA: 2 } }; @@ -621,17 +627,17 @@ describe("loop.conversation", function() sinon.assert.calledWithExactly(log, fakeError); }); it("should close the window", function() { icView.declineAndBlock(); sandbox.clock.tick(1); - sinon.assert.calledOnce(window.close); + sinon.assert.calledOnce(fakeWindow.close); }); }); }); describe("Events", function() { var fakeSessionData; beforeEach(function() {
--- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -8,26 +8,33 @@ var expect = chai.expect; var TestUtils = React.addons.TestUtils; var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; describe("loop.panel", function() { "use strict"; - var sandbox, notifications, fakeXHR, requests = []; + var sandbox, notifications, fakeXHR, fakeWindow, requests = []; beforeEach(function(done) { sandbox = sinon.sandbox.create(); fakeXHR = sandbox.useFakeXMLHttpRequest(); requests = []; // https://github.com/cjohansen/Sinon.JS/issues/393 fakeXHR.xhr.onCreate = function (xhr) { requests.push(xhr); + } + + fakeWindow = { + close: sandbox.stub(), + document: { addEventListener: function(){} } }; + loop.shared.mixins.setRootObject(fakeWindow); + notifications = new loop.shared.models.NotificationCollection(); navigator.mozLoop = { doNotDisturb: true, fxAEnabled: true, getStrings: function() { return JSON.stringify({textContent: "fakeText"}); }, @@ -60,16 +67,17 @@ describe("loop.panel", function() { document.mozL10n.initialize(navigator.mozLoop); // XXX prevent a race whenever mozL10n hasn't been initialized yet setTimeout(done, 0); }); afterEach(function() { delete navigator.mozLoop; + loop.shared.mixins.setRootObject(window); sandbox.restore(); }); describe("#init", function() { beforeEach(function() { sandbox.stub(React, "renderComponent"); sandbox.stub(document.mozL10n, "initialize"); sandbox.stub(document.mozL10n, "get").returns("Fake title"); @@ -835,32 +843,42 @@ describe("loop.panel", function() { TestUtils.Simulate.click(deleteButton); sinon.assert.calledOnce(navigator.mozLoop.confirm); sinon.assert.notCalled(dispatcher.dispatch); }); }); describe("Room URL click", function() { - var roomEntry; + + var roomEntry, urlLink; - it("should dispatch an OpenRoom action", function() { + beforeEach(function() { sandbox.stub(dispatcher, "dispatch"); + roomEntry = mountRoomEntry({ dispatcher: dispatcher, room: new loop.store.Room(roomData) }); - var urlLink = roomEntry.getDOMNode().querySelector("p > a"); + urlLink = roomEntry.getDOMNode().querySelector("p > a"); + }); + it("should dispatch an OpenRoom action", function() { TestUtils.Simulate.click(urlLink); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.OpenRoom({roomToken: roomData.roomToken})); }); + + it("should call window.close", function() { + TestUtils.Simulate.click(urlLink); + + sinon.assert.calledOnce(fakeWindow.close); + }); }); describe("Room name updated", function() { it("should update room name", function() { var roomEntry = mountRoomEntry({ dispatcher: dispatcher, room: new loop.store.Room(roomData) });
--- a/browser/components/loop/test/shared/mixins_test.js +++ b/browser/components/loop/test/shared/mixins_test.js @@ -112,16 +112,44 @@ describe("loop.shared.mixins", function( var comp = TestUtils.renderIntoDocument(TestComp()); comp.setTitle("It's a Fake!"); expect(rootObject.document.title).eql("It's a Fake!"); }); }); + + describe("loop.shared.mixins.WindowCloseMixin", function() { + var TestComp, rootObject; + + beforeEach(function() { + rootObject = { + close: sandbox.stub() + }; + sharedMixins.setRootObject(rootObject); + + TestComp = React.createClass({ + mixins: [loop.shared.mixins.WindowCloseMixin], + render: function() { + return React.DOM.div(); + } + }); + }); + + it("should call window.close", function() { + var comp = TestUtils.renderIntoDocument(TestComp()); + + comp.closeWindow(); + + sinon.assert.calledOnce(rootObject.close); + sinon.assert.calledWithExactly(rootObject.close); + }); + }); + describe("loop.shared.mixins.DocumentVisibilityMixin", function() { var comp, TestComp, onDocumentVisibleStub, onDocumentHiddenStub; beforeEach(function() { onDocumentVisibleStub = sandbox.stub(); onDocumentHiddenStub = sandbox.stub(); TestComp = React.createClass({
--- a/browser/modules/AboutHome.jsm +++ b/browser/modules/AboutHome.jsm @@ -138,17 +138,17 @@ let AboutHome = { window.PlacesCommandHook.showPlacesOrganizer("AllBookmarks"); break; case "AboutHome:History": window.PlacesCommandHook.showPlacesOrganizer("History"); break; case "AboutHome:Apps": - window.openUILinkIn("https://marketplace.mozilla.org/", "tab"); + window.BrowserOpenApps(); break; case "AboutHome:Addons": window.BrowserOpenAddonsMgr(); break; case "AboutHome:Sync": let weave = Cc["@mozilla.org/weave/service;1"]
--- a/browser/modules/UITour.jsm +++ b/browser/modules/UITour.jsm @@ -116,25 +116,29 @@ this.UITour = { ["home", {query: "#home-button"}], ["forget", { query: "#panic-button", widgetName: "panic-button", allowAdd: true, }], ["loop", {query: "#loop-button"}], ["loop-newRoom", { + infoPanelPosition: "leftcenter topright", query: (aDocument) => { let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop"); if (!loopBrowser) { return null; } - return loopBrowser.contentDocument.querySelector(".new-room-button"); + // Use the parentElement full-width container of the button so our arrow + // doesn't overlap the panel contents much. + return loopBrowser.contentDocument.querySelector(".new-room-button").parentElement; }, }], ["loop-roomList", { + infoPanelPosition: "leftcenter topright", query: (aDocument) => { let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop"); if (!loopBrowser) { return null; } return loopBrowser.contentDocument.querySelector(".room-list"); }, }], @@ -145,16 +149,17 @@ this.UITour = { return null; } return loopBrowser.contentDocument.querySelector(".signin-link"); }, }], ["privateWindow", {query: "#privatebrowsing-button"}], ["quit", {query: "#PanelUI-quit"}], ["search", { + infoPanelPosition: "after_start", query: "#searchbar", widgetName: "search-container", }], ["searchProvider", { query: (aDocument) => { let searchbar = aDocument.getElementById("searchbar"); if (searchbar.hasAttribute("oneoffui")) { return null; @@ -872,16 +877,17 @@ this.UITour = { node = null; } } else { node = aWindow.document.querySelector(targetQuery); } deferred.resolve({ addTargetListener: targetObject.addTargetListener, + infoPanelPosition: targetObject.infoPanelPosition, node: node, removeTargetListener: targetObject.removeTargetListener, targetName: aTargetName, widgetName: targetObject.widgetName, allowAdd: targetObject.allowAdd, }); }).catch(log.error); return deferred.promise; @@ -1204,20 +1210,23 @@ this.UITour = { tooltipClose.removeEventListener("command", closeButtonCallback); if (aOptions.targetCallbackID && aAnchor.removeTargetListener) { aAnchor.removeTargetListener(document, targetCallback); } }); tooltip.setAttribute("targetName", aAnchor.targetName); tooltip.hidden = false; + let alignment = "bottomcenter topright"; + if (aAnchor.infoPanelPosition) { + alignment = aAnchor.infoPanelPosition; + } + let xOffset = 0, yOffset = 0; - let alignment = "bottomcenter topright"; if (aAnchor.targetName == "search") { - alignment = "after_start"; xOffset = 18; } this._addAnnotationPanelMutationObserver(tooltip); tooltip.openPopup(aAnchorEl, alignment, xOffset, yOffset); if (tooltip.state == "closed") { document.defaultView.addEventListener("endmodalstate", function endModalStateHandler() { document.defaultView.removeEventListener("endmodalstate", endModalStateHandler); tooltip.openPopup(aAnchorEl, alignment);
--- a/browser/modules/test/browser_UITour_loop.js +++ b/browser/modules/test/browser_UITour_loop.js @@ -95,18 +95,38 @@ let tests = [ ok(false, "No more notifications should have arrived"); }); done(); }); document.querySelector("#pinnedchats > chatbox").close(); }); LoopRooms.open("fakeTourRoom"); }, + taskify(function* test_arrow_panel_position() { + ise(loopButton.open, false, "Menu should initially be closed"); + let popup = document.getElementById("UITourTooltip"); + + yield showMenuPromise("loop"); + + let currentTarget = "loop-newRoom"; + yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be on the side"); + is(popup.popupBoxObject.alignmentPosition, "start_before", "Check " + currentTarget + " position"); + + currentTarget = "loop-roomList"; + yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be on the side"); + is(popup.popupBoxObject.alignmentPosition, "start_before", "Check " + currentTarget + " position"); + + currentTarget = "loop-signInUpLink"; + yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath"); + is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position"); + }), ]; +// End tests + function checkLoopPanelIsHidden() { ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up"); ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen"); isnot(loopPanel.state, "open", "The panel shouldn't be open"); is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed"); } if (Services.prefs.getBoolPref("loop.enabled")) {
--- a/browser/modules/test/head.js +++ b/browser/modules/test/head.js @@ -130,16 +130,22 @@ function hideInfoPromise(...args) { } function showInfoPromise(...args) { let popup = document.getElementById("UITourTooltip"); gContentAPI.showInfo.apply(gContentAPI, args); return promisePanelElementShown(window, popup); } +function showMenuPromise(name) { + return new Promise(resolve => { + gContentAPI.showMenu(name, () => resolve()); + }); +} + function waitForCallbackResultPromise() { return waitForConditionPromise(() => { return gContentWindow.callbackResult; }, "callback should be called"); } function addPinnedTabPromise() { gContentAPI.addPinnedTab();
--- a/browser/themes/shared/devtools/dark-theme.css +++ b/browser/themes/shared/devtools/dark-theme.css @@ -379,25 +379,16 @@ div.CodeMirror span.eval-text { color: white; border-bottom: 1px solid #434850; } .theme-tooltip-panel .devtools-tooltip-simple-text:last-child { border-bottom: 0; } -.devtools-horizontal-splitter { - border-bottom: 1px solid var(--theme-splitter-color); -} - -.devtools-side-splitter { - -moz-border-end: 1px solid var(--theme-splitter-color); - border-color: var(--theme-splitter-color); /* Needed for responsive container at low width. */ -} - .devtools-textinput, .devtools-searchinput { background-color: rgba(24, 29, 32, 1); color: rgba(184, 200, 217, 1); } .CodeMirror-Tern-fname { color: #f7f7f7;
--- a/browser/themes/shared/devtools/debugger.inc.css +++ b/browser/themes/shared/devtools/debugger.inc.css @@ -99,21 +99,21 @@ -moz-image-region: rect(0px,64px,32px,32px); } } #sources .black-boxed { color: rgba(128,128,128,0.4); } -#sources .selected > .black-boxed { +#sources .selected .black-boxed { color: rgba(255,255,255,0.4); } -#sources .black-boxed > .dbg-breakpoint { +#sources .black-boxed ~ .dbg-breakpoint { display: none; } /* Debugger unblackbox button */ #black-boxed-message-button > .button-box > .button-icon { width: 16px; height: 16px;
--- a/browser/themes/shared/devtools/light-theme.css +++ b/browser/themes/shared/devtools/light-theme.css @@ -388,25 +388,16 @@ div.CodeMirror span.eval-text { color: black; border-bottom: 1px solid #d9e1e8; } .theme-tooltip-panel .devtools-tooltip-simple-text:last-child { border-bottom: 0; } -.devtools-horizontal-splitter { - border-bottom: 1px solid var(--theme-splitter-color); -} - -.devtools-side-splitter { - -moz-border-end: 1px solid var(--theme-splitter-color); - border-color: var(--theme-splitter-color); /* Needed for responsive container at low width. */ -} - .CodeMirror-hints, .CodeMirror-Tern-tooltip { box-shadow: 0 0 4px rgba(128, 128, 128, .5); background-color: var(--theme-sidebar-background); } %include toolbars.inc.css
--- a/browser/themes/shared/devtools/timeline.inc.css +++ b/browser/themes/shared/devtools/timeline.inc.css @@ -170,17 +170,17 @@ color: #f5f7fa; /* Light foreground text */ } .waterfall-marker-container.selected .waterfall-marker-bullet, .waterfall-marker-container.selected .waterfall-marker-bar { border-color: initial!important; } -#waterfall-details { +#timeline-waterfall-details { -moz-padding-start: 8px; -moz-padding-end: 8px; padding-top: 8vh; overflow: auto; } .marker-details-bullet { width: 8px;
--- a/browser/themes/shared/devtools/toolbars.inc.css +++ b/browser/themes/shared/devtools/toolbars.inc.css @@ -642,17 +642,17 @@ background-image: url("chrome://browser/skin/devtools/command-eyedropper@2x.png"); } } /* Tabs */ .devtools-tabbar { -moz-appearance: none; - min-height: 28px; + min-height: 24px; border: 0px solid; border-bottom-width: 1px; padding: 0; background: var(--theme-tab-toolbar-background); border-bottom-color: var(--theme-splitter-color); } .theme-light .devtools-tabbar { @@ -667,17 +667,17 @@ margin: 0; } .devtools-tab { -moz-appearance: none; -moz-binding: url("chrome://global/content/bindings/general.xml#control-item"); -moz-box-align: center; min-width: 32px; - min-height: 28px; + min-height: 24px; max-width: 127px; margin: 0; padding: 0; border-style: solid; border-width: 0; -moz-border-start-width: 1px; -moz-box-align: center; } @@ -848,8 +848,18 @@ .hidden-labels-box:not(.visible) > label, .hidden-labels-box.visible ~ .hidden-labels-box > label:last-child { display: none; } .devtools-invisible-splitter { border-color: transparent; } + +.devtools-horizontal-splitter { + border-bottom: 1px solid var(--theme-splitter-color); +} + +.devtools-side-splitter { + -moz-border-end: 1px solid var(--theme-splitter-color); + border-color: var(--theme-splitter-color); /* Needed for responsive container at low width. */ +} +
--- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -45,23 +45,30 @@ } @media (max-width: 700px) { .devtools-responsive-container { -moz-box-orient: vertical; } .devtools-responsive-container > .devtools-side-splitter { - border-width: 0; - border-top-width: 1px; - border-top-style: solid; - margin: 0; + /* This is a normally vertical splitter, but we have turned it horizontal + due to the smaller resolution */ min-height: 3px; height: 3px; - margin-bottom: -3px; + margin-top: -3px; + + /* Reset the vertical splitter styles */ + border-width: 0; + border-bottom-width: 1px; + border-bottom-style: solid; + -moz-margin-start: 0; + width: auto; + min-width: 0; + /* In some edge case the cursor is not changed to n-resize */ cursor: n-resize; } .devtools-responsive-container > .devtools-sidebar-tabs { min-height: 35vh; max-height: 75vh; }
--- a/browser/themes/shared/incontentprefs/preferences.inc.css +++ b/browser/themes/shared/incontentprefs/preferences.inc.css @@ -94,16 +94,22 @@ treecol { -moz-image-region: rect(0, 288px, 48px, 240px); } #category-advanced > .category-icon { -moz-image-region: rect(0, 336px, 48px, 288px); } } +@media (max-width: 800px) { + .category-name { + display: none; + } +} + /* header */ #header-advanced { border-bottom: none; padding-bottom: 0; } /* General Pane */
--- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -1083,21 +1083,16 @@ nsDocumentViewer::PermitUnloadInternal(b static bool sBeforeUnloadPrefCached = false; if (!sBeforeUnloadPrefCached ) { sBeforeUnloadPrefCached = true; Preferences::AddBoolVarCache(&sIsBeforeUnloadDisabled, BEFOREUNLOAD_DISABLED_PREFNAME); } - // If the user has turned off onbeforeunload warnings, no need to check. - if (sIsBeforeUnloadDisabled) { - return NS_OK; - } - // First, get the script global object from the document... nsPIDOMWindow *window = mDocument->GetWindow(); if (!window) { // This is odd, but not fatal NS_WARNING("window not set for document!"); return NS_OK; } @@ -1142,18 +1137,20 @@ nsDocumentViewer::PermitUnloadInternal(b if (dialogsWereEnabled) { utils->EnableDialogs(); } } nsCOMPtr<nsIDocShell> docShell(mContainer); nsAutoString text; beforeUnload->GetReturnValue(text); - if (*aShouldPrompt && (event->GetInternalNSEvent()->mFlags.mDefaultPrevented || - !text.IsEmpty())) { + + if (!sIsBeforeUnloadDisabled && *aShouldPrompt && + (event->GetInternalNSEvent()->mFlags.mDefaultPrevented || + !text.IsEmpty())) { // Ask the user if it's ok to unload the current page nsCOMPtr<nsIPrompt> prompt = do_GetInterface(docShell); if (prompt) { nsCOMPtr<nsIWritablePropertyBag2> promptBag = do_QueryInterface(prompt); if (promptBag) { bool isTabModalPromptAllowed;
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -2192,17 +2192,17 @@ public class BrowserApp extends GeckoApp @Override public void run() { final LocaleManager localeManager = BrowserLocaleManager.getInstance(); final Locale locale = localeManager.getCurrentLocale(getApplicationContext()); Log.d(LOGTAG, "Read persisted locale " + locale); if (locale == null) { return; } - onLocaleChanged(BrowserLocaleManager.getLanguageTag(locale)); + onLocaleChanged(Locales.getLanguageTag(locale)); } }); break; default: super.onActivityResult(requestCode, resultCode, data); } } @@ -2925,17 +2925,17 @@ public class BrowserApp extends GeckoApp // we might need to redisplay based on a locale change. startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES); return true; } if (itemId == R.id.help) { final String VERSION = AppConstants.MOZ_APP_VERSION; final String OS = AppConstants.OS_TARGET; - final String LOCALE = BrowserLocaleManager.getLanguageTag(Locale.getDefault()); + final String LOCALE = Locales.getLanguageTag(Locale.getDefault()); final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE); Tabs.getInstance().loadUrlInTab(URL); return true; } if (itemId == R.id.addons) { Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
--- a/mobile/android/base/BrowserLocaleManager.java +++ b/mobile/android/base/BrowserLocaleManager.java @@ -76,74 +76,16 @@ public class BrowserLocaleManager implem } @Override public boolean isEnabled() { return AppConstants.MOZ_LOCALE_SWITCHER; } /** - * Sometimes we want just the language for a locale, not the entire - * language tag. But Java's .getLanguage method is wrong. - * - * This method is equivalent to the first part of {@link #getLanguageTag(Locale)}. - * - * @return a language string, such as "he" for the Hebrew locales. - */ - public static String getLanguage(final Locale locale) { - final String language = locale.getLanguage(); // Can, but should never be, an empty string. - // Modernize certain language codes. - if (language.equals("iw")) { - return "he"; - } - - if (language.equals("in")) { - return "id"; - } - - if (language.equals("ji")) { - return "yi"; - } - - return language; - } - - /** - * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} - * stringifies as "es_ES". - * - * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>. - * - * @return a locale string suitable for passing to Gecko. - */ - public static String getLanguageTag(final Locale locale) { - // If this were Java 7: - // return locale.toLanguageTag(); - - final String language = getLanguage(locale); - final String country = locale.getCountry(); // Can be an empty string. - if (country.equals("")) { - return language; - } - return language + "-" + country; - } - - public static Locale parseLocaleCode(final String localeCode) { - int index; - if ((index = localeCode.indexOf('-')) != -1 || - (index = localeCode.indexOf('_')) != -1) { - final String langCode = localeCode.substring(0, index); - final String countryCode = localeCode.substring(index + 1); - return new Locale(langCode, countryCode); - } else { - return new Locale(localeCode); - } - } - - /** * Ensure that you call this early in your application startup, * and with a context that's sufficiently long-lived (typically * the application context). * * Calling multiple times is harmless. */ @Override public void initialize(final Context context) { @@ -270,17 +212,17 @@ public class BrowserLocaleManager implem return; } // Store the Java-native form. prefs.edit().putString("osLocale", osLocaleString).apply(); // The value we send to Gecko should be a language tag, not // a Java locale string. - final String osLanguageTag = BrowserLocaleManager.getLanguageTag(osLocale); + final String osLanguageTag = Locales.getLanguageTag(osLocale); final GeckoEvent localeOSEvent = GeckoEvent.createBroadcastEvent("Locale:OS", osLanguageTag); GeckoAppShell.sendEventToGecko(localeOSEvent); } @Override public String getAndApplyPersistedLocale(Context context) { initialize(context); @@ -316,17 +258,17 @@ public class BrowserLocaleManager implem // We always persist and notify Gecko, even if nothing seemed to // change. This might happen if you're picking a locale that's the same // as the current OS locale. The OS locale might change next time we // launch, and we need the Gecko pref and persisted locale to have been // set by the time that happens. persistLocale(context, localeCode); // Tell Gecko. - GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context))); + GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context))); GeckoAppShell.sendEventToGecko(ev); return resultant; } @Override public void resetToSystemLocale(Context context) { // Wipe the pref. @@ -384,17 +326,17 @@ public class BrowserLocaleManager implem if (currentLocale != null) { return currentLocale; } final String current = getPersistedLocale(context); if (current == null) { return null; } - return currentLocale = parseLocaleCode(current); + return currentLocale = Locales.parseLocaleCode(current); } /** * Updates the Java locale and the Android configuration. * * Returns the persisted locale if it differed. * * Does not notify Gecko. @@ -404,17 +346,17 @@ public class BrowserLocaleManager implem */ private String updateLocale(Context context, String localeCode) { // Fast path. final Locale defaultLocale = Locale.getDefault(); if (defaultLocale.toString().equals(localeCode)) { return null; } - final Locale locale = parseLocaleCode(localeCode); + final Locale locale = Locales.parseLocaleCode(localeCode); return updateLocale(context, locale); } /** * @return the Java locale string: e.g., "en_US". */ private String updateLocale(Context context, final Locale locale) { @@ -490,12 +432,13 @@ public class BrowserLocaleManager implem return null; } } /** * @return the single default locale baked into this application. * Applicable when there is no multilocale.json present. */ - public static String getFallbackLocaleTag() { + @SuppressWarnings("static-method") + public String getFallbackLocaleTag() { return FALLBACK_LOCALE_TAG; } }
--- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -1378,17 +1378,17 @@ public abstract class GeckoApp * response to device changes. */ @Override public void onLocaleReady(final String locale) { if (!ThreadUtils.isOnUiThread()) { throw new RuntimeException("onLocaleReady must always be called from the UI thread."); } - final Locale loc = BrowserLocaleManager.parseLocaleCode(locale); + final Locale loc = Locales.parseLocaleCode(locale); if (loc.equals(mLastLocale)) { Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do."); } // The URL bar hint needs to be populated. TextView urlBar = (TextView) findViewById(R.id.url_bar_title); if (urlBar != null) { final String hint = getResources().getString(R.string.url_bar_default_text); @@ -2114,17 +2114,17 @@ public abstract class GeckoApp @Override public void onConfigurationChanged(Configuration newConfig) { Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); final LocaleManager localeManager = BrowserLocaleManager.getInstance(); final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale); if (changed != null) { - onLocaleChanged(BrowserLocaleManager.getLanguageTag(changed)); + onLocaleChanged(Locales.getLanguageTag(changed)); } // onConfigurationChanged is not called for 180 degree orientation changes, // we will miss such rotations and the screen orientation will not be // updated. if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) { if (mFormAssistPopup != null) mFormAssistPopup.hide();
--- a/mobile/android/base/LocaleManager.java +++ b/mobile/android/base/LocaleManager.java @@ -33,9 +33,10 @@ public interface LocaleManager { /** * Call this in your onConfigurationChanged handler. This method is expected * to do the appropriate thing: if the user has selected a locale, it * corrects the incoming configuration; if not, it signals the new locale to * use. */ Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale); + String getFallbackLocaleTag(); }
rename from mobile/android/base/LocaleAware.java rename to mobile/android/base/Locales.java --- a/mobile/android/base/LocaleAware.java +++ b/mobile/android/base/Locales.java @@ -1,52 +1,116 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; +import java.util.Locale; + import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.LocaleManager; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.StrictMode; import android.support.v4.app.FragmentActivity; /** - * This is a helper class to do typical locale switching operations - * without hitting StrictMode errors or adding boilerplate to common - * activity subclasses. + * This is a helper class to do typical locale switching operations without + * hitting StrictMode errors or adding boilerplate to common activity + * subclasses. * - * Either call {@link LocaleAware#initializeLocale(Context)} in your - * <code>onCreate</code> method, or inherit from <code>LocaleAwareFragmentActivity</code> - * or <code>LocaleAwareActivity</code>. + * Either call {@link Locales#initializeLocale(Context)} in your + * <code>onCreate</code> method, or inherit from + * <code>LocaleAwareFragmentActivity</code> or <code>LocaleAwareActivity</code>. */ -public class LocaleAware { - public static void initializeLocale(Context context) { - final LocaleManager localeManager = BrowserLocaleManager.getInstance(); - final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - StrictMode.allowThreadDiskWrites(); - try { - localeManager.getAndApplyPersistedLocale(context); - } finally { - StrictMode.setThreadPolicy(savedPolicy); +public class Locales { + public static void initializeLocale(Context context) { + final LocaleManager localeManager = BrowserLocaleManager.getInstance(); + final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); + StrictMode.allowThreadDiskWrites(); + try { + localeManager.getAndApplyPersistedLocale(context); + } finally { + StrictMode.setThreadPolicy(savedPolicy); + } + } + + public static class LocaleAwareFragmentActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + Locales.initializeLocale(getApplicationContext()); + super.onCreate(savedInstanceState); + } + } + + public static class LocaleAwareActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + Locales.initializeLocale(getApplicationContext()); + super.onCreate(savedInstanceState); + } } - } + + /** + * Sometimes we want just the language for a locale, not the entire language + * tag. But Java's .getLanguage method is wrong. + * + * This method is equivalent to the first part of + * {@link Locales#getLanguageTag(Locale)}. + * + * @return a language string, such as "he" for the Hebrew locales. + */ + public static String getLanguage(final Locale locale) { + // Can, but should never be, an empty string. + final String language = locale.getLanguage(); - public static class LocaleAwareFragmentActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - LocaleAware.initializeLocale(getApplicationContext()); - super.onCreate(savedInstanceState); + // Modernize certain language codes. + if (language.equals("iw")) { + return "he"; + } + + if (language.equals("in")) { + return "id"; + } + + if (language.equals("ji")) { + return "yi"; + } + + return language; } - } + + /** + * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} + * stringifies as "es_ES". + * + * This method approximates the Java 7 method + * <code>Locale#toLanguageTag()</code>. + * + * @return a locale string suitable for passing to Gecko. + */ + public static String getLanguageTag(final Locale locale) { + // If this were Java 7: + // return locale.toLanguageTag(); - public static class LocaleAwareActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - LocaleAware.initializeLocale(getApplicationContext()); - super.onCreate(savedInstanceState); + final String language = getLanguage(locale); + final String country = locale.getCountry(); // Can be an empty string. + if (country.equals("")) { + return language; + } + return language + "-" + country; } - } + + public static Locale parseLocaleCode(final String localeCode) { + int index; + if ((index = localeCode.indexOf('-')) != -1 || + (index = localeCode.indexOf('_')) != -1) { + final String langCode = localeCode.substring(0, index); + final String countryCode = localeCode.substring(index + 1); + return new Locale(langCode, countryCode); + } + + return new Locale(localeCode); + } }
--- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -55,34 +55,58 @@ GARBAGE += \ javah.out \ jni-stubs.inc \ GeneratedJNIWrappers.cpp \ GeneratedJNIWrappers.h \ $(NULL) GARBAGE_DIRS += classes db jars res sync services generated -JAVA_BOOTCLASSPATH = \ +# The bootclasspath is functionally identical to the classpath, but allows the +# classes given to redefine classes in core packages, such as java.lang. +# android.jar is here as it provides Android's definition of the Java Standard +# Library. The compatability lib here tweaks a few of the core classes to paint +# over changes in behaviour between versions. +JAVA_BOOTCLASSPATH := \ $(ANDROID_SDK)/android.jar \ $(ANDROID_COMPAT_LIB) \ $(NULL) JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH))) -# If native devices are enabled, add Google Play Services and some of the v7 compat libraries +# If native devices are enabled, add Google Play Services and some of the v7 +# compat libraries. ifdef MOZ_NATIVE_DEVICES JAVA_CLASSPATH += \ $(GOOGLE_PLAY_SERVICES_LIB) \ $(ANDROID_MEDIAROUTER_LIB) \ $(ANDROID_APPCOMPAT_LIB) \ $(NULL) endif JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH))) +# Library jars that we're bundling: these are subject to Proguard before inclusion +# into classes.dex. +java_bundled_libs := \ + $(ANDROID_COMPAT_LIB) \ + $(NULL) + +ifdef MOZ_NATIVE_DEVICES + java_bundled_libs += \ + $(GOOGLE_PLAY_SERVICES_LIB) \ + $(ANDROID_MEDIAROUTER_LIB) \ + $(ANDROID_APPCOMPAT_LIB) \ + $(NULL) +endif + +java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs))) + +# All the jars we're compiling from source. (not to be confused with +# java_bundled_libs, which holds the jars which we're including as binaries). ALL_JARS = \ constants.jar \ gecko-R.jar \ gecko-browser.jar \ gecko-mozglue.jar \ gecko-thirdparty.jar \ gecko-util.jar \ sync-thirdparty.jar \ @@ -97,75 +121,95 @@ ALL_JARS += search-activity.jar endif ifdef MOZ_ANDROID_MLS_STUMBLER extra_packages += org.mozilla.mozstumbler ALL_JARS += ../stumbler/stumbler.jar generated/org/mozilla/mozstumbler/R.java: .aapt.deps ; endif +# The list of jars in Java classpath notation (colon-separated). +all_jars_classpath := $(subst $(NULL) ,:,$(strip $(ALL_JARS))) + include $(topsrcdir)/config/config.mk -# Note that we're going to set up a dependency directly between embed_android.dex and the java files -# Instead of on the .class files, since more than one .class file might be produced per .java file -# Sync dependencies are provided in a single jar. Sync classes themselves are delivered as source, -# because Android resource classes must be compiled together in order to avoid overlapping resource -# indices. - -library_jars = \ - $(JAVA_CLASSPATH) \ - $(JAVA_BOOTCLASSPATH) \ +library_jars := \ + $(ANDROID_SDK)/android.jar \ $(NULL) library_jars := $(subst $(NULL) ,:,$(strip $(library_jars))) classes.dex: .proguard.deps $(REPORT_BUILD) - $(DX) --dex --output=classes.dex jars-proguarded $(subst :, ,$(ANDROID_COMPAT_LIB):$(JAVA_CLASSPATH)) + $(DX) --dex --output=classes.dex jars-proguarded ifdef MOZ_DISABLE_PROGUARD PROGUARD_PASSES=0 else ifdef MOZ_DEBUG PROGUARD_PASSES=1 else ifndef MOZILLA_OFFICIAL PROGUARD_PASSES=1 else PROGUARD_PASSES=6 endif endif endif +proguard_config_dir=$(topsrcdir)/mobile/android/config/proguard + # This stanza ensures that the set of GeckoView classes does not depend on too # much of Fennec, where "too much" is defined as the set of potentially # non-GeckoView classes that GeckoView already depended on at a certain point in # time. The idea is to set a high-water mark that is not to be crossed. classycle_jar := $(topsrcdir)/mobile/android/build/classycle/classycle-1.4.1.jar .geckoview.deps: geckoview.ddf $(classycle_jar) $(ALL_JARS) java -cp $(classycle_jar) \ classycle.dependency.DependencyChecker \ -mergeInnerClasses \ -dependencies=@$< \ $(ALL_JARS) @$(TOUCH) $@ -# We touch the target file before invoking Proguard so that Proguard's -# outputs are fresher than the target, preventing a subsequent -# invocation from thinking Proguard's outputs are stale. This is safe -# because Make removes the target file if any recipe command fails. -.proguard.deps: .geckoview.deps $(ALL_JARS) $(topsrcdir)/mobile/android/config/proguard.cfg +# First, we delete debugging information from libraries. Having line-number +# information for libraries for which we lack the source isn't useful, so this +# saves us a bit of space. Importantly, Proguard has a bug causing it to +# sometimes corrupt this information if present (which it does for some of the +# included libraries). This corruption prevents dex from completing, so we need +# to get rid of it. This prevents us from seeing line numbers in stack traces +# for stack frames inside libraries. +# +# This step can occur much earlier than the main Proguard pass: it needs only +# gecko-R.jar to have been compiled (as that's where the library R.java files +# end up), but it does block the main Proguard pass. +.bundled.proguard.deps: gecko-R.jar $(proguard_config_dir)/strip-libs.cfg $(REPORT_BUILD) @$(TOUCH) $@ java \ -Xmx512m -Xms128m \ -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \ - @$(topsrcdir)/mobile/android/config/proguard.cfg \ + @$(proguard_config_dir)/strip-libs.cfg \ + -injars $(subst ::,:,$(java_bundled_libs))\ + -outjars bundled-jars-nodebug \ + -libraryjars $(library_jars):gecko-R.jar + +# We touch the target file before invoking Proguard so that Proguard's +# outputs are fresher than the target, preventing a subsequent +# invocation from thinking Proguard's outputs are stale. This is safe +# because Make removes the target file if any recipe command fails. +.proguard.deps: .geckoview.deps .bundled.proguard.deps $(ALL_JARS) $(proguard_config_dir)/proguard.cfg + $(REPORT_BUILD) + @$(TOUCH) $@ + java \ + -Xmx512m -Xms128m \ + -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \ + @$(proguard_config_dir)/proguard.cfg \ -optimizationpasses $(PROGUARD_PASSES) \ - -injars $(subst ::,:,$(subst $(NULL) ,:,$(strip $(ALL_JARS)))) \ + -injars $(subst ::,:,$(all_jars_classpath)):bundled-jars-nodebug \ -outjars jars-proguarded \ -libraryjars $(library_jars) CLASSES_WITH_JNI= \ org.mozilla.gecko.ANRReporter \ org.mozilla.gecko.GeckoAppShell \ org.mozilla.gecko.GeckoJavaSampler \ org.mozilla.gecko.gfx.NativePanZoomController \
--- a/mobile/android/base/db/SuggestedSites.java +++ b/mobile/android/base/db/SuggestedSites.java @@ -29,20 +29,19 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Scanner; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; - -import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.R; import org.mozilla.gecko.distribution.Distribution; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.util.RawResource; import org.mozilla.gecko.util.ThreadUtils; @@ -329,17 +328,17 @@ public class SuggestedSites { * current locale or with the fallback locale (en-US). * * It's assumed that the given distribution instance is ready to be * used and exists. */ static Map<String, Site> loadFromDistribution(Distribution dist) { for (Locale locale : getAcceptableLocales()) { try { - final String languageTag = BrowserLocaleManager.getLanguageTag(locale); + final String languageTag = Locales.getLanguageTag(locale); final String path = String.format("suggestedsites/locales/%s/%s", languageTag, FILENAME); final File f = dist.getDistributionFile(path); if (f == null) { Log.d(LOGTAG, "No suggested sites for locale: " + languageTag); continue; }
--- a/mobile/android/base/fxa/activities/FxAccountAbstractActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountAbstractActivity.java @@ -4,17 +4,17 @@ package org.mozilla.gecko.fxa.activities; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.sync.setup.activities.ActivityUtils; -import org.mozilla.gecko.LocaleAware.LocaleAwareActivity; +import org.mozilla.gecko.Locales.LocaleAwareActivity; import android.accounts.Account; import android.app.Activity; import android.content.Intent; import android.os.SystemClock; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView;
--- a/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountGetStartedActivity.java @@ -8,17 +8,17 @@ import java.util.Locale; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper; import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.sync.setup.activities.ActivityUtils; -import org.mozilla.gecko.LocaleAware; +import org.mozilla.gecko.Locales; import android.accounts.AccountAuthenticatorActivity; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; @@ -34,17 +34,17 @@ public class FxAccountGetStartedActivity /** * {@inheritDoc} */ @Override public void onCreate(Bundle icicle) { Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); Logger.debug(LOG_TAG, "onCreate(" + icicle + ")"); - LocaleAware.initializeLocale(getApplicationContext()); + Locales.initializeLocale(getApplicationContext()); super.onCreate(icicle); setContentView(R.layout.fxaccount_get_started); linkifyOldFirefoxLink(); View button = findViewById(R.id.get_started_button);
--- a/mobile/android/base/fxa/activities/FxAccountStatusActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusActivity.java @@ -1,16 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.fxa.activities; import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity; +import org.mozilla.gecko.Locales.LocaleAwareFragmentActivity; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.sync.Utils; import android.accounts.Account; import android.accounts.AccountManager;
--- a/mobile/android/base/home/TopSitesPanel.java +++ b/mobile/android/base/home/TopSitesPanel.java @@ -10,18 +10,18 @@ import static org.mozilla.gecko.db.URLMe import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.R; import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tabs; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.BrowserContract.Thumbnails; import org.mozilla.gecko.db.BrowserContract.TopSites; import org.mozilla.gecko.db.BrowserDB; @@ -233,17 +233,17 @@ public class TopSitesPanel extends HomeF method = TelemetryContract.Method.GRID_ITEM; } Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, Integer.toString(position)); // Record tile click events on non-private tabs. final Tab tab = Tabs.getInstance().getSelectedTab(); if (!tab.isPrivate()) { final Locale locale = Locale.getDefault(); - final String localeTag = BrowserLocaleManager.getLanguageTag(locale); + final String localeTag = Locales.getLanguageTag(locale); mTilesRecorder.recordAction(tab, TilesRecorder.ACTION_CLICK, position, getTilesSnapshot(), localeTag); } mUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); } } else { if (mEditPinnedSiteListener != null) { mEditPinnedSiteListener.onEditPinnedSite(position, "");
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -321,18 +321,18 @@ gbjar.sources += [ 'home/TopSitesGridView.java', 'home/TopSitesPanel.java', 'home/TopSitesThumbnailView.java', 'home/TransitionAwareCursorLoaderCallbacks.java', 'home/TwoLinePageRow.java', 'InputMethods.java', 'IntentHelper.java', 'JavaAddonManager.java', - 'LocaleAware.java', 'LocaleManager.java', + 'Locales.java', 'lwt/LightweightTheme.java', 'lwt/LightweightThemeDrawable.java', 'MediaCastingBar.java', 'MemoryMonitor.java', 'menu/GeckoMenu.java', 'menu/GeckoMenuInflater.java', 'menu/GeckoMenuItem.java', 'menu/GeckoSubMenu.java',
--- a/mobile/android/base/newtablet/res/layout-large-v11/tab_strip.xml +++ b/mobile/android/base/newtablet/res/layout-large-v11/tab_strip.xml @@ -5,17 +5,17 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android"> <org.mozilla.gecko.tabs.TabStripView android:id="@+id/tab_strip" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" - android:paddingTop="4dp"/> + android:paddingTop="8dp"/> <!-- The right margin creates a "dead area" on the right side of the button which we compensate for with a touch delegate. See TabStrip --> <ImageButton android:id="@+id/add_tab" style="@style/UrlBar.ImageButton" android:layout_width="@dimen/new_tablet_tab_strip_height" android:src="@drawable/tab_new_level"
--- a/mobile/android/base/overlays/ui/ShareDialog.java +++ b/mobile/android/base/overlays/ui/ShareDialog.java @@ -5,17 +5,17 @@ package org.mozilla.gecko.overlays.ui; import java.net.URISyntaxException; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.Assert; import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.LocaleAware; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.R; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.LocalBrowserDB; import org.mozilla.gecko.overlays.OverlayConstants; import org.mozilla.gecko.overlays.service.OverlayActionService; import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord; import org.mozilla.gecko.overlays.service.sharemethods.SendTab; @@ -45,17 +45,17 @@ import android.view.animation.AnimationU import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; /** * A transparent activity that displays the share overlay. */ -public class ShareDialog extends LocaleAware.LocaleAwareActivity implements SendTabTargetSelectedListener { +public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabTargetSelectedListener { private static final String LOGTAG = "GeckoShareDialog"; private String url; private String title; // The override intent specified by SendTab (if any). See SendTab.java. private Intent sendTabOverrideIntent;
--- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -7,31 +7,33 @@ package org.mozilla.gecko.preferences; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import android.os.Build; + import org.json.JSONObject; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.BrowserApp; import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.DataReportingNotification; import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.GeckoActivityStatus; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoApplication; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.GuestSession; import org.mozilla.gecko.LocaleManager; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.NewTabletUI; import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.R; import org.mozilla.gecko.RestrictedProfiles; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract.Method; import org.mozilla.gecko.background.common.GlobalConstants; @@ -1009,17 +1011,17 @@ OnSharedPreferenceChangeListener * * Note that this listener is not always registered: we use it only on * tablets, Honeycomb and up, where we'll have a multi-pane view and prefs * changing multiple times. */ @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PREFS_BROWSER_LOCALE.equals(key)) { - onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale), + onLocaleSelected(Locales.getLanguageTag(lastLocale), sharedPreferences.getString(key, null)); } else if (PREFS_SUGGESTED_SITES.equals(key)) { refreshSuggestedSites(); } else if (PREFS_NEW_TABLET_UI.equals(key)) { Toast.makeText(this, R.string.new_tablet_restart, Toast.LENGTH_SHORT).show(); } } @@ -1049,17 +1051,17 @@ OnSharedPreferenceChangeListener // We don't want the "use master password" pref to change until the // user has gone through the dialog. return false; } if (PREFS_BROWSER_LOCALE.equals(prefName)) { // Even though this is a list preference, we don't want to handle it // below, so we return here. - return onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale), (String) newValue); + return onLocaleSelected(Locales.getLanguageTag(lastLocale), (String) newValue); } if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) { setCharEncodingState(((String) newValue).equals("true")); } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) { UpdateServiceHelper.registerForUpdates(this, (String) newValue); } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) { // The healthreport pref only lives in Android, so we do not persist
--- a/mobile/android/base/preferences/LocaleListPreference.java +++ b/mobile/android/base/preferences/LocaleListPreference.java @@ -9,16 +9,17 @@ import java.text.Collator; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Locale; import java.util.Set; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.BrowserLocaleManager; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.R; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.preference.ListPreference; import android.text.TextUtils; @@ -112,17 +113,17 @@ public class LocaleListPreference extend private static final class LocaleDescriptor implements Comparable<LocaleDescriptor> { // We use Locale.US here to ensure a stable ordering of entries. private static final Collator COLLATOR = Collator.getInstance(Locale.US); public final String tag; private final String nativeName; public LocaleDescriptor(String tag) { - this(BrowserLocaleManager.parseLocaleCode(tag), tag); + this(Locales.parseLocaleCode(tag), tag); } public LocaleDescriptor(Locale locale, String tag) { this.tag = tag; final String displayName = locale.getDisplayName(locale); if (TextUtils.isEmpty(displayName)) { // There's nothing sane we can do. @@ -216,17 +217,17 @@ public class LocaleListPreference extend * * This method filters down the list before generating the descriptor array. */ private LocaleDescriptor[] getUsableLocales() { Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext()); // Future: single-locale builds should be specified, too. if (shippingLocales == null) { - final String fallbackTag = BrowserLocaleManager.getFallbackLocaleTag(); + final String fallbackTag = BrowserLocaleManager.getInstance().getFallbackLocaleTag(); return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) }; } final int initialCount = shippingLocales.size(); final Set<LocaleDescriptor> locales = new HashSet<LocaleDescriptor>(initialCount); for (String tag : shippingLocales) { final LocaleDescriptor descriptor = new LocaleDescriptor(tag); @@ -257,17 +258,17 @@ public class LocaleListPreference extend BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale); } private Locale getSelectedLocale() { final String tag = getValue(); if (tag == null || tag.equals("")) { return Locale.getDefault(); } - return BrowserLocaleManager.parseLocaleCode(tag); + return Locales.parseLocaleCode(tag); } @Override public CharSequence getSummary() { final String value = getValue(); if (TextUtils.isEmpty(value)) { return getContext().getString(R.string.locale_system_default);
--- a/mobile/android/base/resources/values/dimens.xml +++ b/mobile/android/base/resources/values/dimens.xml @@ -17,17 +17,17 @@ <dimen name="browser_toolbar_favicon_size">21.33dip</dimen> <dimen name="browser_toolbar_shadow_size">2dp</dimen> <!-- If you update one of these values, update the others. --> <dimen name="new_tablet_nav_button_width">42dp</dimen> <dimen name="new_tablet_nav_button_width_half">21dp</dimen> <dimen name="new_tablet_nav_button_width_plus_half">63dp</dimen> - <dimen name="new_tablet_tab_strip_height">44dp</dimen> + <dimen name="new_tablet_tab_strip_height">48dp</dimen> <dimen name="new_tablet_tab_strip_item_width">208dp</dimen> <dimen name="new_tablet_tab_strip_item_margin">-28dp</dimen> <dimen name="new_tablet_tab_strip_favicon_size">16dp</dimen> <dimen name="new_tablet_tab_strip_fading_edge_size">15dp</dimen> <dimen name="new_tablet_site_security_height">60dp</dimen> <dimen name="new_tablet_site_security_width">34dp</dimen> <!-- We primarily use padding (instead of margins) to increase the hit area. --> <dimen name="new_tablet_site_security_padding_vertical">21dp</dimen> @@ -133,16 +133,17 @@ <dimen name="url_bar_offset_left">32dp</dimen> <dimen name="history_tab_indicator_height">50dp</dimen> <dimen name="new_tablet_tab_thumbnail_width">168dp</dimen> <dimen name="new_tablet_tab_thumbnail_height">140dp</dimen> <dimen name="new_tablet_tab_panel_column_width">178dp</dimen> <dimen name="new_tablet_tab_panel_grid_padding">19dp</dimen> + <dimen name="new_tablet_tab_panel_grid_vspacing">21dp</dimen> <dimen name="new_tablet_tab_panel_grid_padding_top">24dp</dimen> <dimen name="new_tablet_tab_highlight_stroke_width">5dp</dimen> <!-- PageActionButtons dimensions --> <dimen name="page_action_button_width">32dp</dimen> <!-- Banner -->
--- a/mobile/android/base/resources/values/styles.xml +++ b/mobile/android/base/resources/values/styles.xml @@ -197,17 +197,17 @@ <style name="Widget.TabsGridLayout" parent="Widget.GridView"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">match_parent</item> <item name="android:paddingTop">0dp</item> <item name="android:stretchMode">columnWidth</item> <item name="android:numColumns">auto_fit</item> <item name="android:columnWidth">@dimen/tabs_grid_view_column_width</item> <item name="android:horizontalSpacing">2dp</item> - <item name="android:verticalSpacing">21dp</item> + <item name="android:verticalSpacing">@dimen/new_tablet_tab_panel_grid_vspacing</item> <item name="android:drawSelectorOnTop">true</item> <item name="android:clipToPadding">false</item> </style> <style name="Widget.BookmarkItemView" parent="Widget.TwoLinePageRow"/> <style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
--- a/mobile/android/base/sync/setup/activities/SendTabActivity.java +++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java @@ -22,17 +22,17 @@ import org.mozilla.gecko.sync.CommandPro import org.mozilla.gecko.sync.CommandRunner; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConstants; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.setup.SyncAccounts; -import org.mozilla.gecko.LocaleAware.LocaleAwareActivity; +import org.mozilla.gecko.Locales.LocaleAwareActivity; import org.mozilla.gecko.sync.syncadapter.SyncAdapter; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences;
--- a/mobile/android/base/tabs/PrivateTabsPanel.java +++ b/mobile/android/base/tabs/PrivateTabsPanel.java @@ -2,17 +2,17 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.tabs; import java.util.Locale; -import org.mozilla.gecko.BrowserLocaleManager; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.NewTabletUI; import org.mozilla.gecko.R; import org.mozilla.gecko.Tabs; import org.mozilla.gecko.tabs.TabsPanel.CloseAllPanelView; import org.mozilla.gecko.tabs.TabsPanel.TabsLayout; import android.content.Context; import android.content.res.Resources; @@ -53,17 +53,17 @@ class PrivateTabsPanel extends FrameLayo emptyTabsFrame = (LinearLayout) findViewById(R.id.private_tabs_empty); tabsLayout.setEmptyView(emptyTabsFrame); final View learnMore = findViewById(R.id.private_tabs_learn_more); learnMore.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - final String locale = BrowserLocaleManager.getLanguageTag(Locale.getDefault()); + final String locale = Locales.getLanguageTag(Locale.getDefault()); final String url = getResources().getString(R.string.private_tabs_panel_learn_more_link, locale); Tabs.getInstance().loadUrlInTab(url); if (tabsPanel != null) { tabsPanel.autoHidePanel(); } } });
--- a/mobile/android/base/tabs/TabsGridLayout.java +++ b/mobile/android/base/tabs/TabsGridLayout.java @@ -159,17 +159,18 @@ class TabsGridLayout extends GridView final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1); final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0); if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) { // We need to set the view's bottom padding to prevent a sudden jump as the // last item in the row is being removed. We then need to remove the padding // via a sweet animation final int removedHeight = getChildAt(0).getMeasuredHeight(); - final int verticalSpacing = getVerticalSpacing(); + final int verticalSpacing = + getResources().getDimensionPixelOffset(R.dimen.new_tablet_tab_panel_grid_vspacing); ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom()); paddingAnimator.setDuration(ANIM_TIME_MS * 2); paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {
--- a/mobile/android/base/tests/testOSLocale.java +++ b/mobile/android/base/tests/testOSLocale.java @@ -5,16 +5,17 @@ package org.mozilla.gecko.tests; import java.util.Locale; import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.PrefsHelper; import android.content.SharedPreferences; public class testOSLocale extends BaseTest { @Override public void setUp() throws Exception { @@ -85,17 +86,17 @@ public class testOSLocale extends BaseTe // If we cleared the pref above prior to BrowserApp's delayed init, or our Gecko // profile has been used before, then we're already going to be set up for en-US. // // If we cleared the pref after the initial broadcast, and our Android-side profile // has been used before but the Gecko profile is clean, then the Gecko prefs won't // have been set. // // Instead, we always send a new locale code, and see what we get. - final Locale fr = BrowserLocaleManager.parseLocaleCode("fr"); + final Locale fr = Locales.parseLocaleCode("fr"); BrowserLocaleManager.storeAndNotifyOSLocale(prefs, fr); state.fetch(); mAsserter.is(state.osLocale, "fr", "We're in fr."); // Now we can see what the expected Accept-Languages header should be. // The OS locale is 'fr', so we have our app locale (en-US), @@ -120,17 +121,17 @@ public class testOSLocale extends BaseTe // Expected, from es-ES's intl.properties: final String EXPECTED = SELECTED_LOCALES + (isMultiLocaleBuild ? "es,en-us,en" : // Expected, from es-ES's intl.properties. "en-us,en"); // Expected, from en-US (the default). mAsserter.is(state.acceptLanguages, EXPECTED, "We have the right es-ES+fr Accept-Languages for this build."); // And back to en-US. - final Locale en_US = BrowserLocaleManager.parseLocaleCode("en-US"); + final Locale en_US = Locales.parseLocaleCode("en-US"); BrowserLocaleManager.storeAndNotifyOSLocale(prefs, en_US); BrowserLocaleManager.getInstance().resetToSystemLocale(getActivity()); state.fetch(); mAsserter.is(state.osLocale, "en-US", "We're in en-US."); mAsserter.is(state.acceptLanguages, "en-us,en", "We have the default processed en-US Accept-Languages."); }
--- a/mobile/android/base/widget/ResizablePathDrawable.java +++ b/mobile/android/base/widget/ResizablePathDrawable.java @@ -30,16 +30,17 @@ public class ResizablePathDrawable exten this.colorStateList = colorStateList; updateColor(getState()); } private boolean updateColor(int[] stateSet) { int newColor = colorStateList.getColorForState(stateSet, Color.WHITE); if (newColor != currentColor) { currentColor = newColor; + alpha = Color.alpha(currentColor); invalidateSelf(); return true; } return false; } public Path getPath() { @@ -51,21 +52,25 @@ public class ResizablePathDrawable exten public boolean isStateful() { return true; } @Override protected void onDraw(Shape shape, Canvas canvas, Paint paint) { paint.setColor(currentColor); // setAlpha overrides the alpha value in set color. Since we just set the color, - // the alpha value is reset: override the alpha value with the old value. + // the alpha value is reset: override the alpha value with the old value. We don't + // set alpha if the color is transparent. // // Note: We *should* be able to call Shape.setAlpha, rather than Paint.setAlpha, but // then the opacity doesn't change - dunno why but probably not worth the time. - paint.setAlpha(alpha); + if (currentColor != Color.TRANSPARENT) { + paint.setAlpha(alpha); + } + super.onDraw(shape, canvas, paint); } @Override public void setAlpha(final int alpha) { super.setAlpha(alpha); this.alpha = alpha; }
--- a/mobile/android/chrome/content/aboutAddons.js +++ b/mobile/android/chrome/content/aboutAddons.js @@ -239,17 +239,17 @@ var Addons = { _getElementForAddon: function(aKey) { let list = document.getElementById("addons-list"); let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]"); return element; }, init: function init() { let self = this; - AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(aAddons) { + AddonManager.getAllAddons(function(aAddons) { // Clear all content before filling the addons let list = document.getElementById("addons-list"); list.innerHTML = ""; aAddons.sort(function(a,b) { return a.name.localeCompare(b.name); }); for (let i=0; i<aAddons.length; i++) { @@ -343,25 +343,21 @@ var Addons = { } box.appendChild(setting); } // Send an event so add-ons can prepopulate any non-preference based // settings let event = document.createEvent("Events"); event.initEvent("AddonOptionsLoad", true, false); window.dispatchEvent(event); - - // Also send a notification to match the behavior of desktop Firefox - let id = aListItem.getAttribute("addonID"); - Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id); - } else { - // No options, so hide the header and reset the list item - detailItem.setAttribute("optionsURL", ""); - aListItem.setAttribute("optionsURL", ""); } + + // Also send a notification to match the behavior of desktop Firefox + let id = aListItem.getAttribute("addonID"); + Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id); } } xhr.send(null); } catch (e) { } let list = document.querySelector("#addons-list"); list.style.display = "none"; let details = document.querySelector("#addons-details");
new file mode 100644 --- /dev/null +++ b/mobile/android/config/proguard/play-services-keeps.cfg @@ -0,0 +1,19 @@ +# Rules to prevent Google Play Services from exploding +# (From http://developer.android.com/google/play-services/setup.html#Proguard +# With the reference to "Object" changed so it'll actually *work*...) +-keep class * extends java.util.ListResourceBundle { + protected java.lang.Object[][] getContents(); +} + +-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { + public static final *** NULL; +} + +-keepnames @com.google.android.gms.common.annotation.KeepName class * +-keepclassmembernames class * { + @com.google.android.gms.common.annotation.KeepName *; +} + +-keepnames class * implements android.os.Parcelable { + public static final ** CREATOR; +}
rename from mobile/android/config/proguard.cfg rename to mobile/android/config/proguard/proguard.cfg --- a/mobile/android/config/proguard.cfg +++ b/mobile/android/config/proguard/proguard.cfg @@ -105,16 +105,21 @@ # # Mozilla-specific rules # # Merging classes can generate dex warnings about anonymous inner classes. -optimizations !class/merging/horizontal -optimizations !class/merging/vertical +# This optimisation causes corrupt bytecode if we run more than two passes. +# Testing shows that running the extra passes of everything else saves us +# more than this optimisation does, so bye bye! +-optimizations !code/allocation/variable + # Keep miscellaneous targets. # Keep the annotation. -keep @interface org.mozilla.gecko.mozglue.JNITarget # Keep classes tagged with the annotation. -keep @org.mozilla.gecko.mozglue.JNITarget class * @@ -202,8 +207,14 @@ *; } # Disable obfuscation because it makes exception stack traces more difficult to read. -dontobfuscate # Suppress warnings about missing descriptor classes. #-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.** + +-include "play-services-keeps.cfg" + +# Don't print spurious warnings from the support library. +# See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl +-dontnote android.support.**
new file mode 100644 --- /dev/null +++ b/mobile/android/config/proguard/strip-libs.cfg @@ -0,0 +1,40 @@ +# Proguard step for stripping debug information. +# +# This is useful to work around a bug in the way Proguard handles debug information: it +# sometimes corrupts it. Classes with corrupt debug information cannot be dexed, but +# classes with *no* debug information can be. There's no way to configure Proguard to +# delete debug information on a per-class basis, so we need this special extra step for +# stripping debug information only from those classes for which the Proguard bug is +# encountered. +# +# Currently, this pass is applied to all bundled library jars for which we are not +# compiling the source. This is slightly more than is strictly necessary to work around +# the Proguard bug, but such debug information is of negligible value and stripping it +# too slightly simplifies the makefile and saves us a handful of kilobytes of binary size. +# +# Configuring Proguard to do nothing except strip metadata is done by having it run only +# the obfuscation pass, but with a configuration that prevents it from renaming any classes. +# It then attempts to delete class metadata, so we further configure it not to do so for +# anything except the problematic debug information. + +# Run only the obfuscator. +-dontoptimize +-dontshrink +-dontpreverify +-verbose + +# Don't rename anything. +-keeppackagenames + +# Seriously, don't rename anything. +-keep class * +-keepclassmembers class * { + *; +} + +# Don't delete other useful metadata. +-keepattributes Exceptions,InnerClasses,Signature,Deprecated,*Annotation*,EnclosingMethod + +# Don't print spurious warnings from the support library. +# See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl +-dontnote android.support.**
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java @@ -1,16 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.search; import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.LocaleAware; +import org.mozilla.gecko.Locales; 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; @@ -34,17 +34,17 @@ import com.nineoldandroids.animation.Ani import com.nineoldandroids.animation.ObjectAnimator; /** * The main entrance for the Android search intent. * <p/> * State management is delegated to child fragments. Fragments communicate * with each other by passing messages through this activity. */ -public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity +public class SearchActivity extends Locales.LocaleAwareFragmentActivity implements AcceptsSearchQuery, SearchEngineCallback { private static final String LOGTAG = "GeckoSearchActivity"; private static final String KEY_SEARCH_STATE = "search_state"; private static final String KEY_EDIT_STATE = "edit_state"; private static final String KEY_QUERY = "query";
--- a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java @@ -1,16 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.search; import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.LocaleAware; +import org.mozilla.gecko.Locales; import org.mozilla.gecko.R; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.BrowserContract; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.AsyncTask; @@ -36,17 +36,17 @@ public class SearchPreferenceActivity ex private static final String LOG_TAG = "SearchPreferenceActivity"; public static final String PREF_CLEAR_HISTORY_KEY = "search.not_a_preference.clear_history"; @Override @SuppressWarnings("deprecation") protected void onCreate(Bundle savedInstanceState) { - LocaleAware.initializeLocale(getApplicationContext()); + Locales.initializeLocale(getApplicationContext()); super.onCreate(savedInstanceState); getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (getActionBar() != null) { getActionBar().setDisplayHomeAsUpEnabled(true); }
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java @@ -7,19 +7,19 @@ package org.mozilla.search.providers; import android.content.Context; import android.content.SharedPreferences; 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.Locales; 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; @@ -183,17 +183,17 @@ public class SearchEngineManager impleme 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 languageTag = Locales.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); } } @@ -416,26 +416,26 @@ public class SearchEngineManager impleme * * @param fileName name of the file to read. * @return InputStream for file. */ private InputStream getInputStreamFromSearchPluginsJar(String fileName) { final Locale locale = Locale.getDefault(); // First, try a file path for the full locale. - final String languageTag = BrowserLocaleManager.getLanguageTag(locale); + final String languageTag = Locales.getLanguageTag(locale); String url = getSearchPluginsJarURL(languageTag, fileName); InputStream in = GeckoJarReader.getStream(url); if (in != null) { return in; } // If that doesn't work, try a file path for just the language. - final String language = BrowserLocaleManager.getLanguage(locale); + final String language = Locales.getLanguage(locale); if (!languageTag.equals(language)) { url = getSearchPluginsJarURL(language, fileName); in = GeckoJarReader.getStream(url); if (in != null) { return in; } }
--- a/mobile/android/tests/browser/junit3/src/TestSuggestedSites.java +++ b/mobile/android/tests/browser/junit3/src/TestSuggestedSites.java @@ -91,17 +91,17 @@ public class TestSuggestedSites extends public TestDistribution(Context context) { super(context); this.filesPerLocale = new HashMap<Locale, File>(); } @Override public File getDistributionFile(String name) { for (Locale locale : filesPerLocale.keySet()) { - if (name.startsWith("suggestedsites/locales/" + BrowserLocaleManager.getLanguageTag(locale))) { + if (name.startsWith("suggestedsites/locales/" + Locales.getLanguageTag(locale))) { return filesPerLocale.get(locale); } } return null; } @Override
--- a/mobile/android/themes/core/aboutAddons.css +++ b/mobile/android/themes/core/aboutAddons.css @@ -34,20 +34,18 @@ .options-header { font-weight: bold; text-transform: uppercase; margin-top: 1em; } .addon-item[isDisabled="true"] .options-header, -.addon-item:not([optionsURL]) .options-header, .addon-item[optionsURL=""] .options-header, .addon-item[isDisabled="true"] .options-box, -.addon-item:not([optionsURL]) .options-box, .addon-item[optionsURL=""] .options-box { display: none; } #addons-details > .list-item:active { background-color: #fff; }
--- a/services/sync/modules/FxaMigrator.jsm +++ b/services/sync/modules/FxaMigrator.jsm @@ -244,41 +244,33 @@ Migrator.prototype = { yield WeaveService.whenLoaded(); let signedInUser = yield fxAccounts.getSignedInUser(); let sentinel = { email: signedInUser.email, uid: signedInUser.uid, verified: signedInUser.verified, prefs: this._getSentinelPrefs(), }; - if (Weave.Service.setFxaMigrationSentinel) { - yield Weave.Service.setFxaMigrationSentinel(sentinel); - } else { - this.log.warn("Waiting on bug 1017433; no sync sentinel"); - } + yield Weave.Service.setFxAMigrationSentinel(sentinel); }), /* Ask sync to upload the migration sentinal if we (or any other linked device) haven't previously written one. */ _setMigrationSentinelIfNecessary: Task.async(function* () { if (!(yield this._getSyncMigrationSentinel())) { this.log.info("writing the migration sentinel"); yield this._setSyncMigrationSentinel(); } }), /* Ask sync to return a migration sentinel if one exists, otherwise return null */ _getSyncMigrationSentinel: Task.async(function* () { yield WeaveService.whenLoaded(); - if (!Weave.Service.getFxaMigrationSentinel) { - this.log.warn("Waiting on bug 1017433; no sync sentinel"); - return null; - } - let sentinel = yield Weave.Service.getFxaMigrationSentinel(); + let sentinel = yield Weave.Service.getFxAMigrationSentinel(); this.log.debug("got migration sentinel ${}", sentinel); return sentinel; }), _getDefaultAccountName: Task.async(function* (sentinel) { // Requires looking to see if other devices have written a migration // sentinel (eg, see _haveSynchedMigrationSentinel), and if not, see if // the legacy account name appears to be a valid email address (via the @@ -296,29 +288,21 @@ Migrator.prototype = { return account; } this.log.info("defaultAccountName could not find an account"); return null; }), // Prevent sync from automatically starting _blockSync() { - if (Weave.Service.scheduler.blockSync) { - Weave.Service.scheduler.blockSync(); - } else { - this.log.warn("Waiting on bug 1019408; sync not blocked"); - } + Weave.Service.scheduler.blockSync(); }, _unblockSync() { - if (Weave.Service.scheduler.unblockSync) { - Weave.Service.scheduler.unblockSync(); - } else { - this.log.warn("Waiting on bug 1019408; sync not unblocked"); - } + Weave.Service.scheduler.unblockSync(); }, /* * Some helpers for the UI to try and move to the next state. */ // Open a UI for the user to create a Firefox Account. This should only be // called while we are in the STATE_USER_FXA state. When the user completes
--- a/services/sync/modules/browserid_identity.js +++ b/services/sync/modules/browserid_identity.js @@ -395,16 +395,23 @@ this.BrowserIDManager.prototype = { resetSyncKey: function() { this._syncKey = null; this._syncKeyBundle = null; this._syncKeyUpdated = true; this._shouldHaveSyncKeyBundle = false; }, /** + * Return credentials hosts for this identity only. + */ + _getSyncCredentialsHosts: function() { + return Utils.getSyncCredentialsHostsFxA(); + }, + + /** * The current state of the auth credentials. * * This essentially validates that enough credentials are available to use * Sync, although it effectively ignores the state of the master-password - * if that's locked and that's the only problem we can see, say everything * is OK - unlockAndVerifyAuthState will be used to perform the unlock * and re-verification if necessary. */
--- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -49,16 +49,19 @@ MAXIMUM_BACKOFF_INTERVAL: 8 // HMAC event handling timeout. // 10 minutes: a compromise between the multi-desktop sync interval // and the mobile sync interval. HMAC_EVENT_INTERVAL: 600000, // How long to wait between sync attempts if the Master Password is locked. MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes +// The default for how long we "block" sync from running when doing a migration. +DEFAULT_BLOCK_PERIOD: 2 * 24 * 60 * 60 * 1000, // 2 days + // Separate from the ID fetch batch size to allow tuning for mobile. MOBILE_BATCH_SIZE: 50, // 50 is hardcoded here because of URL length restrictions. // (GUIDs can be up to 64 chars long.) // Individual engines can set different values for their limit if their // identifiers are shorter. DEFAULT_GUID_FETCH_BATCH_SIZE: 50,
--- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -480,20 +480,27 @@ IdentityManager.prototype = { let loginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); let login = new loginInfo(PWDMGR_HOST, null, realm, username, password, "", ""); Services.logins.addLogin(login); }, /** + * Return credentials hosts for this identity only. + */ + _getSyncCredentialsHosts: function() { + return Utils.getSyncCredentialsHostsLegacy(); + }, + + /** * Deletes Sync credentials from the password manager. */ deleteSyncCredentials: function deleteSyncCredentials() { - for (let host of Utils.getSyncCredentialsHosts()) { + for (let host of this._getSyncCredentialsHosts()) { let logins = Services.logins.findLogins({}, host, "", ""); for each (let login in logins) { Services.logins.removeLogin(login); } } // Wait until after store is updated in case it fails. this._basicPassword = null;
--- a/services/sync/modules/policies.js +++ b/services/sync/modules/policies.js @@ -492,16 +492,56 @@ SyncScheduler.prototype = { clearSyncTriggers: function clearSyncTriggers() { this._log.debug("Clearing sync triggers and the global score."); this.globalScore = this.nextSync = 0; // Clear out any scheduled syncs if (this.syncTimer) this.syncTimer.clear(); }, + + /** + * Prevent new syncs from starting. This is used by the FxA migration code + * where we can't afford to have a sync start partway through the migration. + * To handle the edge-case of a sync starting and not stopping, we store + * this state in a pref, so on the next startup we remain blocked (and thus + * sync will never start) so the migration can complete. + * + * As a safety measure, we only block for some period of time, and after + * that it will automatically unblock. This ensures that if things go + * really pear-shaped and we never end up calling unblockSync() we haven't + * completely broken the world. + */ + blockSync: function(until = null) { + if (!until) { + until = Date.now() + DEFAULT_BLOCK_PERIOD; + } + // until is specified in ms, but Prefs can't hold that much + Svc.Prefs.set("scheduler.blocked-until", Math.floor(until / 1000)); + }, + + unblockSync: function() { + Svc.Prefs.reset("scheduler.blocked-until"); + // the migration code should be ready to roll, so resume normal operations. + this.checkSyncStatus(); + }, + + get isBlocked() { + let until = Svc.Prefs.get("scheduler.blocked-until"); + if (until === undefined) { + return false; + } + if (until <= Math.floor(Date.now() / 1000)) { + // we were previously blocked but the time has expired. + Svc.Prefs.reset("scheduler.blocked-until"); + return false; + } + // we remain blocked. + return true; + }, }; const LOG_PREFIX_SUCCESS = "success-"; const LOG_PREFIX_ERROR = "error-"; this.ErrorHandler = function ErrorHandler(service) { this.service = service; this.init();
--- a/services/sync/modules/record.js +++ b/services/sync/modules/record.js @@ -100,78 +100,16 @@ WBORecord.prototype = { "ttl: " + this.ttl + " " + "payload: " + JSON.stringify(this.payload) + " }"; } }; Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]); -/** - * An interface and caching layer for records. - */ -this.RecordManager = function RecordManager(service) { - this.service = service; - - this._log = Log.repository.getLogger(this._logName); - this._records = {}; -} -RecordManager.prototype = { - _recordType: WBORecord, - _logName: "Sync.RecordManager", - - import: function RecordMgr_import(url) { - this._log.trace("Importing record: " + (url.spec ? url.spec : url)); - try { - // Clear out the last response with empty object if GET fails - this.response = {}; - this.response = this.service.resource(url).get(); - - // Don't parse and save the record on failure - if (!this.response.success) - return null; - - let record = new this._recordType(url); - record.deserialize(this.response); - - return this.set(url, record); - } catch(ex) { - this._log.debug("Failed to import record: " + Utils.exceptionStr(ex)); - return null; - } - }, - - get: function RecordMgr_get(url) { - // Use a url string as the key to the hash - let spec = url.spec ? url.spec : url; - if (spec in this._records) - return this._records[spec]; - return this.import(url); - }, - - set: function RecordMgr_set(url, record) { - let spec = url.spec ? url.spec : url; - return this._records[spec] = record; - }, - - contains: function RecordMgr_contains(url) { - if ((url.spec || url) in this._records) - return true; - return false; - }, - - clearCache: function recordMgr_clearCache() { - this._records = {}; - }, - - del: function RecordMgr_del(url) { - delete this._records[url]; - } -}; - this.CryptoWrapper = function CryptoWrapper(collection, id) { this.cleartext = {}; WBORecord.call(this, collection, id); this.ciphertext = null; this.id = id; } CryptoWrapper.prototype = { __proto__: WBORecord.prototype, @@ -264,16 +202,77 @@ CryptoWrapper.prototype = { WBORecord.prototype.__lookupSetter__("id").call(this, val); return this.cleartext.id = val; }, }; Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]); Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted"); +/** + * An interface and caching layer for records. + */ +this.RecordManager = function RecordManager(service) { + this.service = service; + + this._log = Log.repository.getLogger(this._logName); + this._records = {}; +} +RecordManager.prototype = { + _recordType: CryptoWrapper, + _logName: "Sync.RecordManager", + + import: function RecordMgr_import(url) { + this._log.trace("Importing record: " + (url.spec ? url.spec : url)); + try { + // Clear out the last response with empty object if GET fails + this.response = {}; + this.response = this.service.resource(url).get(); + + // Don't parse and save the record on failure + if (!this.response.success) + return null; + + let record = new this._recordType(url); + record.deserialize(this.response); + + return this.set(url, record); + } catch(ex) { + this._log.debug("Failed to import record: " + Utils.exceptionStr(ex)); + return null; + } + }, + + get: function RecordMgr_get(url) { + // Use a url string as the key to the hash + let spec = url.spec ? url.spec : url; + if (spec in this._records) + return this._records[spec]; + return this.import(url); + }, + + set: function RecordMgr_set(url, record) { + let spec = url.spec ? url.spec : url; + return this._records[spec] = record; + }, + + contains: function RecordMgr_contains(url) { + if ((url.spec || url) in this._records) + return true; + return false; + }, + + clearCache: function recordMgr_clearCache() { + this._records = {}; + }, + + del: function RecordMgr_del(url) { + delete this._records[url]; + } +}; /** * Keeps track of mappings between collection names ('tabs') and KeyBundles. * * You can update this thing simply by giving it /info/collections. It'll * use the last modified time to bring itself up to date. */ this.CollectionKeyManager = function CollectionKeyManager() {
--- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1275,17 +1275,26 @@ Sync11Service.prototype = { synchronizer.sync(); // wait() throws if the first argument is truthy, which is exactly what // we want. let result = cb.wait(); histogram = Services.telemetry.getHistogramById("WEAVE_COMPLETE_SUCCESS_COUNT"); histogram.add(1); - // We successfully synchronized. Now let's update our declined engines. + // We successfully synchronized. + // Try and fetch the migration sentinel - it will end up in the recordManager + // cache, so a sync migration doesn't need a server round-trip. + // If we have no clusterURL, we are probably doing a node reassignment + // do don't attempt to get the credentials. + if (this.clusterURL) { + this.recordManager.get(this.storageURL + "meta/fxa_credentials"); + } + + // Now let's update our declined engines. let meta = this.recordManager.get(this.metaURL); if (!meta) { this._log.warn("No meta/global; can't update declined state."); return; } let declinedEngines = new DeclinedEngines(this); let didChange = declinedEngines.updateDeclined(meta, this.engineManager); @@ -1312,16 +1321,102 @@ Sync11Service.prototype = { let response = res.put(meta); if (!response.success) { throw response; } this.recordManager.set(this.metaURL, meta); }, /** + * Get a migration sentinel for the Firefox Accounts migration. + * Returns a JSON blob - it is up to callers of this to make sense of the + * data. + * + * Returns a promise that resolves with the sentinel, or null. + */ + getFxAMigrationSentinel: function() { + if (this._shouldLogin()) { + this._log.debug("In getFxAMigrationSentinel: should login."); + if (!this.login()) { + this._log.debug("Can't get migration sentinel: login returned false."); + return Promise.resolve(null); + } + } + if (!this.identity.syncKeyBundle) { + this._log.error("Can't get migration sentinel: no syncKeyBundle."); + return Promise.resolve(null); + } + try { + let collectionURL = this.storageURL + "meta/fxa_credentials"; + let cryptoWrapper = this.recordManager.get(collectionURL); + if (!cryptoWrapper.payload) { + // nothing to decrypt - .decrypt is noisy in that case, so just bail + // now. + return Promise.resolve(null); + } + // If the payload has a sentinel it means we must have put back the + // decrypted version last time we were called. + if (cryptoWrapper.payload.sentinel) { + return Promise.resolve(cryptoWrapper.payload.sentinel); + } + // If decryption fails it almost certainly means the key is wrong - but + // it's not clear if we need to take special action for that case? + let payload = cryptoWrapper.decrypt(this.identity.syncKeyBundle); + // After decrypting the ciphertext is lost, so we just stash the + // decrypted payload back into the wrapper. + cryptoWrapper.payload = payload; + return Promise.resolve(payload.sentinel); + } catch (ex) { + this._log.error("Failed to fetch the migration sentinel: ${}", ex); + return Promise.resolve(null); + } + }, + + /** + * Set a migration sentinel for the Firefox Accounts migration. + * Accepts a JSON blob - it is up to callers of this to make sense of the + * data. + * + * Returns a promise that resolves with a boolean which indicates if the + * sentinel was successfully written. + */ + setFxAMigrationSentinel: function(sentinel) { + if (this._shouldLogin()) { + this._log.debug("In setFxAMigrationSentinel: should login."); + if (!this.login()) { + this._log.debug("Can't set migration sentinel: login returned false."); + return Promise.resolve(false); + } + } + if (!this.identity.syncKeyBundle) { + this._log.error("Can't set migration sentinel: no syncKeyBundle."); + return Promise.resolve(false); + } + try { + let collectionURL = this.storageURL + "meta/fxa_credentials"; + let cryptoWrapper = new CryptoWrapper("meta", "fxa_credentials"); + cryptoWrapper.cleartext.sentinel = sentinel; + + cryptoWrapper.encrypt(this.identity.syncKeyBundle); + + let res = this.resource(collectionURL); + let response = res.put(cryptoWrapper.toJSON()); + + if (!response.success) { + throw response; + } + this.recordManager.set(collectionURL, cryptoWrapper); + } catch (ex) { + this._log.error("Failed to set the migration sentinel: ${}", ex); + return Promise.resolve(false); + } + return Promise.resolve(true); + }, + + /** * If we have a passphrase, rather than a 25-alphadigit sync key, * use the provided sync ID to bootstrap it using PBKDF2. * * Store the new 'passphrase' back into the identity manager. * * We can check this as often as we want, because once it's done the * check will no longer succeed. It only matters that it happens after * we decide to bump the server storage version.
--- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -591,23 +591,40 @@ this.Utils = { /** * Return a set of hostnames (including the protocol) which may have * credentials for sync itself stored in the login manager. * * In general, these hosts will not have their passwords synced, will be * reset when we drop sync credentials, etc. */ getSyncCredentialsHosts: function() { + let result = new Set(this.getSyncCredentialsHostsLegacy()); + for (let host of this.getSyncCredentialsHostsFxA()) { + result.add(host); + } + return result; + }, + + /* + * Get the "legacy" identity hosts. + */ + getSyncCredentialsHostsLegacy: function() { + // the legacy sync host + return new Set([PWDMGR_HOST]); + }, + + /* + * Get the FxA identity hosts. + */ + getSyncCredentialsHostsFxA: function() { // This is somewhat expensive and the result static, so we cache the result. - if (this._syncCredentialsHosts) { - return this._syncCredentialsHosts; + if (this._syncCredentialsHostsFxA) { + return this._syncCredentialsHostsFxA; } let result = new Set(); - // the legacy sync host - result.add(PWDMGR_HOST); // the FxA host result.add(FxAccountsCommon.FXA_PWDMGR_HOST); // // The FxA hosts - these almost certainly all have the same hostname, but // better safe than sorry... for (let prefName of ["identity.fxaccounts.remote.force_auth.uri", "identity.fxaccounts.remote.signup.uri", "identity.fxaccounts.remote.signin.uri", @@ -616,17 +633,17 @@ this.Utils = { try { prefVal = Services.prefs.getCharPref(prefName); } catch (_) { continue; } let uri = Services.io.newURI(prefVal, null, null); result.add(uri.prePath); } - return this._syncCredentialsHosts = result; + return this._syncCredentialsHostsFxA = result; }, }; XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; return converter;
new file mode 100644 --- /dev/null +++ b/services/sync/tests/unit/test_block_sync.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://services-sync/util.js"); + +// Simple test for block/unblock. +add_task(function *() { + Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.") + Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref"); + Weave.Service.scheduler.blockSync(); + + Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.") + Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref"); + + Weave.Service.scheduler.unblockSync(); + Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.") + Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref"); + + // now check the "until" functionality. + let until = Date.now() + 1000; + Weave.Service.scheduler.blockSync(until); + Assert.ok(Weave.Service.scheduler.isBlocked, "sync is blocked.") + Assert.ok(Svc.Prefs.has("scheduler.blocked-until"), "have the blocked pref"); + + // wait for 'until' to pass. + yield new Promise((resolve, reject) => { + CommonUtils.namedTimer(resolve, 1000, {}, "timer"); + }); + + // should have automagically unblocked and removed the pref. + Assert.ok(!Weave.Service.scheduler.isBlocked, "sync is not blocked.") + Assert.ok(!Svc.Prefs.has("scheduler.blocked-until"), "have no blocked pref"); +}); + +function run_test() { + run_next_test(); +}
--- a/services/sync/tests/unit/test_fxa_migration.js +++ b/services/sync/tests/unit/test_fxa_migration.js @@ -98,30 +98,28 @@ add_task(function *testMigration() { Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null, "no user state when complete"); // Arrange for a legacy sync user and manually bump the migrator let [engine, server] = configureLegacySync(); // monkey-patch the migration sentinel code so we know it was called. let haveStartedSentinel = false; -// (This is waiting on bug 1017433) -/** - let origSetFxaMigrationSentinel = Service.setFxaMigrationSentinel; + let origSetFxAMigrationSentinel = Service.setFxAMigrationSentinel; let promiseSentinelWritten = new Promise((resolve, reject) => { - Service.setFxaMigrationSentinel = function(arg) { + Service.setFxAMigrationSentinel = function(arg) { haveStartedSentinel = true; - return origSetFxaMigrationSentinel.call(Service, arg).then(result => { - Service.setFxaMigrationSentinel = origSetFxaMigrationSentinel; + return origSetFxAMigrationSentinel.call(Service, arg).then(result => { + Service.setFxAMigrationSentinel = origSetFxAMigrationSentinel; resolve(result); return result; }); } }); -**/ + // We are now configured for legacy sync, but we aren't in an EOL state yet, // so should still be not waiting for a user. Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null, "no user state before server EOL"); // Start a sync - this will cause an EOL notification which the migrator's // observer will notice. let promise = promiseOneObserver("fxa-migration:state-changed"); @@ -194,20 +192,17 @@ add_task(function *testMigration() { let cb = Async.makeSpinningCallback(); promiseOneObserver("fxa-migration:state-changed").then(state => cb(null, state)); Assert.equal(cb.wait(), null, "no user action necessary while sync completes."); // We must not have started writing the sentinel yet. Assert.ok(!haveStartedSentinel, "haven't written a sentinel yet"); // sync should be blocked from continuing -// (This is waiting on bug 1019408) -/** Assert.ok(Service.scheduler.isBlocked, "sync is blocked.") -**/ wasWaiting = true; throw ex; }; _("Starting sync"); Service.sync(); _("Finished sync"); @@ -222,26 +217,20 @@ add_task(function *testMigration() { resolve(); } }); Assert.ok(wasWaiting, "everything was good while sync was running.") // The migration is now going to run to completion. // sync should still be "blocked" -// (This is waiting on bug 1019408) -/** Assert.ok(Service.scheduler.isBlocked, "sync is blocked."); -**/ // We should see the migration sentinel written and it should return true. -// (This is waiting on bug 1017433) -/** Assert.ok((yield promiseSentinelWritten), "wrote the sentinel"); -**/ // And we should see a new sync start yield promiseFinalSync; // and we should be configured for FxA let WeaveService = Cc["@mozilla.org/weave/service;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject;
new file mode 100644 --- /dev/null +++ b/services/sync/tests/unit/test_fxa_migration_sentinel.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the reading and writing of the sync migration sentinel. +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/FxAccounts.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); + +Cu.import("resource://testing-common/services/sync/utils.js"); +Cu.import("resource://testing-common/services/common/logging.js"); + +Cu.import("resource://services-sync/record.js"); + +// Set our username pref early so sync initializes with the legacy provider. +Services.prefs.setCharPref("services.sync.username", "foo"); + +// Now import sync +Cu.import("resource://services-sync/service.js"); + +const USER = "foo"; +const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea"; + +function promiseStopServer(server) { + return new Promise((resolve, reject) => { + server.stop(resolve); + }); +} + +let numServerRequests = 0; + +// Helpers +function configureLegacySync() { + let contents = { + meta: {global: {}}, + crypto: {}, + }; + + setBasicCredentials(USER, "password", PASSPHRASE); + + numServerRequests = 0; + let server = new SyncServer({ + onRequest: () => { + ++numServerRequests + } + }); + server.registerUser(USER, "password"); + server.createContents(USER, contents); + server.start(); + + Service.serverURL = server.baseURI; + Service.clusterURL = server.baseURI; + Service.identity.username = USER; + Service._updateCachedURLs(); + + return server; +} + +// Test a simple round-trip of the get/set functions. +add_task(function *() { + // Arrange for a legacy sync user. + let server = configureLegacySync(); + + Assert.equal((yield Service.getFxAMigrationSentinel()), null, "no sentinel to start"); + + let sentinel = {foo: "bar"}; + yield Service.setFxAMigrationSentinel(sentinel); + + Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back"); + + yield promiseStopServer(server); +}); + +// Test the records are cached by the record manager. +add_task(function *() { + // Arrange for a legacy sync user. + let server = configureLegacySync(); + Service.login(); + + // Reset the request count here as the login would have made some. + numServerRequests = 0; + + Assert.equal((yield Service.getFxAMigrationSentinel()), null, "no sentinel to start"); + Assert.equal(numServerRequests, 1, "first fetch should hit the server"); + + let sentinel = {foo: "bar"}; + yield Service.setFxAMigrationSentinel(sentinel); + Assert.equal(numServerRequests, 2, "setting sentinel should hit the server"); + + Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back"); + Assert.equal(numServerRequests, 2, "second fetch should not should hit the server"); + + // Clobber the caches and ensure we still get the correct value back when we + // do hit the server. + Service.recordManager.clearCache(); + Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got the sentinel back"); + Assert.equal(numServerRequests, 3, "should have re-hit the server with empty caches"); + + yield promiseStopServer(server); +}); + +// Test the records are cached by a sync. +add_task(function* () { + let server = configureLegacySync(); + + // A first sync clobbers meta/global due to it being empty, so we first + // do a sync which forces a good set of data on the server. + Service.sync(); + + // Now create a sentinel exists on the server. It's encrypted, so we need to + // put an encrypted version. + let cryptoWrapper = new CryptoWrapper("meta", "fxa_credentials"); + let sentinel = {foo: "bar"}; + cryptoWrapper.cleartext = { + id: "fxa_credentials", + sentinel: sentinel, + deleted: false, + } + cryptoWrapper.encrypt(Service.identity.syncKeyBundle); + let payload = { + ciphertext: cryptoWrapper.ciphertext, + IV: cryptoWrapper.IV, + hmac: cryptoWrapper.hmac, + }; + + server.createContents(USER, { + meta: {fxa_credentials: payload}, + crypto: {}, + }); + + // Another sync - this will cause the encrypted record to be fetched. + Service.sync(); + // Reset the request count here as the sync will have made many! + numServerRequests = 0; + + // Asking for the sentinel should use the copy cached in the record manager. + Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got it"); + Assert.equal(numServerRequests, 0, "should not have hit the server"); + + // And asking for it again should work (we have to work around the fact the + // ciphertext is clobbered on first decrypt...) + Assert.deepEqual((yield Service.getFxAMigrationSentinel()), sentinel, "got it again"); + Assert.equal(numServerRequests, 0, "should not have hit the server"); + + yield promiseStopServer(server); +}); + +function run_test() { + initTestLogging(); + run_next_test(); +}
--- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -169,9 +169,11 @@ skip-if = debug [test_tab_tracker.js] [test_healthreport.js] skip-if = ! healthreport [test_warn_on_truncated_response.js] # FxA migration +[test_block_sync.js] [test_fxa_migration.js] +[test_fxa_migration_sentinel.js]
--- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -670,18 +670,18 @@ var PageStyleActor = protocol.ActorClass this.cssLogic.highlight(node.rawNode); let layout = {}; // First, we update the first part of the layout view, with // the size of the element. let clientRect = node.rawNode.getBoundingClientRect(); - layout.width = Math.round(clientRect.width); - layout.height = Math.round(clientRect.height); + layout.width = Math.ceil(clientRect.width); + layout.height = Math.ceil(clientRect.height); // We compute and update the values of margins & co. let style = CssLogic.getComputedStyle(node.rawNode); for (let prop of [ "position", "margin-top", "margin-right", "margin-bottom",