);
}
});
@@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
- sdk: React.PropTypes.object.isRequired
+ sdk: React.PropTypes.object.isRequired,
+ feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.props.conversation.off(null, null, this);
},
+ shouldComponentUpdate: function(nextProps, nextState) {
+ // Only rerender if current state has actually changed
+ return nextState.callStatus !== this.state.callStatus;
+ },
+
+ callStatusSwitcher: function(status) {
+ return function() {
+ this.setState({callStatus: status});
+ }.bind(this);
+ },
+
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "failure":
- case "end":
case "start": {
return (
);
}
+ case "end": {
+ return (
+
+ );
+ }
case "expired": {
return (
);
}
default: {
- return
+ return ;
}
}
},
@@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
- console.log(error);
+ console.error(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
@@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
- this.setState({callStatus: "end"});
- // For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this.props.notifications.errorL10n("call_timeout_notification_text");
}
+ // redirects the user to the call start view
+ // XXX should switch callStatus to failed for specific reasons when we
+ // get the call failed view; for now, switch back to start.
+ this.setState({callStatus: "start"});
},
/**
@@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
- sdk: React.PropTypes.object.isRequired
+ sdk: React.PropTypes.object.isRequired,
+ feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@@ -680,6 +717,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper={this.props.helper}
notifications={this.props.notifications}
sdk={this.props.sdk}
+ feedbackApiClient={this.props.feedbackApiClient}
/>
);
} else {
@@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
+ var feedbackApiClient = new loop.FeedbackAPIClient(
+ loop.config.feedbackApiUrl, {
+ product: loop.config.feedbackProductName,
+ user_agent: navigator.userAgent,
+ url: document.location.origin
+ });
// Obtain the loopToken and pass it to the conversation
var locationHash = helper.locationHash();
@@ -734,6 +778,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper={helper}
notifications={notifications}
sdk={OT}
+ feedbackApiClient={feedbackApiClient}
/>, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to when the page is translated
@@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
OutgoingConversationView: OutgoingConversationView,
+ EndedConversationView: EndedConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,
diff --git a/browser/components/loop/standalone/content/l10n/loop.en-US.properties b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
index 12ccf926f676..64c326c3268e 100644
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -46,3 +46,32 @@ clientShortname=WebRTC!
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
+
+feedback_call_experience_heading2=How was your conversation?
+feedback_what_makes_you_sad=What makes you sad?
+feedback_thank_you_heading=Thank you for your feedback!
+feedback_category_audio_quality=Audio quality
+feedback_category_video_quality=Video quality
+feedback_category_was_disconnected=Was disconnected
+feedback_category_confusing=Confusing
+feedback_category_other=Other:
+feedback_custom_category_text_placeholder=What went wrong?
+feedback_submit_button=Submit
+feedback_back_button=Back
+## LOCALIZATION NOTE (feedback_window_will_close_in2):
+## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
+## In this item, don't translate the part between {{..}}
+feedback_window_will_close_in2={[ plural(countdown) ]}
+feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
+feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
+feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
+
+## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
+## a signed-in to signed-in user call.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
+feedback_rejoin_button=Rejoin
+## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
+## an abusive user.
+feedback_report_user_button=Report User
diff --git a/browser/components/loop/standalone/server.js b/browser/components/loop/standalone/server.js
index ba8fdfb93dcb..d3bf4c072912 100644
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -7,17 +7,21 @@ var app = express();
var port = process.env.PORT || 3000;
var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
+var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL ||
+ "https://input.allizom.org/api/v1/feedback";
+var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop";
function getConfigFile(req, res) {
"use strict";
res.set('Content-Type', 'text/javascript');
- res.send(
- "var loop = loop || {};" +
- "loop.config = loop.config || {};" +
- "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
- "loop.config.pendingCallTimeout = 20000;"
- );
+ res.send([
+ "var loop = loop || {};",
+ "loop.config = loop.config || {};",
+ "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';",
+ "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
+ "loop.config.feedbackProductName = '" + feedbackProductName + "';",
+ ].join("\n"));
}
app.get('/content/config.js', getConfigFile);
diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js
index ba89cd094799..c4ba9e7f8995 100644
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -108,8 +108,7 @@ describe("loop.conversation", function() {
beforeEach(function() {
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
- sdk: {},
- pendingCallTimeout: 1000,
+ sdk: {}
});
sandbox.spy(conversation, "setIncomingSessionData");
sandbox.stub(conversation, "setOutgoingSessionData");
diff --git a/browser/components/loop/test/shared/feedbackApiClient_test.js b/browser/components/loop/test/shared/feedbackApiClient_test.js
index 1b6f1a0da3bf..fbcb705a13e0 100644
--- a/browser/components/loop/test/shared/feedbackApiClient_test.js
+++ b/browser/components/loop/test/shared/feedbackApiClient_test.js
@@ -138,6 +138,13 @@ describe("loop.FeedbackAPIClient", function() {
expect(parsed.user_agent).eql("MOZAGENT");
});
+ it("should send url information when provided", function() {
+ client.send({url: "http://fake.invalid"}, function(){});
+
+ var parsed = JSON.parse(requests[0].requestBody);
+ expect(parsed.url).eql("http://fake.invalid");
+ });
+
it("should throw on invalid feedback data", function() {
expect(function() {
client.send("invalid data", function(){});
diff --git a/browser/components/loop/test/shared/router_test.js b/browser/components/loop/test/shared/router_test.js
index f9223fc6813d..c825c19ab841 100644
--- a/browser/components/loop/test/shared/router_test.js
+++ b/browser/components/loop/test/shared/router_test.js
@@ -60,8 +60,7 @@ describe("loop.shared.router", function() {
conversation = new loop.shared.models.ConversationModel({
loopToken: "fakeToken"
}, {
- sdk: {},
- pendingCallTimeout: 1000
+ sdk: {}
});
});
diff --git a/browser/components/loop/test/shared/views_test.js b/browser/components/loop/test/shared/views_test.js
index 910dee348153..cdcf6a8dea58 100644
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -187,13 +187,12 @@ describe("loop.shared.views", function() {
initSession: sandbox.stub().returns(fakeSession)
};
model = new sharedModels.ConversationModel(fakeSessionData, {
- sdk: fakeSDK,
- pendingCallTimeout: 1000
+ sdk: fakeSDK
});
});
describe("#componentDidMount", function() {
- it("should start a session", function() {
+ it("should start a session by default", function() {
sandbox.stub(model, "startSession");
mountTestComponent({
@@ -205,6 +204,19 @@ describe("loop.shared.views", function() {
sinon.assert.calledOnce(model.startSession);
});
+ it("shouldn't start a session if initiate is false", function() {
+ sandbox.stub(model, "startSession");
+
+ mountTestComponent({
+ initiate: false,
+ sdk: fakeSDK,
+ model: model,
+ video: {enabled: true}
+ });
+
+ sinon.assert.notCalled(model.startSession);
+ });
+
it("should set the correct stream publish options", function() {
var component = mountTestComponent({
diff --git a/browser/components/loop/test/standalone/index.html b/browser/components/loop/test/standalone/index.html
index ef496f0c88ba..cece91948a6d 100644
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -36,6 +36,7 @@
+
diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js
index f090639cefd3..52bc57e7a264 100644
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -13,11 +13,15 @@ describe("loop.webapp", function() {
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
sandbox,
- notifications;
+ notifications,
+ feedbackApiClient;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifications = new sharedModels.NotificationCollection();
+ feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
+ product: "Loop"
+ });
});
afterEach(function() {
@@ -31,6 +35,7 @@ describe("loop.webapp", function() {
sandbox.stub(React, "renderComponent");
sandbox.stub(loop.webapp.WebappHelper.prototype,
"locationHash").returns("#call/fake-Token");
+ loop.config.feedbackApiUrl = "http://fake.invalid";
conversationSetStub =
sandbox.stub(sharedModels.ConversationModel.prototype, "set");
});
@@ -77,7 +82,8 @@ describe("loop.webapp", function() {
client: client,
conversation: conversation,
notifications: notifications,
- sdk: {}
+ sdk: {},
+ feedbackApiClient: feedbackApiClient
});
});
@@ -305,7 +311,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:ended");
TestUtils.findRenderedComponentWithType(ocView,
- loop.webapp.StartConversationView);
+ loop.webapp.EndedConversationView);
});
});
@@ -314,7 +320,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(ocView,
- loop.webapp.StartConversationView);
+ loop.webapp.EndedConversationView);
});
it("should notify the user", function() {
@@ -333,7 +339,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(ocView,
- loop.webapp.StartConversationView);
+ loop.webapp.EndedConversationView);
});
it("should notify the user", function() {
@@ -474,8 +480,10 @@ describe("loop.webapp", function() {
loop.webapp.WebappRootView({
client: client,
helper: webappHelper,
+ notifications: notifications,
sdk: sdk,
- conversation: conversationModel
+ conversation: conversationModel,
+ feedbackApiClient: feedbackApiClient
}));
}
@@ -772,6 +780,32 @@ describe("loop.webapp", function() {
});
});
+ describe("EndedConversationView", function() {
+ var view, conversation;
+
+ beforeEach(function() {
+ conversation = new sharedModels.ConversationModel({}, {
+ sdk: {}
+ });
+ view = React.addons.TestUtils.renderIntoDocument(
+ loop.webapp.EndedConversationView({
+ conversation: conversation,
+ sdk: {},
+ feedbackApiClient: feedbackApiClient,
+ onAfterFeedbackReceived: function(){}
+ })
+ );
+ });
+
+ it("should render a ConversationView", function() {
+ TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
+ });
+
+ it("should render a FeedbackView", function() {
+ TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
+ });
+ });
+
describe("PromoteFirefoxView", function() {
describe("#render", function() {
it("should not render when using Firefox", function() {
diff --git a/browser/components/loop/ui/fake-l10n.js b/browser/components/loop/ui/fake-l10n.js
index 77b8297382e7..daf2a9dd4948 100644
--- a/browser/components/loop/ui/fake-l10n.js
+++ b/browser/components/loop/ui/fake-l10n.js
@@ -9,6 +9,10 @@
* @type {Object}
*/
navigator.mozL10n = document.mozL10n = {
+ initialize: function(){},
+
+ getDirection: function(){},
+
get: function(stringId, vars) {
// upcase the first letter
diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js
index d8b5b77cf92c..411590015f37 100644
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -7,6 +7,7 @@
* @type {Object}
*/
navigator.mozLoop = {
+ ensureRegistered: function() {},
getLoopCharPref: function() {},
getLoopBoolPref: function() {}
};
diff --git a/browser/components/loop/ui/ui-showcase.css b/browser/components/loop/ui/ui-showcase.css
index eea87cc2cc08..7a6ed557cd82 100644
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -37,7 +37,7 @@
.showcase > section {
position: relative;
- padding-top: 12em;
+ padding-top: 14em;
clear: both;
}
@@ -149,3 +149,9 @@
* When tokbox inserts the markup into the page the problem goes away */
bottom: auto;
}
+
+.standalone .ended-conversation .remote_wrapper,
+.standalone .video-layout-wrapper {
+ /* Removes the fake video image for ended conversations */
+ background: none;
+}
diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js
index ece54dffc0e3..047f7cc19b9e 100644
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -23,6 +23,7 @@
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
+ var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@@ -338,6 +339,19 @@
)
),
+ Section({name: "EndedConversationView"},
+ Example({summary: "Displays the feedback form"},
+ React.DOM.div({className: "standalone"},
+ EndedConversationView({sdk: mockSDK,
+ video: {enabled: true},
+ audio: {enabled: true},
+ conversation: mockConversationModel,
+ feedbackApiClient: stageFeedbackApiClient,
+ onAfterFeedbackReceived: noop})
+ )
+ )
+ ),
+
Section({name: "AlertMessages"},
Example({summary: "Various alerts"},
React.DOM.div({className: "alert alert-warning"},
diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx
index 715f45feb759..eb7cfb8e34e9 100644
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -23,6 +23,7 @@
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
+ var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@@ -338,6 +339,19 @@
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js
index 0b59b64894ff..174677dd6aa3 100644
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -75,6 +75,8 @@ let UI = {
}
Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
+
this.setupDeck();
},
@@ -125,6 +127,7 @@ let UI = {
switch (what) {
case "runtimelist":
this.updateRuntimeList();
+ this.autoConnectRuntime();
break;
case "connection":
this.updateRuntimeButton();
@@ -145,6 +148,7 @@ let UI = {
break;
case "runtime":
this.updateRuntimeButton();
+ this.saveLastConnectedRuntime();
break;
case "project-validated":
this.updateTitle();
@@ -343,6 +347,34 @@ let UI = {
}
},
+ autoConnectRuntime: function () {
+ // Automatically reconnect to the previously selected runtime,
+ // if available and has an ID
+ if (AppManager.selectedRuntime || !this.lastConnectedRuntime) {
+ return;
+ }
+ let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
+
+ type = type.toLowerCase();
+
+ // Local connection is mapped to AppManager.runtimeList.custom array
+ if (type == "local") {
+ type = "custom";
+ }
+
+ // We support most runtimes except simulator, that needs to be manually
+ // launched
+ if (type == "usb" || type == "wifi" || type == "custom") {
+ for (let runtime of AppManager.runtimeList[type]) {
+ // Some runtimes do not expose getID function and don't support
+ // autoconnect (like remote connection)
+ if (typeof(runtime.getID) == "function" && runtime.getID() == id) {
+ this.connectToRuntime(runtime);
+ }
+ }
+ }
+ },
+
connectToRuntime: function(runtime) {
let name = runtime.getName();
let promise = AppManager.connectToRuntime(runtime);
@@ -359,6 +391,17 @@ let UI = {
}
},
+ saveLastConnectedRuntime: function () {
+ if (AppManager.selectedRuntime &&
+ typeof(AppManager.selectedRuntime.getID) === "function") {
+ this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID();
+ } else {
+ this.lastConnectedRuntime = "";
+ }
+ Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime",
+ this.lastConnectedRuntime);
+ },
+
/********** PROJECTS **********/
// Panel & button
diff --git a/browser/devtools/webide/modules/app-manager.js b/browser/devtools/webide/modules/app-manager.js
index dd4792e9cf3c..73a504eb0dfe 100644
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -651,7 +651,15 @@ exports.AppManager = AppManager = {
let r = new USBRuntime(id);
this.runtimeList.usb.push(r);
r.updateNameFromADB().then(
- () => this.update("runtimelist"), () => {});
+ () => {
+ this.update("runtimelist");
+ // Also update the runtime button label, if the currently selected
+ // runtime name changes
+ if (r == this.selectedRuntime) {
+ this.update("runtime");
+ }
+ },
+ () => {});
}
this.update("runtimelist");
},
diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js
index b17dbe49fd06..e9c667457ea0 100644
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -13,11 +13,21 @@ const promise = require("promise");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
+// These type strings are used for logging events to Telemetry
+let RuntimeTypes = {
+ usb: "USB",
+ wifi: "WIFI",
+ simulator: "SIMULATOR",
+ remote: "REMOTE",
+ local: "LOCAL"
+};
+
function USBRuntime(id) {
this.id = id;
}
USBRuntime.prototype = {
+ type: RuntimeTypes.usb,
connect: function(connection) {
let device = Devices.getByName(this.id);
if (!device) {
@@ -59,6 +69,7 @@ function WiFiRuntime(deviceName) {
}
WiFiRuntime.prototype = {
+ type: RuntimeTypes.wifi,
connect: function(connection) {
let service = discovery.getRemoteService("devtools", this.deviceName);
if (!service) {
@@ -82,6 +93,7 @@ function SimulatorRuntime(version) {
}
SimulatorRuntime.prototype = {
+ type: RuntimeTypes.simulator,
connect: function(connection) {
let port = ConnectionManager.getFreeTCPPort();
let simulator = Simulator.getByVersion(this.version);
@@ -105,6 +117,7 @@ SimulatorRuntime.prototype = {
}
let gLocalRuntime = {
+ type: RuntimeTypes.local,
connect: function(connection) {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
@@ -118,9 +131,13 @@ let gLocalRuntime = {
getName: function() {
return Strings.GetStringFromName("local_runtime");
},
+ getID: function () {
+ return "local";
+ }
}
let gRemoteRuntime = {
+ type: RuntimeTypes.remote,
connect: function(connection) {
let win = Services.wm.getMostRecentWindow("devtools:webide");
if (!win) {
diff --git a/browser/devtools/webide/test/chrome.ini b/browser/devtools/webide/test/chrome.ini
index 9af2900f9c11..65cc7281f98f 100644
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -31,3 +31,4 @@ support-files =
[test_manifestUpdate.html]
[test_addons.html]
[test_deviceinfo.html]
+[test_autoconnect_runtime.html]
diff --git a/browser/devtools/webide/test/test_autoconnect_runtime.html b/browser/devtools/webide/test/test_autoconnect_runtime.html
new file mode 100644
index 000000000000..6087a98af97f
--- /dev/null
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/webide-prefs.js b/browser/devtools/webide/webide-prefs.js
index 3c828ee27ae1..6f6807e4f571 100644
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -15,3 +15,4 @@ pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozil
pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
+pref("devtools.webide.lastConnectedRuntime", "");
diff --git a/browser/themes/shared/devtools/webaudioeditor.inc.css b/browser/themes/shared/devtools/webaudioeditor.inc.css
index 95faced4d4db..9e33f2b8e6e5 100644
--- a/browser/themes/shared/devtools/webaudioeditor.inc.css
+++ b/browser/themes/shared/devtools/webaudioeditor.inc.css
@@ -105,9 +105,9 @@ g.edgePath.param-connection {
fill: #4c9ed9; /* Select Highlight Blue */
}
-/* Text in nodes */
+/* Text in nodes and edges */
text {
- cursor: pointer;
+ cursor: default; /* override the "text" cursor */
font-weight: 300;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
font-size: 14px;
@@ -123,6 +123,10 @@ text {
fill: #f0f1f2; /* Toolbars */
}
+.nodes text {
+ cursor: pointer;
+}
+
/**
* Inspector Styles
*/
diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js
index eb4d956c9448..77eea56ea0f6 100644
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -283,6 +283,13 @@ pref("browser.search.official", true);
// Control media casting feature
pref("browser.casting.enabled", true);
+#ifdef RELEASE_BUILD
+pref("browser.mirroring.enabled", false);
+pref("browser.mirroring.enabled.roku", false);
+#else
+pref("browser.mirroring.enabled", true);
+pref("browser.mirroring.enabled.roku", true);
+#endif
// Enable sparse localization by setting a few package locale overrides
pref("chrome.override_package.global", "browser");
diff --git a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
index 4b05034e3330..3a84e4b83627 100644
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
@@ -26,7 +26,7 @@
-
-
diff --git a/mobile/android/base/resources/values-land/layout.xml b/mobile/android/base/resources/values-land/layout.xml
index 863376cabcb1..00cc1d624540 100644
--- a/mobile/android/base/resources/values-land/layout.xml
+++ b/mobile/android/base/resources/values-land/layout.xml
@@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
- @layout/tabs_item_cell
+ @layout/tabs_item_cell
diff --git a/mobile/android/base/resources/values-large-v11/layout.xml b/mobile/android/base/resources/values-large-v11/layout.xml
index 863376cabcb1..00cc1d624540 100644
--- a/mobile/android/base/resources/values-large-v11/layout.xml
+++ b/mobile/android/base/resources/values-large-v11/layout.xml
@@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
- @layout/tabs_item_cell
+ @layout/tabs_item_cell
diff --git a/mobile/android/base/resources/values/layout.xml b/mobile/android/base/resources/values/layout.xml
index 9823217a76bc..854537a2ed91 100644
--- a/mobile/android/base/resources/values/layout.xml
+++ b/mobile/android/base/resources/values/layout.xml
@@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
- @layout/tabs_item_row
+ @layout/tabs_item_row
diff --git a/mobile/android/base/tabs/TabsLayoutAdapter.java b/mobile/android/base/tabs/TabsLayoutAdapter.java
index 6baaefdfa6d5..26cbcc27165c 100644
--- a/mobile/android/base/tabs/TabsLayoutAdapter.java
+++ b/mobile/android/base/tabs/TabsLayoutAdapter.java
@@ -81,7 +81,7 @@ public class TabsLayoutAdapter extends BaseAdapter {
}
View newView(int position, ViewGroup parent) {
- final View view = mInflater.inflate(R.layout.tabs_row, parent, false);
+ final View view = mInflater.inflate(R.layout.tabs_layout_item_view, parent, false);
final TabsLayoutItemView item = new TabsLayoutItemView(view);
view.setTag(item);
return view;
diff --git a/mobile/android/base/tabs/TabsPanel.java b/mobile/android/base/tabs/TabsPanel.java
index 7e09fbeb7068..58a85c77abd4 100644
--- a/mobile/android/base/tabs/TabsPanel.java
+++ b/mobile/android/base/tabs/TabsPanel.java
@@ -77,7 +77,7 @@ public class TabsPanel extends LinearLayout
private final GeckoApp mActivity;
private final LightweightTheme mTheme;
private RelativeLayout mHeader;
- private TabsListContainer mTabsContainer;
+ private PanelViewContainer mPanelsContainer;
private PanelView mPanel;
private PanelView mPanelNormal;
private PanelView mPanelPrivate;
@@ -137,7 +137,7 @@ public class TabsPanel extends LinearLayout
private void initialize() {
mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
- mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container);
+ mPanelsContainer = (PanelViewContainer) findViewById(R.id.tabs_container);
mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
mPanelNormal.setTabsPanel(this);
@@ -264,10 +264,10 @@ public class TabsPanel extends LinearLayout
return mActivity.onOptionsItemSelected(item);
}
- private static int getTabContainerHeight(TabsListContainer listContainer) {
- Resources resources = listContainer.getContext().getResources();
+ private static int getPanelsContainerHeight(PanelViewContainer panelsContainer) {
+ Resources resources = panelsContainer.getContext().getResources();
- PanelView panelView = listContainer.getCurrentPanelView();
+ PanelView panelView = panelsContainer.getCurrentPanelView();
if (panelView != null && !panelView.shouldExpand()) {
return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height);
}
@@ -276,7 +276,7 @@ public class TabsPanel extends LinearLayout
int screenHeight = resources.getDisplayMetrics().heightPixels;
Rect windowRect = new Rect();
- listContainer.getWindowVisibleDisplayFrame(windowRect);
+ panelsContainer.getWindowVisibleDisplayFrame(windowRect);
int windowHeight = windowRect.bottom - windowRect.top;
// The web content area should have at least 1.5x the height of the action bar.
@@ -323,9 +323,9 @@ public class TabsPanel extends LinearLayout
onLightweightThemeChanged();
}
- // Tabs List Container holds the ListView
- static class TabsListContainer extends FrameLayout {
- public TabsListContainer(Context context, AttributeSet attrs) {
+ // Panel View Container holds the ListView
+ static class PanelViewContainer extends FrameLayout {
+ public PanelViewContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -346,7 +346,7 @@ public class TabsPanel extends LinearLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!GeckoAppShell.getGeckoInterface().hasTabsSideBar()) {
- int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsListContainer.this), MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(getPanelsContainerHeight(PanelViewContainer.this), MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -468,7 +468,7 @@ public class TabsPanel extends LinearLayout
dispatchLayoutChange(getWidth(), getHeight());
} else {
int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
- int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
+ int height = actionBarHeight + getPanelsContainerHeight(mPanelsContainer);
dispatchLayoutChange(getWidth(), height);
}
mHeaderVisible = true;
@@ -526,13 +526,13 @@ public class TabsPanel extends LinearLayout
final int tabsPanelWidth = getWidth();
if (mVisible) {
ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
- ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
+ ViewHelper.setTranslationX(mPanelsContainer, -tabsPanelWidth);
// The footer view is only present on the sidebar, v11+.
ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
}
final int translationX = (mVisible ? 0 : -tabsPanelWidth);
- animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
+ animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX);
@@ -542,16 +542,16 @@ public class TabsPanel extends LinearLayout
final int translationY = (mVisible ? 0 : -toolbarHeight);
if (mVisible) {
ViewHelper.setTranslationY(mHeader, -toolbarHeight);
- ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
- ViewHelper.setAlpha(mTabsContainer, 0.0f);
+ ViewHelper.setTranslationY(mPanelsContainer, -toolbarHeight);
+ ViewHelper.setAlpha(mPanelsContainer, 0.0f);
}
- animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
- animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
+ animator.attach(mPanelsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
+ animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
}
mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ mPanelsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
public void finishTabsAnimation() {
@@ -560,7 +560,7 @@ public class TabsPanel extends LinearLayout
}
mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
- mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ mPanelsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
// If the tray is now hidden, call hide() on current panel and unset it as the current panel
// to avoid hide() being called again when the tray is opened next.
diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js
index d20d7c96ddca..3598204cb5f5 100644
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -16,6 +16,7 @@ var rokuDevice = {
Cu.import("resource://gre/modules/RokuApp.jsm");
return new RokuApp(aService);
},
+ mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"),
types: ["video/mp4"],
extensions: ["mp4"]
};
@@ -52,7 +53,7 @@ var CastingApps = {
mirrorStopMenuId: -1,
init: function ca_init() {
- if (!this.isEnabled()) {
+ if (!this.isCastingEnabled()) {
return;
}
@@ -99,22 +100,25 @@ var CastingApps = {
NativeWindow.contextmenus.remove(this._castMenuId);
},
+ _mirrorStarted: function(stopMirrorCallback) {
+ this.stopMirrorCallback = stopMirrorCallback;
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+ },
+
serviceAdded: function(aService) {
- if (aService.mirror && this.mirrorStartMenuId == -1) {
+ if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
this.mirrorStartMenuId = NativeWindow.menu.add({
name: Strings.browser.GetStringFromName("casting.mirrorTab"),
callback: function() {
- function callbackFunc(aService) {
+ let callbackFunc = function(aService) {
let app = SimpleServiceDiscovery.findAppForService(aService);
- if (app)
- app.mirror(function() {
- });
- }
+ if (app) {
+ app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this));
+ }
+ }.bind(this);
- function filterFunc(aService) {
- return aService.mirror == true;
- }
- this.prompt(callbackFunc, filterFunc);
+ this.prompt(callbackFunc, aService => aService.mirror);
}.bind(this),
parent: NativeWindow.menu.toolsMenuID
});
@@ -125,6 +129,9 @@ var CastingApps = {
if (this.tabMirror) {
this.tabMirror.stop();
this.tabMirror = null;
+ } else if (this.stopMirrorCallback) {
+ this.stopMirrorCallback();
+ this.stopMirrorCallback = null;
}
NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true });
NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
@@ -132,7 +139,9 @@ var CastingApps = {
parent: NativeWindow.menu.toolsMenuID
});
}
- NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ if (this.mirrorStartMenuId != -1) {
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ }
},
serviceLost: function(aService) {
@@ -150,10 +159,14 @@ var CastingApps = {
}
},
- isEnabled: function isEnabled() {
+ isCastingEnabled: function isCastingEnabled() {
return Services.prefs.getBoolPref("browser.casting.enabled");
},
+ isMirroringEnabled: function isMirroringEnabled() {
+ return Services.prefs.getBoolPref("browser.mirroring.enabled");
+ },
+
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "Casting:Play":
diff --git a/mobile/android/chrome/content/WebrtcUI.js b/mobile/android/chrome/content/WebrtcUI.js
index 9e6e717cd395..854e85f6b204 100644
--- a/mobile/android/chrome/content/WebrtcUI.js
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -4,14 +4,9 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "contentPrefs",
- "@mozilla.org/content-pref/service;1",
- "nsIContentPrefService2");
var WebrtcUI = {
_notificationId: null,
- VIDEO_SOURCE: "videoSource",
- AUDIO_SOURCE: "audioDevice",
observe: function(aSubject, aTopic, aData) {
if (aTopic === "getUserMedia:request") {
@@ -83,42 +78,39 @@ var WebrtcUI = {
contentWindow.navigator.mozGetUserMediaDevices(
constraints,
- function (aDevices) {
- WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio, constraints.video, aDevices);
+ function (devices) {
+ WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
+ constraints.video, devices);
},
- Cu.reportError, aSubject.innerWindowID);
+ function (error) {
+ Cu.reportError(error);
+ },
+ aSubject.innerWindowID);
},
- getDeviceButtons: function(aAudioDevices, aVideoDevices, aCallID, aHost) {
+ getDeviceButtons: function(audioDevices, videoDevices, aCallID) {
return [{
label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
- callback: () => {
+ callback: function() {
Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
}
- }, {
+ },
+ {
label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
- callback: (checked /* ignored */, inputs) => {
+ callback: function(checked /* ignored */, inputs) {
let allowedDevices = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
let audioId = 0;
- if (inputs && inputs[this.AUDIO_SOURCE] != undefined) {
- audioId = inputs[this.AUDIO_SOURCE];
- }
-
- if (aAudioDevices[audioId]) {
- allowedDevices.AppendElement(aAudioDevices[audioId]);
- this.setDefaultDevice(this.AUDIO_SOURCE, aAudioDevices[audioId].name, aHost);
- }
+ if (inputs && inputs.audioDevice != undefined)
+ audioId = inputs.audioDevice;
+ if (audioDevices[audioId])
+ allowedDevices.AppendElement(audioDevices[audioId]);
let videoId = 0;
- if (inputs && inputs[this.VIDEO_SOURCE] != undefined) {
- videoId = inputs[this.VIDEO_SOURCE];
- }
-
- if (aVideoDevices[videoId]) {
- allowedDevices.AppendElement(aVideoDevices[videoId]);
- this.setDefaultDevice(this.VIDEO_SOURCE, aVideoDevices[videoId].name, aHost);
- }
+ if (inputs && inputs.videoSource != undefined)
+ videoId = inputs.videoSource;
+ if (videoDevices[videoId])
+ allowedDevices.AppendElement(videoDevices[videoId]);
Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
}
@@ -129,34 +121,30 @@ var WebrtcUI = {
_getList: function(aDevices, aType) {
let defaultCount = 0;
return aDevices.map(function(device) {
- let name = device.name;
- // if this is a Camera input, convert the name to something readable
- let res = /Camera\ \d+,\ Facing (front|back)/.exec(name);
- if (res) {
- return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
- }
+ // if this is a Camera input, convert the name to something readable
+ let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
+ if (res)
+ return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
- if (name.startsWith("&") && name.endsWith(";")) {
- return Strings.browser.GetStringFromName(name.substring(1, name.length -1));
- }
+ if (device.name.startsWith("&") && device.name.endsWith(";"))
+ return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1));
- if (name.trim() == "") {
- defaultCount++;
- return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
- }
-
- return name;
- }, this);
+ if (device.name.trim() == "") {
+ defaultCount++;
+ return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
+ }
+ return device.name
+ }, this);
},
- _addDevicesToOptions: function(aDevices, aType, aOptions, aHost, aContext) {
- if (aDevices.length == 0) {
- return Promise.resolve(aOptions);
- }
+ _addDevicesToOptions: function(aDevices, aType, aOptions, extraOptions) {
+ if (aDevices.length) {
- let updateOptions = () => {
// Filter out empty items from the list
let list = this._getList(aDevices, aType);
+ if (extraOptions)
+ list = list.concat(extraOptions);
+
if (list.length > 0) {
aOptions.inputs.push({
id: aType,
@@ -164,96 +152,40 @@ var WebrtcUI = {
label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
values: list
});
+
}
-
- return aOptions;
- }
-
- return this.getDefaultDevice(aType, aHost, aContext).then((defaultDevice) => {
- aDevices.sort((a, b) => {
- if (b.name === defaultDevice) return 1;
- return 0;
- });
- return updateOptions();
- }).catch(updateOptions);
- },
-
- // Sets the default for a aHost. If no aHost is specified, sets the browser wide default.
- // Saving is async, but this doesn't wait for a result.
- setDefaultDevice: function(aType, aValue, aHost, aContext) {
- if (aHost) {
- contentPrefs.set(aHost, "webrtc." + aType, aValue, aContext);
- } else {
- contentPrefs.setGlobal("webrtc." + aType, aValue, aContext);
}
},
- _checkContentPref(aHost, aType, aContext) {
- return new Promise((resolve, reject) => {
- let result = null;
- let handler = {
- handleResult: (aResult) => result = aResult,
- handleCompletion: function(aReason) {
- if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK &&
- result instanceof Components.interfaces.nsIContentPref) {
- resolve(result.value);
- } else {
- reject(result);
- }
- }
- };
-
- if (aHost) {
- contentPrefs.getByDomainAndName(aHost, "webrtc." + aType, aContext, handler);
- } else {
- contentPrefs.getGlobal("webrtc." + aType, aContext, handler);
- }
- });
- },
-
- // Returns the default device for this aHost. If no aHost is specified, returns a browser wide default
- getDefaultDevice: function(aType, aHost, aContext) {
- return this._checkContentPref(aHost, aType, aContext).catch(() => {
- // If we found nothing for the initial pref, try looking for a global one
- return this._checkContentPref(null, aType, aContext);
- });
- },
-
- prompt: function (aWindow, aCallID, aAudioRequested, aVideoRequested, aDevices) {
+ prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
+ aVideoRequested, aDevices) {
let audioDevices = [];
let videoDevices = [];
-
- // Split up all the available aDevices into audio and video categories
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
switch (device.type) {
case "audio":
- if (aAudioRequested) {
+ if (aAudioRequested)
audioDevices.push(device);
- }
break;
case "video":
- if (aVideoRequested) {
+ if (aVideoRequested)
videoDevices.push(device);
- }
break;
}
}
- // Bsaed on the aTypes available, setup the prompt and icon text
let requestType;
- if (audioDevices.length && videoDevices.length) {
+ if (audioDevices.length && videoDevices.length)
requestType = "CameraAndMicrophone";
- } else if (audioDevices.length) {
+ else if (audioDevices.length)
requestType = "Microphone";
- } else if (videoDevices.length) {
+ else if (videoDevices.length)
requestType = "Camera";
- } else {
+ else
return;
- }
- let host = aWindow.document.documentURIObject.host;
- // Show the app name if this is a WebRT app, otherwise show the host.
+ let host = aContentWindow.document.documentURIObject.host;
let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
@@ -261,20 +193,24 @@ var WebrtcUI = {
// if the users only option would be to select "No Audio" or "No Video"
// i.e. we're only showing audio or only video and there is only one device for that type
// don't bother showing a menulist to select from
- if (videoDevices.length > 0 && audioDevices.length > 0) {
- videoDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.videoSource.none") });
- audioDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") });
+ var extraItems = null;
+ if (videoDevices.length > 1 || audioDevices.length > 0) {
+ // Only show the No Video option if there are also Audio devices to choose from
+ if (audioDevices.length > 0)
+ extraItems = [ Strings.browser.GetStringFromName("getUserMedia.videoSource.none") ];
+ // videoSource is both the string used for l10n lookup and the object that will be returned
+ this._addDevicesToOptions(videoDevices, "videoSource", options, extraItems);
}
- let loadContext = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsILoadContext);
- // videoSource is both the string used for l10n lookup and the object that will be returned
- this._addDevicesToOptions(videoDevices, this.VIDEO_SOURCE, options, host, loadContext).then((aOptions) => {
- return this._addDevicesToOptions(audioDevices, this.AUDIO_SOURCE, aOptions, host, loadContext);
- }).catch(Cu.reportError).then((aOptions) => {
- let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, host);
- NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, aOptions);
- });
+ if (audioDevices.length > 1 || videoDevices.length > 0) {
+ // Only show the No Audio option if there are also Video devices to choose from
+ if (videoDevices.length > 0)
+ extraItems = [ Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") ];
+ this._addDevicesToOptions(audioDevices, "audioDevice", options, extraItems);
+ }
+
+ let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID);
+
+ NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options);
}
}
diff --git a/mobile/android/modules/RokuApp.jsm b/mobile/android/modules/RokuApp.jsm
index 8fb5dfdd4177..bcd637162c6b 100644
--- a/mobile/android/modules/RokuApp.jsm
+++ b/mobile/android/modules/RokuApp.jsm
@@ -11,6 +11,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
+const WEBRTC_PLAYER_NAME = "WebRTC Player";
+const MIRROR_PORT = 8011;
+const JSON_MESSAGE_TERMINATOR = "\r\n";
+
function log(msg) {
//Services.console.logStringMessage(msg);
}
@@ -29,13 +33,14 @@ function RokuApp(service) {
#else
this.app = "Firefox Nightly";
#endif
- this.appID = -1;
+ this.mediaAppID = -1;
+ this.mirrorAppID = -1;
}
RokuApp.prototype = {
status: function status(callback) {
// We have no way to know if the app is running, so just return "unknown"
- // but we use this call to fetch the appID for the given app name
+ // but we use this call to fetch the mediaAppID for the given app name
let url = this.resourceURL + "query/apps";
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", url, true);
@@ -48,7 +53,9 @@ RokuApp.prototype = {
let apps = doc.querySelectorAll("app");
for (let app of apps) {
if (app.textContent == this.app) {
- this.appID = app.id;
+ this.mediaAppID = app.id;
+ } else if (app.textContent == WEBRTC_PLAYER_NAME) {
+ this.mirrorAppID = app.id
}
}
}
@@ -69,11 +76,11 @@ RokuApp.prototype = {
},
start: function start(callback) {
- // We need to make sure we have cached the appID
- if (this.appID == -1) {
+ // We need to make sure we have cached the mediaAppID
+ if (this.mediaAppID == -1) {
this.status(function() {
- // If we found the appID, use it to make a new start call
- if (this.appID != -1) {
+ // If we found the mediaAppID, use it to make a new start call
+ if (this.mediaAppID != -1) {
this.start(callback);
} else {
// We failed to start the app, so let the caller know
@@ -85,7 +92,7 @@ RokuApp.prototype = {
// Start a given app with any extra query data. Each app uses it's own data scheme.
// NOTE: Roku will also pass "source=external-control" as a param
- let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION);
+ let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION);
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", url, true);
xhr.overrideMimeType("text/plain");
@@ -129,7 +136,7 @@ RokuApp.prototype = {
},
remoteMedia: function remoteMedia(callback, listener) {
- if (this.appID != -1) {
+ if (this.mediaAppID != -1) {
if (callback) {
callback(new RemoteMedia(this.resourceURL, listener));
}
@@ -138,6 +145,44 @@ RokuApp.prototype = {
callback();
}
}
+ },
+
+ mirror: function(callback, win, viewport, mirrorStartedCallback) {
+ if (this.mirrorAppID == -1) {
+ // The status function may not have been called yet if mirrorAppID is -1
+ this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback));
+ } else {
+ this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback);
+ }
+ },
+
+ _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) {
+ if (this.mirrorAppID == -1) {
+ // TODO: Inform user to install Roku WebRTC Player Channel.
+ log("RokuApp: Failed to find Mirror App ID.");
+ } else {
+ let url = this.resourceURL + "launch/" + this.mirrorAppID;
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open("POST", url, true);
+ xhr.overrideMimeType("text/plain");
+
+ xhr.addEventListener("load", (function() {
+ // 204 seems to be returned if the channel is already running
+ if ((xhr.status == 200) || (xhr.status == 204)) {
+ this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback);
+ }
+ }).bind(this), false);
+
+ xhr.addEventListener("error", function() {
+ log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME);
+ }, false);
+
+ xhr.send(null);
+ }
+
+ if (callback) {
+ callback();
+ }
}
}
@@ -225,11 +270,153 @@ RemoteMedia.prototype = {
this._sendMsg({ type: "STOP" });
},
- load: function load(aData) {
- this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster });
+ load: function load(data) {
+ this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster });
},
get status() {
return this._status;
}
}
+
+function RemoteMirror(url, win, viewport, mirrorStartedCallback) {
+ this._serverURI = Services.io.newURI(url , null, null);
+ this._window = win;
+ this._iceCandidates = [];
+ this.mirrorStarted = mirrorStartedCallback;
+
+ // This code insures the generated tab mirror is not wider than 800 nor taller than 600
+ // Better dimensions should be chosen after the Roku Channel is working.
+ let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ let cWidth = Math.max(viewport.cssWidth, viewport.width);
+ let cHeight = Math.max(viewport.cssHeight, viewport.height);
+
+ const MAX_WIDTH = 800;
+ const MAX_HEIGHT = 600;
+
+ let tWidth = 0;
+ let tHeight = 0;
+
+ if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) {
+ tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight);
+ tWidth = MAX_WIDTH;
+ } else {
+ tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth);
+ tHeight = MAX_HEIGHT;
+ }
+
+ let constraints = {
+ video: {
+ mediaSource: "browser",
+ browserWindow: windowId,
+ scrollWithPage: true,
+ advanced: [
+ {
+ width: { min: tWidth, max: tWidth },
+ height: { min: tHeight, max: tHeight }
+ },
+ { aspectRatio: cWidth / cHeight }
+ ]
+ }
+ };
+
+ this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {});
+}
+
+RemoteMirror.prototype = {
+ _sendOffer: function(offer) {
+ if (!this._baseSocket) {
+ this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket);
+ }
+ this._jsonOffer = JSON.stringify(offer);
+ this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" });
+ this._socket.onopen = this._onSocketOpen.bind(this);
+ this._socket.ondata = this._onSocketData.bind(this);
+ this._socket.onerror = this._onSocketError.bind(this);
+ },
+
+ _onReceiveGUMStream: function(stream) {
+ this._pc = new this._window.mozRTCPeerConnection;
+ this._pc.addStream(stream);
+ this._pc.onicecandidate = (evt => {
+ // Usually the last candidate is null, expected?
+ if (!evt.candidate) {
+ return;
+ }
+ let jsonCandidate = JSON.stringify(evt.candidate);
+ this._iceCandidates.push(jsonCandidate);
+ this._sendIceCandidates();
+ });
+
+ this._pc.createOffer(offer => {
+ this._pc.setLocalDescription(
+ new this._window.mozRTCSessionDescription(offer),
+ () => this._sendOffer(offer),
+ () => log("RemoteMirror: Failed to set local description."));
+ },
+ () => log("RemoteMirror: Failed to create offer."));
+ },
+
+ _stopMirror: function() {
+ if (this._socket) {
+ this._socket.close();
+ this._socket = null;
+ }
+ if (this._pc) {
+ this._pc.close();
+ this._pc = null;
+ }
+ this._jsonOffer = null;
+ this._iceCandidates = [];
+ },
+
+ _onSocketData: function(response) {
+ if (response.type == "data") {
+ response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => {
+ if (data) {
+ let parsedData = JSON.parse(data);
+ if (parsedData.type == "answer") {
+ this._pc.setRemoteDescription(
+ new this._window.mozRTCSessionDescription(parsedData),
+ () => this.mirrorStarted(this._stopMirror.bind(this)),
+ () => log("RemoteMirror: Failed to set remote description."));
+ } else {
+ this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData))
+ }
+ } else {
+ log("RemoteMirror: data is null");
+ }
+ });
+ } else if (response.type == "error") {
+ log("RemoteMirror: Got socket error.");
+ this._stopMirror();
+ } else {
+ log("RemoteMirror: Got unhandled socket event: " + response.type);
+ }
+ },
+
+ _onSocketError: function(err) {
+ log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA"));
+ this._stopMirror();
+ },
+
+ _onSocketOpen: function() {
+ this._open = true;
+ if (this._jsonOffer) {
+ let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR;
+ this._socket.send(jsonOffer, jsonOffer.length);
+ this._jsonOffer = null;
+ this._sendIceCandidates();
+ }
+ },
+
+ _sendIceCandidates: function() {
+ if (this._socket && this._open) {
+ this._iceCandidates.forEach(value => {
+ value = value + JSON_MESSAGE_TERMINATOR;
+ this._socket.send(value, value.length);
+ });
+ this._iceCandidates = [];
+ }
+ }
+};
diff --git a/mobile/android/modules/SimpleServiceDiscovery.jsm b/mobile/android/modules/SimpleServiceDiscovery.jsm
index d23295b96a75..ca352e205b31 100644
--- a/mobile/android/modules/SimpleServiceDiscovery.jsm
+++ b/mobile/android/modules/SimpleServiceDiscovery.jsm
@@ -409,6 +409,10 @@ var SimpleServiceDiscovery = {
// Only add and notify if we don't already know about this service
if (!this._services.has(service.uuid)) {
+ let device = this._devices.get(service.target);
+ if (device && device.mirror) {
+ service.mirror = true;
+ }
this._services.set(service.uuid, service);
Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid);
}
diff --git a/toolkit/components/places/Bookmarks.jsm b/toolkit/components/places/Bookmarks.jsm
new file mode 100644
index 000000000000..88818bdd4e67
--- /dev/null
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -0,0 +1,295 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * This module provides an asynchronous API for managing bookmarks.
+ *
+ * Bookmarks are organized in a tree structure, and can be bookmarked URIs,
+ * folders or separators. Multiple bookmarks for the same URI are allowed.
+ *
+ * Note that if you are handling bookmarks operations in the UI, you should
+ * not use this API directly, but rather use PlacesTransactions.jsm, so that
+ * any operation is undo/redo-able.
+ *
+ * Each bookmarked item is represented by an object having the following
+ * properties:
+ *
+ * - guid (string)
+ * The globally unique identifier of the item.
+ * - parentGuid (string)
+ * The globally unique identifier of the folder containing the item.
+ * This will be an empty string for the Places root folder.
+ * - index (number)
+ * The 0-based position of the item in the parent folder.
+ * - dateAdded (number, microseconds from the epoch)
+ * The time at which the item was added. This is a PRTime (microseconds).
+ * - lastModified (number, microseconds from the epoch)
+ * The time at which the item was last modified. This is a PRTime (microseconds).
+ * - type (number)
+ * The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
+ *
+ * The following properties are only valid for bookmarks or folders.
+ *
+ * - title (string)
+ * The item's title, if any. Empty titles and null titles are considered
+ * the same and the property is unset on retrieval in such a case.
+ *
+ * The following properties are only valid for bookmarks:
+ *
+ * - uri (nsIURI)
+ * The item's URI.
+ * - keyword (string)
+ * The associated keyword, if any.
+ *
+ * Each successful operation notifies through the nsINavBookmarksObserver
+ * interface. To listen to such notifications you must register using
+ * nsINavBookmarksService addObserver and removeObserver methods.
+ * Note that bookmark addition or order changes won't notify onItemMoved for
+ * items that have their indexes changed.
+ * Similarly, lastModified changes not done explicitly (like changing another
+ * property) won't fire an onItemChanged notification for the lastModified
+ * property.
+ * @see nsINavBookmarkObserver
+ *
+ * @note livemarks are implemented as empty folders.
+ * @see mozIAsyncLivemarks.idl
+ */
+
+this.EXPORTED_SYMBOLS = [ "Bookmarks" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+const URI_LENGTH_MAX = 65536;
+const TITLE_LENGTH_MAX = 4096;
+
+let Bookmarks = Object.freeze({
+ /**
+ * Item's type constants.
+ * These should stay consistent with nsINavBookmarksService.idl
+ */
+ TYPE_BOOKMARK: 1,
+ TYPE_FOLDER: 2,
+ TYPE_SEPARATOR: 3,
+
+ /**
+ * Creates or updates a bookmarked item.
+ *
+ * If the given guid is found the corresponding item is updated, otherwise,
+ * if no guid is provided, a bookmark is created and a new guid is assigned
+ * to it.
+ *
+ * In the creation case, a minimum set of properties must be provided:
+ * - type
+ * - parentGuid
+ * - URI, only for bookmarks
+ * If an index is not specified, it defaults to appending.
+ * It's also possible to pass a non-existent guid to force creation of an
+ * item with the given guid, but unless you have a very sound reason, such as
+ * an undo manager implementation or synchronization, you should not do that.
+ *
+ * In the update case, you should only set the properties which should be
+ * changed, undefined properties won't be taken into account for the update.
+ * Moreover, the item's type and the guid are ignored, since they are
+ * immutable after creation. Note that if the passed in values are not
+ * coherent with the known values, this rejects.
+ * Passing null or an empty string as keyword clears any keyword
+ * associated with this bookmark.
+ *
+ * Note that any known property that doesn't apply to the specific item type
+ * causes rejection.
+ *
+ * @param info
+ * object representing a bookmarked item, as defined above.
+ *
+ * @return {Promise} resolved when the update is complete.
+ * @resolves to the input object, updated with relevant information.
+ * @rejects JavaScript exception.
+ *
+ * @note title is truncated to TITLE_LENGTH_MAX and URI is rejected if
+ * greater than URI_LENGTH_MAX.
+ */
+ // XXX WIP XXX Will replace functionality from these methods:
+ // long long insertBookmark(in long long aParentId, in nsIURI aURI, in long aIndex, in AUTF8String aTitle, [optional] in ACString aGUID);
+ // long long createFolder(in long long aParentFolder, in AUTF8String name, in long index, [optional] in ACString aGUID);
+ // void moveItem(in long long aItemId, in long long aNewParentId, in long aIndex);
+ // long long insertSeparator(in long long aParentId, in long aIndex, [optional] in ACString aGUID);
+ // void setItemTitle(in long long aItemId, in AUTF8String aTitle);
+ // void setItemDateAdded(in long long aItemId, in PRTime aDateAdded);
+ // void setItemLastModified(in long long aItemId, in PRTime aLastModified);
+ // void changeBookmarkURI(in long long aItemId, in nsIURI aNewURI);
+ // void setKeywordForBookmark(in long long aItemId, in AString aKeyword);
+ update: Task.async(function* (info) {
+ throw new Error("Not yet implemented");
+ }),
+
+ /**
+ * Removes a bookmarked item.
+ *
+ * Input can either be a guid or an object with one of the following
+ * properties set:
+ * - guid: if set, only the corresponding item is removed.
+ * - parentGuid: if it's set and is a folder, any children of that folder is
+ * removed, but not the folder itself.
+ * - URI: if set, any bookmark for that URI is removed.
+ * If multiple of these properties are set, the method rejects.
+ *
+ * Any other property is ignored, known properties may be overwritten.
+ *
+ * @param guidOrInfo
+ * The globally unique identifier of the item to remove, or an
+ * object representing it, as defined above.
+ *
+ * @return {Promise} resolved when the removal is complete.
+ * @resolves to the removed object or an array of them.
+ * @rejects JavaScript exception.
+ */
+ // XXX WIP XXX Will replace functionality from these methods:
+ // removeItem(in long long aItemId);
+ // removeFolderChildren(in long long aItemId);
+ remove: Task.async(function* (guidOrInfo) {
+ throw new Error("Not yet implemented");
+ }),
+
+ /**
+ * Fetches information about a bookmarked item.
+ *
+ * Input can be either a guid or an object with one, and only one, of these
+ * filtering properties set:
+ * - guid
+ * retrieves the item with the specified guid
+ * - parentGuid and index
+ * retrieves the item by its position
+ * - URI
+ * retrieves all items having the given URI.
+ * - keyword
+ * retrieves all items having the given keyword.
+ *
+ * Any other property is ignored. Known properties may be overwritten.
+ *
+ * @param guidOrInfo
+ * The globally unique identifier of the item to fetch, or an
+ * object representing it, as defined above.
+ *
+ * @return {Promise} resolved when the fetch is complete.
+ * @resolves to an object representing the found item, as described above, or
+ * an array of such objects. if no item is found, the returned
+ * promise is resolved to null.
+ * @rejects JavaScript exception.
+ */
+ // XXX WIP XXX Will replace functionality from these methods:
+ // long long getIdForItemAt(in long long aParentId, in long aIndex);
+ // AUTF8String getItemTitle(in long long aItemId);
+ // PRTime getItemDateAdded(in long long aItemId);
+ // PRTime getItemLastModified(in long long aItemId);
+ // nsIURI getBookmarkURI(in long long aItemId);
+ // long getItemIndex(in long long aItemId);
+ // unsigned short getItemType(in long long aItemId);
+ // boolean isBookmarked(in nsIURI aURI);
+ // long long getFolderIdForItem(in long long aItemId);
+ // void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count, [array, retval, size_is(count)] out long long bookmarks);
+ // AString getKeywordForURI(in nsIURI aURI);
+ // AString getKeywordForBookmark(in long long aItemId);
+ // nsIURI getURIForKeyword(in AString keyword);
+ fetch: Task.async(function* (guidOrInfo) {
+ throw new Error("Not yet implemented");
+ }),
+
+ /**
+ * Retrieves an object representation of a bookmarked item, along with all of
+ * its descendants, if any.
+ *
+ * Each node in the tree is an object that extends
+ * the item representation described above with some additional properties:
+ *
+ * - [deprecated] id (number)
+ * the item's id. Defined only if aOptions.includeItemIds is set.
+ * - annos (array)
+ * the item's annotations. This is not set if there are no annotations
+ * set for the item.
+ *
+ * The root object of the tree also has the following properties set:
+ * - itemsCount (number, not enumerable)
+ * the number of items, including the root item itself, which are
+ * represented in the resolved object.
+ *
+ * Bookmarked URIs may also have the following properties:
+ * - tags (string)
+ * csv string of the bookmark's tags, if any.
+ * - charset (string)
+ * the last known charset of the bookmark, if any.
+ * - iconuri (string)
+ * the bookmark's favicon URL, if any.
+ *
+ * Folders may also have the following properties:
+ * - children (array)
+ * the folder's children information, each of them having the same set of
+ * properties as above.
+ *
+ * @param [optional] guid
+ * the topmost item to be queried. If it's not passed, the Places
+ * root folder is queried: that is, you get a representation of the
+ * entire bookmarks hierarchy.
+ * @param [optional] options
+ * Options for customizing the query behavior, in the form of an
+ * object with any of the following properties:
+ * - excludeItemsCallback: a function for excluding items, along with
+ * their descendants. Given an item object (that has everything set
+ * apart its potential children data), it should return true if the
+ * item should be excluded. Once an item is excluded, the function
+ * isn't called for any of its descendants. This isn't called for
+ * the root item.
+ * WARNING: since the function may be called for each item, using
+ * this option can slow down the process significantly if the
+ * callback does anything that's not relatively trivial. It is
+ * highly recommended to avoid any synchronous I/O or DB queries.
+ * - includeItemIds: opt-in to include the deprecated id property.
+ * Use it if you must. It'll be removed once the switch to guids is
+ * complete.
+ *
+ * @return {Promise} resolved when the fetch is complete.
+ * @resolves to an object that represents either a single item or a
+ * bookmarks tree. if guid points to a non-existent item, the
+ * returned promise is resolved to null.
+ * @rejects JavaScript exception.
+ */
+ // XXX WIP XXX: will replace functionality for these methods:
+ // PlacesUtils.promiseBookmarksTree()
+ fetchTree: Task.async(function* (guid = "", options = {}) {
+ throw new Error("Not yet implemented");
+ }),
+
+ /**
+ * Reorders contents of a folder based on a provided array of GUIDs.
+ *
+ * @param parentGuid
+ * The globally unique identifier of the folder whose contents should
+ * be reordered.
+ * @param orderedChildrenGuids
+ * Ordered array of the children's GUIDs. If this list contains
+ * non-existing entries they will be ignored. If the list is
+ * incomplete, missing entries will be appended.
+ *
+ * @return {Promise} resolved when reordering is complete.
+ * @rejects JavaScript exception.
+ */
+ // XXX WIP XXX Will replace functionality from these methods:
+ // void setItemIndex(in long long aItemId, in long aNewIndex);
+ reorder: Task.async(function* (parentGuid, orderedChildrenGuids) {
+ throw new Error("Not yet implemented");
+ })
+});
diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm
index b415e37b7178..2ab9c422bec8 100644
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -25,33 +25,25 @@ this.EXPORTED_SYMBOLS = [
, "PlacesUntagURITransaction"
];
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cr = Components.results;
-const Cu = Components.utils;
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
-
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
+ "resource://gre/modules/Bookmarks.jsm");
// The minimum amount of transactions before starting a batch. Usually we do
// do incremental updates, a batch will cause views to completely
@@ -1834,9 +1826,14 @@ XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
"@mozilla.org/browser/favicon-service;1",
"mozIAsyncFavicons");
-XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks",
- "@mozilla.org/browser/nav-bookmarks-service;1",
- "nsINavBookmarksService");
+XPCOMUtils.defineLazyGetter(PlacesUtils, "bookmarks", () => {
+ let bm = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
+ .getService(Ci.nsINavBookmarksService);
+ return Object.freeze(new Proxy(bm, {
+ get: (target, name) => target.hasOwnProperty(name) ? target[name]
+ : Bookmarks[name]
+ }));
+});
XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
"@mozilla.org/browser/annotation-service;1",
diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build
index cf2d83d0ea5d..d18e019e82fe 100644
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -62,6 +62,7 @@ if CONFIG['MOZ_PLACES']:
EXTRA_JS_MODULES += [
'BookmarkHTMLUtils.jsm',
'BookmarkJSONUtils.jsm',
+ 'Bookmarks.jsm',
'ClusterLib.js',
'ColorAnalyzer_worker.js',
'ColorConversion.js',
diff --git a/toolkit/devtools/apps/tests/debugger-protocol-helper.js b/toolkit/devtools/apps/tests/debugger-protocol-helper.js
index 0434ee86b0ee..b215932cab0b 100644
--- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js
+++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js
@@ -7,6 +7,9 @@ const Cu = Components.utils;
const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
const { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {require} = devtools;
+
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
@@ -114,9 +117,46 @@ addMessageListener("install", function (aMessage) {
}
});
+addMessageListener("getAppActor", function (aMessage) {
+ let { manifestURL } = aMessage;
+ let request = {type: "getAppActor", manifestURL: manifestURL};
+ webappActorRequest(request, function (aResponse) {
+ sendAsyncMessage("appActor", aResponse);
+ });
+});
+
+let Frames = [];
+addMessageListener("addFrame", function (aMessage) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let doc = win.document;
+ let frame = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+ frame.setAttribute("mozbrowser", "true");
+ if (aMessage.mozapp) {
+ frame.setAttribute("mozapp", aMessage.mozapp);
+ }
+ if (aMessage.remote) {
+ frame.setAttribute("remote", aMessage.remote);
+ }
+ if (aMessage.src) {
+ frame.setAttribute("src", aMessage.src);
+ }
+ doc.documentElement.appendChild(frame);
+ Frames.push(frame);
+ sendAsyncMessage("frameAdded");
+});
+
addMessageListener("cleanup", function () {
webappActorRequest({type: "unwatchApps"}, function () {
gClient.close();
});
});
+let AppFramesMock = {
+ list: function () {
+ return Frames;
+ },
+ addObserver: function () {},
+ removeObserver: function () {}
+};
+
+require("devtools/server/actors/webapps").setAppFramesMock(AppFramesMock);
diff --git a/toolkit/devtools/apps/tests/test_webapps_actor.html b/toolkit/devtools/apps/tests/test_webapps_actor.html
index 063217da72c1..84424bd5eb50 100644
--- a/toolkit/devtools/apps/tests/test_webapps_actor.html
+++ b/toolkit/devtools/apps/tests/test_webapps_actor.html
@@ -224,6 +224,21 @@ var steps = [
info("== SETUP == Disable certified app access");
SpecialPowers.popPrefEnv(next);
},
+ function() {
+ info("== TEST == Get packaged app actor");
+ addFrame(
+ { mozapp: PACKAGED_APP_MANIFEST, remote: true },
+ function () {
+ getAppActor(PACKAGED_APP_MANIFEST, function (response) {
+ let tabActor = response.actor;
+ ok(!!tabActor, "TabActor is correctly instanciated in child.js");
+ ok("actor" in tabActor, "Tab actor is available in child");
+ ok("consoleActor" in tabActor, "Console actor is available in child");
+ next();
+ });
+ });
+
+ },
function() {
info("== TEST == Uninstall packaged app");
uninstall(PACKAGED_APP_MANIFEST);
@@ -309,6 +324,22 @@ function uninstall(manifestURL) {
});
}
+function getAppActor(manifestURL, callback) {
+ mm.addMessageListener("appActor", function onAppActor(aResponse) {
+ mm.removeMessageListener("appActor", onAppActor);
+ callback(aResponse);
+ });
+ mm.sendAsyncMessage("getAppActor", { manifestURL: manifestURL });
+}
+
+function addFrame(options, callback) {
+ mm.addMessageListener("frameAdded", function onFrameAdded() {
+ mm.removeMessageListener("frameAdded", onFrameAdded);
+ callback();
+ });
+ mm.sendAsyncMessage("addFrame", options);
+}
+
diff --git a/toolkit/devtools/server/actors/webapps.js b/toolkit/devtools/server/actors/webapps.js
index 044edf407270..def41829a97b 100644
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -4,10 +4,7 @@
"use strict";
-let Cu = Components.utils;
-let Cc = Components.classes;
-let Ci = Components.interfaces;
-let CC = Components.Constructor;
+let {Cu, Cc, Ci} = require("chrome");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -16,7 +13,22 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+let { ActorPool } = require("devtools/server/actors/common");
+let { DebuggerServer } = require("devtools/server/main");
+let Services = require("Services");
+
+let AppFramesMock = null;
+
+exports.setAppFramesMock = function (mock) {
+ AppFramesMock = mock;
+}
+
DevToolsUtils.defineLazyGetter(this, "AppFrames", () => {
+ // Offer a way for unit test to provide a mock
+ if (AppFramesMock) {
+ return AppFramesMock;
+ }
try {
return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames;
} catch(e) {}
@@ -518,11 +530,11 @@ WebappsActor.prototype = {
// frame script. That will flush the jar cache for this app and allow
// loading fresh updated resources if we reload its document.
let FlushFrameScript = function (path) {
- let jar = Components.classes["@mozilla.org/file/local;1"]
- .createInstance(Components.interfaces.nsILocalFile);
+ let jar = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
jar.initWithPath(path);
- let obs = Components.classes["@mozilla.org/observer-service;1"]
- .getService(Components.interfaces.nsIObserverService);
+ let obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
obs.notifyObservers(jar, "flush-cache-entry", null);
};
for each (let frame in self._appFrames()) {
@@ -1038,4 +1050,4 @@ WebappsActor.prototype.requestTypes = {
"getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL
};
-DebuggerServer.addGlobalActor(WebappsActor, "webappsActor");
+exports.WebappsActor = WebappsActor;
diff --git a/toolkit/devtools/server/child.js b/toolkit/devtools/server/child.js
index ff37fd1b14d0..e2b3e70b99b2 100644
--- a/toolkit/devtools/server/child.js
+++ b/toolkit/devtools/server/child.js
@@ -4,6 +4,8 @@
"use strict";
+try {
+
let chromeGlobal = this;
// Encapsulate in its own scope to allows loading this frame script
@@ -64,3 +66,7 @@ let chromeGlobal = this;
});
addMessageListener("debug:disconnect", onDisconnect);
})();
+
+} catch(e) {
+ dump("Exception in app child process: " + e + "\n");
+}
diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js
index 9ab852f455fd..a9417175c935 100644
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -442,7 +442,11 @@ var DebuggerServer = {
});
}
- this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
+ this.registerModule("devtools/server/actors/webapps", {
+ prefix: "webapps",
+ constructor: "WebappsActor",
+ type: { global: true }
+ });
this.registerModule("devtools/server/actors/device", {
prefix: "device",
constructor: "DeviceActor",
diff --git a/toolkit/modules/Sqlite.jsm b/toolkit/modules/Sqlite.jsm
index 6a2ec123e654..57d432441e84 100644
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -37,6 +37,12 @@ XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
// used for logging to distinguish connection instances.
let connectionCounters = new Map();
+// Tracks identifiers of wrapped connections, that are Storage connections
+// opened through mozStorage and then wrapped by Sqlite.jsm to use its syntactic
+// sugar API. Since these connections have an unknown origin, we use this set
+// to differentiate their behavior.
+let wrappedConnections = new Set();
+
/**
* Once `true`, reject any attempt to open or close a database.
*/
@@ -66,6 +72,20 @@ function logScriptError(message) {
}
}
+/**
+ * Gets connection identifier from its database file path.
+ *
+ * @param path
+ * A file string path pointing to a database file.
+ * @return the connection identifier.
+ */
+function getIdentifierByPath(path) {
+ let basename = OS.Path.basename(path);
+ let number = connectionCounters.get(basename) || 0;
+ connectionCounters.set(basename, number + 1);
+ return basename + "#" + number;
+}
+
/**
* Barriers used to ensure that Sqlite.jsm is shutdown after all
* its clients.
@@ -95,17 +115,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => {
* The observer is passed the connection identifier of the database
* connection that is being finalized.
*/
- let finalizationObserver = function (subject, topic, connectionIdentifier) {
- let connectionData = ConnectionData.byId.get(connectionIdentifier);
+ let finalizationObserver = function (subject, topic, identifier) {
+ let connectionData = ConnectionData.byId.get(identifier);
if (connectionData === undefined) {
logScriptError("Error: Attempt to finalize unknown Sqlite connection: " +
- connectionIdentifier + "\n");
+ identifier + "\n");
return;
}
- ConnectionData.byId.delete(connectionIdentifier);
- logScriptError("Warning: Sqlite connection '" + connectionIdentifier +
+ ConnectionData.byId.delete(identifier);
+ logScriptError("Warning: Sqlite connection '" + identifier +
"' was not properly closed. Auto-close triggered by garbage collection.\n");
connectionData.close();
};
@@ -170,13 +190,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => {
* OpenedConnection needs to use the methods in this object, it will
* dispatch its method calls here.
*/
-function ConnectionData(connection, basename, number, options) {
- this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
- "Conn #" + number + ": ");
+function ConnectionData(connection, identifier, options={}) {
+ this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." +
+ identifier + ": ");
this._log.info("Opened");
this._dbConn = connection;
- this._connectionIdentifier = basename + " Conn #" + number;
+
+ // This is a unique identifier for the connection, generated through
+ // getIdentifierByPath. It may be used for logging or as a key in Maps.
+ this._identifier = identifier;
+
this._open = true;
this._cachedStatements = new Map();
@@ -204,10 +228,10 @@ function ConnectionData(connection, basename, number, options) {
this._closeRequested = false;
Barriers.connections.client.addBlocker(
- this._connectionIdentifier + ": waiting for shutdown",
+ this._identifier + ": waiting for shutdown",
this._deferredClose.promise,
() => ({
- identifier: this._connectionIdentifier,
+ identifier: this._identifier,
isCloseRequested: this._closeRequested,
hasDbConn: !!this._dbConn,
hasInProgressTransaction: !!this._inProgressTransaction,
@@ -224,7 +248,7 @@ function ConnectionData(connection, basename, number, options) {
* database. Used by finalization witnesses to be able to close opened
* connections on garbage collection.
*
- * Key: _connectionIdentifier of ConnectionData
+ * Key: _identifier of ConnectionData
* Value: ConnectionData object
*/
ConnectionData.byId = new Map();
@@ -304,15 +328,23 @@ ConnectionData.prototype = Object.freeze({
// function and asyncClose() finishing. See also bug 726990.
this._open = false;
- this._log.debug("Calling asyncClose().");
- this._dbConn.asyncClose(() => {
+ // We must always close the connection at the Sqlite.jsm-level, not
+ // necessarily at the mozStorage-level.
+ let markAsClosed = () => {
this._log.info("Closed");
this._dbConn = null;
// Now that the connection is closed, no need to keep
// a blocker for Barriers.connections.
Barriers.connections.client.removeBlocker(deferred.promise);
deferred.resolve();
- });
+ }
+ if (wrappedConnections.has(this._identifier)) {
+ wrappedConnections.delete(this._identifier);
+ markAsClosed();
+ } else {
+ this._log.debug("Calling asyncClose().");
+ this._dbConn.asyncClose(markAsClosed);
+ }
},
executeCached: function (sql, params=null, onRow=null) {
@@ -722,12 +754,7 @@ function openConnection(options) {
}
let file = FileUtils.File(path);
-
- let basename = OS.Path.basename(path);
- let number = connectionCounters.get(basename) || 0;
- connectionCounters.set(basename, number + 1);
-
- let identifier = basename + "#" + number;
+ let identifier = getIdentifierByPath(path);
log.info("Opening database: " + path + " (" + identifier + ")");
let deferred = Promise.defer();
@@ -746,8 +773,8 @@ function openConnection(options) {
log.info("Connection opened");
try {
deferred.resolve(
- new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number,
- openedOptions));
+ new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
+ identifier, openedOptions));
} catch (ex) {
log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
deferred.reject(ex);
@@ -759,7 +786,7 @@ function openConnection(options) {
/**
* Creates a clone of an existing and open Storage connection. The clone has
* the same underlying characteristics of the original connection and is
- * returned in form of on OpenedConnection handle.
+ * returned in form of an OpenedConnection handle.
*
* The following parameters can control the cloned connection:
*
@@ -812,10 +839,7 @@ function cloneStorageConnection(options) {
}
let path = source.databaseFile.path;
- let basename = OS.Path.basename(path);
- let number = connectionCounters.get(basename) || 0;
- connectionCounters.set(basename, number + 1);
- let identifier = basename + "#" + number;
+ let identifier = getIdentifierByPath(path);
log.info("Cloning database: " + path + " (" + identifier + ")");
let deferred = Promise.defer();
@@ -828,8 +852,7 @@ function cloneStorageConnection(options) {
log.info("Connection cloned");
try {
let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
- deferred.resolve(new OpenedConnection(conn, basename, number,
- openedOptions));
+ deferred.resolve(new OpenedConnection(conn, identifier, openedOptions));
} catch (ex) {
log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
deferred.reject(ex);
@@ -838,6 +861,55 @@ function cloneStorageConnection(options) {
return deferred.promise;
}
+/**
+ * Wraps an existing and open Storage connection with Sqlite.jsm API. The
+ * wrapped connection clone has the same underlying characteristics of the
+ * original connection and is returned in form of an OpenedConnection handle.
+ *
+ * Clients are responsible for closing both the Sqlite.jsm wrapper and the
+ * underlying mozStorage connection.
+ *
+ * The following parameters can control the wrapped connection:
+ *
+ * connection -- (mozIStorageAsyncConnection) The original Storage connection
+ * to wrap.
+ *
+ * @param options
+ * (Object) Parameters to control connection and wrap options.
+ *
+ * @return Promise
+ */
+function wrapStorageConnection(options) {
+ let log = Log.repository.getLogger("Sqlite.ConnectionWrapper");
+
+ let connection = options && options.connection;
+ if (!connection || !(connection instanceof Ci.mozIStorageAsyncConnection)) {
+ throw new TypeError("connection not specified or invalid.");
+ }
+
+ if (isClosed) {
+ throw new Error("Sqlite.jsm has been shutdown. Cannot wrap connection to: " + connection.database.path);
+ }
+
+ let path = connection.databaseFile.path;
+ let identifier = getIdentifierByPath(path);
+
+ log.info("Wrapping database: " + path + " (" + identifier + ")");
+ return new Promise(resolve => {
+ try {
+ let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
+ let wrapper = new OpenedConnection(conn, identifier);
+ // We must not handle shutdown of a wrapped connection, since that is
+ // already handled by the opener.
+ wrappedConnections.add(identifier);
+ resolve(wrapper);
+ } catch (ex) {
+ log.warn("Could not wrap database: " + CommonUtils.exceptionStr(ex));
+ throw ex;
+ }
+ });
+}
+
/**
* Handle on an opened SQLite database.
*
@@ -877,25 +949,24 @@ function cloneStorageConnection(options) {
*
* @param connection
* (mozIStorageConnection) Underlying SQLite connection.
- * @param basename
- * (string) The basename of this database name. Used for logging.
- * @param number
- * (Number) The connection number to this database.
- * @param options
+ * @param identifier
+ * (string) The unique identifier of this database. It may be used for
+ * logging or as a key in Maps.
+ * @param options [optional]
* (object) Options to control behavior of connection. See
* `openConnection`.
*/
-function OpenedConnection(connection, basename, number, options) {
+function OpenedConnection(connection, identifier, options={}) {
// Store all connection data in a field distinct from the
// witness. This enables us to store an additional reference to this
// field without preventing garbage collection of
// OpenedConnection. On garbage collection, we will still be able to
// close the database using this extra reference.
- this._connectionData = new ConnectionData(connection, basename, number, options);
+ this._connectionData = new ConnectionData(connection, identifier, options);
// Store the extra reference in a map with connection identifier as
// key.
- ConnectionData.byId.set(this._connectionData._connectionIdentifier,
+ ConnectionData.byId.set(this._connectionData._identifier,
this._connectionData);
// Make a finalization witness. If this object is garbage collected
@@ -904,7 +975,7 @@ function OpenedConnection(connection, basename, number, options) {
// connection identifier string of the database.
this._witness = FinalizationWitnessService.make(
"sqlite-finalization-witness",
- this._connectionData._connectionIdentifier);
+ this._connectionData._identifier);
}
OpenedConnection.prototype = Object.freeze({
@@ -964,8 +1035,8 @@ OpenedConnection.prototype = Object.freeze({
// Unless cleanup has already been done by a previous call to
// `close`, delete the database entry from map and tell the
// finalization witness to forget.
- if (ConnectionData.byId.has(this._connectionData._connectionIdentifier)) {
- ConnectionData.byId.delete(this._connectionData._connectionIdentifier);
+ if (ConnectionData.byId.has(this._connectionData._identifier)) {
+ ConnectionData.byId.delete(this._connectionData._identifier);
this._witness.forget();
}
return this._connectionData.close();
@@ -1175,6 +1246,7 @@ OpenedConnection.prototype = Object.freeze({
this.Sqlite = {
openConnection: openConnection,
cloneStorageConnection: cloneStorageConnection,
+ wrapStorageConnection: wrapStorageConnection,
/**
* Shutdown barrier client. May be used by clients to perform last-minute
* cleanup prior to the shutdown of this module.
diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js
index 58407e7821ef..104fdfb9900e 100644
--- a/toolkit/modules/tests/xpcshell/test_sqlite.js
+++ b/toolkit/modules/tests/xpcshell/test_sqlite.js
@@ -845,12 +845,12 @@ add_task(function test_direct() {
add_task(function* test_cloneStorageConnection() {
let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir,
"test_cloneStorageConnection.sqlite"));
- let c = yield new Promise((success, failure) => {
+ let c = yield new Promise((resolve, reject) => {
Services.storage.openAsyncDatabase(file, null, (status, db) => {
if (Components.isSuccessCode(status)) {
- success(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+ resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
} else {
- failure(new Error(status));
+ reject(new Error(status));
}
});
});
@@ -913,6 +913,33 @@ add_task(function* test_readOnly_clone() {
yield clone.close();
});
+/**
+ * Test Sqlite.wrapStorageConnection.
+ */
+add_task(function* test_wrapStorageConnection() {
+ let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir,
+ "test_wrapStorageConnection.sqlite"));
+ let c = yield new Promise((resolve, reject) => {
+ Services.storage.openAsyncDatabase(file, null, (status, db) => {
+ if (Components.isSuccessCode(status)) {
+ resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+ } else {
+ reject(new Error(status));
+ }
+ });
+ });
+
+ let wrapper = yield Sqlite.wrapStorageConnection({ connection: c });
+ // Just check that it works.
+ yield wrapper.execute("SELECT 1");
+ yield wrapper.executeCached("SELECT 1");
+
+ // Closing the wrapper should just finalize statements but not close the
+ // database.
+ yield wrapper.close();
+ yield c.asyncClose();
+});
+
/**
* Test finalization
*/
@@ -921,7 +948,7 @@ add_task(function* test_closed_by_witness() {
let c = yield getDummyDatabase("closed_by_witness");
Services.obs.notifyObservers(null, "sqlite-finalization-witness",
- c._connectionData._connectionIdentifier);
+ c._connectionData._identifier);
// Since we triggered finalization ourselves, tell the witness to
// forget the connection so it does not trigger a finalization again
c._witness.forget();
@@ -933,7 +960,7 @@ add_task(function* test_closed_by_witness() {
add_task(function* test_warning_message_on_finalization() {
failTestsOnAutoClose(false);
let c = yield getDummyDatabase("warning_message_on_finalization");
- let connectionIdentifier = c._connectionData._connectionIdentifier;
+ let identifier = c._connectionData._identifier;
let deferred = Promise.defer();
let listener = {
@@ -941,14 +968,14 @@ add_task(function* test_warning_message_on_finalization() {
let messageText = msg.message;
// Make sure the message starts with a warning containing the
// connection identifier
- if (messageText.indexOf("Warning: Sqlite connection '" + connectionIdentifier + "'") !== -1) {
+ if (messageText.indexOf("Warning: Sqlite connection '" + identifier + "'") !== -1) {
deferred.resolve();
}
}
};
Services.console.registerListener(listener);
- Services.obs.notifyObservers(null, "sqlite-finalization-witness", connectionIdentifier);
+ Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier);
// Since we triggered finalization ourselves, tell the witness to
// forget the connection so it does not trigger a finalization again
c._witness.forget();
diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h
index 6fc2d818886e..4171a71716e7 100644
--- a/widget/cocoa/nsMenuBarX.h
+++ b/widget/cocoa/nsMenuBarX.h
@@ -103,7 +103,6 @@ public:
// The following content nodes have been removed from the menu system.
// We save them here for use in command handling.
nsCOMPtr mAboutItemContent;
- nsCOMPtr mUpdateItemContent;
nsCOMPtr mPrefItemContent;
nsCOMPtr mQuitItemContent;
diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm
index 1934d5697b08..66e4bb992769 100644
--- a/widget/cocoa/nsMenuBarX.mm
+++ b/widget/cocoa/nsMenuBarX.mm
@@ -37,7 +37,6 @@ BOOL gSomeMenuBarPainted = NO;
// window does not have a quit or pref item. We don't need strong refs here because
// these items are always strong ref'd by their owning menu bar (instance variable).
static nsIContent* sAboutItemContent = nullptr;
-static nsIContent* sUpdateItemContent = nullptr;
static nsIContent* sPrefItemContent = nullptr;
static nsIContent* sQuitItemContent = nullptr;
@@ -75,8 +74,6 @@ nsMenuBarX::~nsMenuBarX()
// hidden window, thus we need to invalidate the weak references.
if (sAboutItemContent == mAboutItemContent)
sAboutItemContent = nullptr;
- if (sUpdateItemContent == mUpdateItemContent)
- sUpdateItemContent = nullptr;
if (sQuitItemContent == mQuitItemContent)
sQuitItemContent = nullptr;
if (sPrefItemContent == mPrefItemContent)
@@ -478,13 +475,6 @@ void nsMenuBarX::AquifyMenuBar()
if (!sAboutItemContent)
sAboutItemContent = mAboutItemContent;
- // Hide the software update menu item, since it belongs in the application
- // menu on Mac OS X.
- HideItem(domDoc, NS_LITERAL_STRING("updateSeparator"), nullptr);
- HideItem(domDoc, NS_LITERAL_STRING("checkForUpdates"), getter_AddRefs(mUpdateItemContent));
- if (!sUpdateItemContent)
- sUpdateItemContent = mUpdateItemContent;
-
// remove quit item and its separator
HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr);
HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent));
@@ -600,7 +590,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu)
========================
= About This App = <- aboutName
- = Check for Updates... = <- checkForUpdates
========================
= Preferences... = <- menu_preferences
========================
@@ -644,17 +633,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu)
addAboutSeparator = TRUE;
}
- // Add the software update menu item
- itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("checkForUpdates"), @selector(menuItemHit:),
- eCommand_ID_Update, nsMenuBarX::sNativeEventTarget);
- if (itemBeingAdded) {
- [sApplicationMenu addItem:itemBeingAdded];
- [itemBeingAdded release];
- itemBeingAdded = nil;
-
- addAboutSeparator = TRUE;
- }
-
// Add separator if either the About item or software update item exists
if (addAboutSeparator)
[sApplicationMenu addItem:[NSMenuItem separatorItem]];
@@ -950,12 +928,6 @@ static BOOL gMenuItemsExecuteCommands = YES;
nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
return;
}
- else if (tag == eCommand_ID_Update) {
- nsIContent* mostSpecificContent = sUpdateItemContent;
- if (menuBar && menuBar->mUpdateItemContent)
- mostSpecificContent = menuBar->mUpdateItemContent;
- nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
- }
else if (tag == eCommand_ID_Prefs) {
nsIContent* mostSpecificContent = sPrefItemContent;
if (menuBar && menuBar->mPrefItemContent)