diff options
Diffstat (limited to 'dom/presentation/provider')
20 files changed, 5884 insertions, 0 deletions
diff --git a/dom/presentation/provider/AndroidCastDeviceProvider.js b/dom/presentation/provider/AndroidCastDeviceProvider.js new file mode 100644 index 000000000..cf555f77b --- /dev/null +++ b/dom/presentation/provider/AndroidCastDeviceProvider.js @@ -0,0 +1,461 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ +/* globals Components, dump */ +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +// globals XPCOMUtils +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +// globals Services +Cu.import("resource://gre/modules/Services.jsm"); +// globals Messaging +Cu.import("resource://gre/modules/Messaging.jsm"); + +function log(str) { + // dump("-*- AndroidCastDeviceProvider -*-: " + str + "\n"); +} + +// Helper function: transfer nsIPresentationChannelDescription to json +function descriptionToString(aDescription) { + let json = {}; + json.type = aDescription.type; + switch(aDescription.type) { + case Ci.nsIPresentationChannelDescription.TYPE_TCP: + let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray); + json.tcpAddress = []; + for (let idx = 0; idx < addresses.length; idx++) { + let address = addresses.queryElementAt(idx, Ci.nsISupportsCString); + json.tcpAddress.push(address.data); + } + json.tcpPort = aDescription.tcpPort; + break; + case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL: + json.dataChannelSDP = aDescription.dataChannelSDP; + break; + } + return JSON.stringify(json); +} + +const TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE = "AndroidCastDevice:SyncDevice"; +const TOPIC_ANDROID_CAST_DEVICE_ADDED = "AndroidCastDevice:Added"; +const TOPIC_ANDROID_CAST_DEVICE_REMOVED = "AndroidCastDevice:Removed"; +const TOPIC_ANDROID_CAST_DEVICE_START = "AndroidCastDevice:Start"; +const TOPIC_ANDROID_CAST_DEVICE_STOP = "AndroidCastDevice:Stop"; +const TOPIC_PRESENTATION_VIEW_READY = "presentation-view-ready"; + +function LocalControlChannel(aProvider, aDeviceId, aRole) { + log("LocalControlChannel - create new LocalControlChannel for : " + + aRole); + this._provider = aProvider; + this._deviceId = aDeviceId; + this._role = aRole; +} + +LocalControlChannel.prototype = { + _listener: null, + _provider: null, + _deviceId: null, + _role: null, + _isOnTerminating: false, + _isOnDisconnecting: false, + _pendingConnected: false, + _pendingDisconnect: null, + _pendingOffer: null, + _pendingCandidate: null, + /* For the controller, it would be the control channel of the receiver. + * For the receiver, it would be the control channel of the controller. */ + _correspondingControlChannel: null, + + set correspondingControlChannel(aCorrespondingControlChannel) { + this._correspondingControlChannel = aCorrespondingControlChannel; + }, + + get correspondingControlChannel() { + return this._correspondingControlChannel; + }, + + notifyConnected: function LCC_notifyConnected() { + this._pendingDisconnect = null; + + if (!this._listener) { + this._pendingConnected = true; + } else { + this._listener.notifyConnected(); + } + }, + + onOffer: function LCC_onOffer(aOffer) { + if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) { + log("LocalControlChannel - onOffer of controller should not be called."); + return; + } + if (!this._listener) { + this._pendingOffer = aOffer; + } else { + this._listener.onOffer(aOffer); + } + }, + + onAnswer: function LCC_onAnswer(aAnswer) { + if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) { + log("LocalControlChannel - onAnswer of receiver should not be called."); + return; + } + this._listener.onAnswer(aAnswer); + }, + + notifyIceCandidate: function LCC_notifyIceCandidate(aCandidate) { + if (!this._listener) { + this._pendingCandidate = aCandidate; + } else { + this._listener.onIceCandidate(aCandidate); + } + }, + + // nsIPresentationControlChannel + get listener() { + return this._listener; + }, + + set listener(aListener) { + this._listener = aListener; + + if (!this._listener) { + return; + } + + if (this._pendingConnected) { + this.notifyConnected(); + this._pendingConnected = false; + } + + if (this._pendingOffer) { + this.onOffer(this._pendingOffer); + this._pendingOffer = null; + } + + if (this._pendingCandidate) { + this.notifyIceCandidate(this._pendingCandidate); + this._pendingCandidate = null; + } + + if (this._pendingDisconnect != null) { + this.disconnect(this._pendingDisconnect); + this._pendingDisconnect = null; + } + }, + + sendOffer: function LCC_sendOffer(aOffer) { + if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) { + log("LocalControlChannel - sendOffer of receiver should not be called."); + return; + } + log("LocalControlChannel - sendOffer aOffer=" + descriptionToString(aOffer)); + this._correspondingControlChannel.onOffer(aOffer); + }, + + sendAnswer: function LCC_sendAnswer(aAnswer) { + if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) { + log("LocalControlChannel - sendAnswer of controller should not be called."); + return; + } + log("LocalControlChannel - sendAnswer aAnswer=" + descriptionToString(aAnswer)); + this._correspondingControlChannel.onAnswer(aAnswer); + }, + + sendIceCandidate: function LCC_sendIceCandidate(aCandidate) { + log("LocalControlChannel - sendAnswer aCandidate=" + aCandidate); + this._correspondingControlChannel.notifyIceCandidate(aCandidate); + }, + + launch: function LCC_launch(aPresentationId, aUrl) { + log("LocalControlChannel - launch aPresentationId=" + + aPresentationId + " aUrl=" + aUrl); + // Create control channel for receiver directly. + let controlChannel = new LocalControlChannel(this._provider, + this._deviceId, + Ci.nsIPresentationService.ROLE_RECEIVER); + + // Set up the corresponding control channels for both controller and receiver. + this._correspondingControlChannel = controlChannel; + controlChannel._correspondingControlChannel = this; + + this._provider.onSessionRequest(this._deviceId, + aUrl, + aPresentationId, + controlChannel); + controlChannel.notifyConnected(); + }, + + terminate: function LCC_terminate(aPresentationId) { + log("LocalControlChannel - terminate aPresentationId=" + + aPresentationId); + + if (this._isOnTerminating) { + return; + } + + // Create control channel for corresponding role directly. + let correspondingRole = this._role == Ci.nsIPresentationService.ROLE_CONTROLLER + ? Ci.nsIPresentationService.ROLE_RECEIVER + : Ci.nsIPresentationService.ROLE_CONTROLLER; + let controlChannel = new LocalControlChannel(this._provider, + this._deviceId, + correspondingRole); + // Prevent the termination recursion. + controlChannel._isOnTerminating = true; + + // Set up the corresponding control channels for both controller and receiver. + this._correspondingControlChannel = controlChannel; + controlChannel._correspondingControlChannel = this; + + this._provider.onTerminateRequest(this._deviceId, + aPresentationId, + controlChannel, + this._role == Ci.nsIPresentationService.ROLE_RECEIVER); + controlChannel.notifyConnected(); + }, + + disconnect: function LCC_disconnect(aReason) { + log("LocalControlChannel - disconnect aReason=" + aReason); + + if (this._isOnDisconnecting) { + return; + } + + this._pendingOffer = null; + this._pendingCandidate = null; + this._pendingConnected = false; + + // this._pendingDisconnect is a nsresult. + // If it is null, it means no pending disconnect. + // If it is NS_OK, it means this control channel is disconnected normally. + // If it is other nsresult value, it means this control channel is + // disconnected abnormally. + + // Remote endpoint closes the control channel with abnormal reason. + if (aReason == Cr.NS_OK && + this._pendingDisconnect != null && + this._pendingDisconnect != Cr.NS_OK) { + aReason = this._pendingDisconnect; + } + + if (!this._listener) { + this._pendingDisconnect = aReason; + return; + } + + this._isOnDisconnecting = true; + this._correspondingControlChannel.disconnect(aReason); + this._listener.notifyDisconnected(aReason); + }, + + reconnect: function LCC_reconnect(aPresentationId, aUrl) { + log("1-UA on Android doesn't support reconnect."); + throw Cr.NS_ERROR_FAILURE; + }, + + classID: Components.ID("{c9be9450-e5c7-4294-a287-376971b017fd}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), +}; + +function ChromecastRemoteDisplayDevice(aProvider, aId, aName, aRole) { + this._provider = aProvider; + this._id = aId; + this._name = aName; + this._role = aRole; +} + +ChromecastRemoteDisplayDevice.prototype = { + _id: null, + _name: null, + _role: null, + _provider: null, + _ctrlChannel: null, + + update: function CRDD_update(aName) { + this._name = aName || this._name; + }, + + // nsIPresentationDevice + get id() { return this._id; }, + + get name() { return this._name; }, + + get type() { return "chromecast"; }, + + establishControlChannel: function CRDD_establishControlChannel() { + this._ctrlChannel = new LocalControlChannel(this._provider, + this._id, + this._role); + + if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) { + // Only connect to Chromecast for controller. + // Monitor the receiver being ready. + Services.obs.addObserver(this, TOPIC_PRESENTATION_VIEW_READY, true); + + // Launch Chromecast service in Android. + Messaging.sendRequestForResult({ + type: TOPIC_ANDROID_CAST_DEVICE_START, + id: this.id + }).then(result => { + log("Chromecast is connected."); + }).catch(error => { + log("Can not connect to Chromecast."); + // If Chromecast can not be launched, remove the observer. + Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY); + this._ctrlChannel.disconnect(Cr.NS_ERROR_FAILURE); + }); + } else { + // If establishControlChannel called from the receiver, we don't need to + // wait the 'presentation-view-ready' event. + this._ctrlChannel.notifyConnected(); + } + + return this._ctrlChannel; + }, + + disconnect: function CRDD_disconnect() { + // Disconnect from Chromecast. + Messaging.sendRequestForResult({ + type: TOPIC_ANDROID_CAST_DEVICE_STOP, + id: this.id + }); + }, + + isRequestedUrlSupported: function CRDD_isRequestedUrlSupported(aUrl) { + let url = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(aUrl, null, null); + return url.scheme == "http" || url.scheme == "https"; + }, + + // nsIPresentationLocalDevice + get windowId() { return this._id; }, + + // nsIObserver + observe: function CRDD_observe(aSubject, aTopic, aData) { + if (aTopic == TOPIC_PRESENTATION_VIEW_READY) { + log("ChromecastRemoteDisplayDevice - observe: aTopic=" + + aTopic + " data=" + aData); + if (this.windowId === aData) { + Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY); + this._ctrlChannel.notifyConnected(); + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice, + Ci.nsIPresentationLocalDevice, + Ci.nsISupportsWeakReference, + Ci.nsIObserver]), +}; + +function AndroidCastDeviceProvider() { +} + +AndroidCastDeviceProvider.prototype = { + _listener: null, + _deviceList: new Map(), + + onSessionRequest: function APDP_onSessionRequest(aDeviceId, + aUrl, + aPresentationId, + aControlChannel) { + log("AndroidCastDeviceProvider - onSessionRequest" + + " aDeviceId=" + aDeviceId); + let device = this._deviceList.get(aDeviceId); + let receiverDevice = new ChromecastRemoteDisplayDevice(this, + device.id, + device.name, + Ci.nsIPresentationService.ROLE_RECEIVER); + this._listener.onSessionRequest(receiverDevice, + aUrl, + aPresentationId, + aControlChannel); + }, + + onTerminateRequest: function APDP_onTerminateRequest(aDeviceId, + aPresentationId, + aControlChannel, + aIsFromReceiver) { + log("AndroidCastDeviceProvider - onTerminateRequest" + + " aDeviceId=" + aDeviceId + + " aPresentationId=" + aPresentationId + + " aIsFromReceiver=" + aIsFromReceiver); + let device = this._deviceList.get(aDeviceId); + this._listener.onTerminateRequest(device, + aPresentationId, + aControlChannel, + aIsFromReceiver); + }, + + // nsIPresentationDeviceProvider + set listener(aListener) { + this._listener = aListener; + + // When unload this provider. + if (!this._listener) { + // remove observer + Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED); + Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED); + return; + } + + // Sync all device already found by Android. + Services.obs.notifyObservers(null, TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE, ""); + // Observer registration + Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED, false); + Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED, false); + }, + + get listener() { + return this._listener; + }, + + forceDiscovery: function APDP_forceDiscovery() { + // There is no API to do force discovery in Android SDK. + }, + + // nsIObserver + observe: function APDP_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_ANDROID_CAST_DEVICE_ADDED: { + let deviceInfo = JSON.parse(aData); + let deviceId = deviceInfo.uuid; + + if (!this._deviceList.has(deviceId)) { + let device = new ChromecastRemoteDisplayDevice(this, + deviceInfo.uuid, + deviceInfo.friendlyName, + Ci.nsIPresentationService.ROLE_CONTROLLER); + this._deviceList.set(device.id, device); + this._listener.addDevice(device); + } else { + let device = this._deviceList.get(deviceId); + device.update(deviceInfo.friendlyName); + this._listener.updateDevice(device); + } + break; + } + case TOPIC_ANDROID_CAST_DEVICE_REMOVED: { + let deviceId = aData; + let device = this._deviceList.get(deviceId); + this._listener.removeDevice(device); + this._deviceList.delete(deviceId); + break; + } + } + }, + + classID: Components.ID("{7394f24c-dbc3-48c8-8a47-cd10169b7c6b}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIPresentationDeviceProvider]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AndroidCastDeviceProvider]); diff --git a/dom/presentation/provider/AndroidCastDeviceProvider.manifest b/dom/presentation/provider/AndroidCastDeviceProvider.manifest new file mode 100644 index 000000000..db2aa101b --- /dev/null +++ b/dom/presentation/provider/AndroidCastDeviceProvider.manifest @@ -0,0 +1,4 @@ +# AndroidCastDeviceProvider.js +component {7394f24c-dbc3-48c8-8a47-cd10169b7c6b} AndroidCastDeviceProvider.js +contract @mozilla.org/presentation-device/android-cast-device-provider;1 {7394f24c-dbc3-48c8-8a47-cd10169b7c6b} +category presentation-device-provider AndroidCastDeviceProvider @mozilla.org/presentation-device/android-cast-device-provider;1 diff --git a/dom/presentation/provider/BuiltinProviders.manifest b/dom/presentation/provider/BuiltinProviders.manifest new file mode 100644 index 000000000..0ba7bcaa7 --- /dev/null +++ b/dom/presentation/provider/BuiltinProviders.manifest @@ -0,0 +1,2 @@ +component {f4079b8b-ede5-4b90-a112-5b415a931deb} PresentationControlService.js +contract @mozilla.org/presentation/control-service;1 {f4079b8b-ede5-4b90-a112-5b415a931deb} diff --git a/dom/presentation/provider/ControllerStateMachine.jsm b/dom/presentation/provider/ControllerStateMachine.jsm new file mode 100644 index 000000000..b568a8e9a --- /dev/null +++ b/dom/presentation/provider/ControllerStateMachine.jsm @@ -0,0 +1,240 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ +/* globals Components, dump */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ControllerStateMachine"]; // jshint ignore:line + +const { utils: Cu } = Components; + +/* globals State, CommandType */ +Cu.import("resource://gre/modules/presentation/StateMachineHelper.jsm"); + +const DEBUG = false; +function debug(str) { + dump("-*- ControllerStateMachine: " + str + "\n"); +} + +var handlers = [ + function _initHandler(stateMachine, command) { + // shouldn't receive any command at init state. + DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line + }, + function _connectingHandler(stateMachine, command) { + switch (command.type) { + case CommandType.CONNECT_ACK: + stateMachine.state = State.CONNECTED; + stateMachine._notifyDeviceConnected(); + break; + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command. + break; + } + }, + function _connectedHandler(stateMachine, command) { + switch (command.type) { + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + case CommandType.LAUNCH_ACK: + stateMachine._notifyLaunch(command.presentationId); + break; + case CommandType.TERMINATE: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.TERMINATE_ACK: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.ANSWER: + case CommandType.ICE_CANDIDATE: + stateMachine._notifyChannelDescriptor(command); + break; + case CommandType.RECONNECT_ACK: + stateMachine._notifyReconnect(command.presentationId); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command. + break; + } + }, + function _closingHandler(stateMachine, command) { + switch (command.type) { + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command. + break; + } + }, + function _closedHandler(stateMachine, command) { + // ignore every command in closed state. + DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line + }, +]; + +function ControllerStateMachine(channel, deviceId) { + this.state = State.INIT; + this._channel = channel; + this._deviceId = deviceId; +} + +ControllerStateMachine.prototype = { + launch: function _launch(presentationId, url) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.LAUNCH, + presentationId: presentationId, + url: url, + }); + } + }, + + terminate: function _terminate(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE, + presentationId: presentationId, + }); + } + }, + + terminateAck: function _terminateAck(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE_ACK, + presentationId: presentationId, + }); + } + }, + + reconnect: function _reconnect(presentationId, url) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.RECONNECT, + presentationId: presentationId, + url: url, + }); + } + }, + + sendOffer: function _sendOffer(offer) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.OFFER, + offer: offer, + }); + } + }, + + sendAnswer: function _sendAnswer() { + // answer can only be sent by presenting UA. + debug("controller shouldn't generate answer"); + }, + + updateIceCandidate: function _updateIceCandidate(candidate) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.ICE_CANDIDATE, + candidate: candidate, + }); + } + }, + + onCommand: function _onCommand(command) { + handlers[this.state](this, command); + }, + + onChannelReady: function _onChannelReady() { + if (this.state === State.INIT) { + this._sendCommand({ + type: CommandType.CONNECT, + deviceId: this._deviceId + }); + this.state = State.CONNECTING; + } + }, + + onChannelClosed: function _onChannelClose(reason, isByRemote) { + switch (this.state) { + case State.CONNECTED: + if (isByRemote) { + this.state = State.CLOSED; + this._notifyDisconnected(reason); + } else { + this._sendCommand({ + type: CommandType.DISCONNECT, + reason: reason + }); + this.state = State.CLOSING; + this._closeReason = reason; + } + break; + case State.CLOSING: + if (isByRemote) { + this.state = State.CLOSED; + if (this._closeReason) { + reason = this._closeReason; + delete this._closeReason; + } + this._notifyDisconnected(reason); + } + break; + default: + DEBUG && debug("unexpected channel close: " + reason + ", " + isByRemote); // jshint ignore:line + break; + } + }, + + _sendCommand: function _sendCommand(command) { + this._channel.sendCommand(command); + }, + + _notifyDeviceConnected: function _notifyDeviceConnected() { + //XXX trigger following command + this._channel.notifyDeviceConnected(); + }, + + _notifyDisconnected: function _notifyDisconnected(reason) { + this._channel.notifyDisconnected(reason); + }, + + _notifyLaunch: function _notifyLaunch(presentationId) { + this._channel.notifyLaunch(presentationId); + }, + + _notifyTerminate: function _notifyTerminate(presentationId) { + this._channel.notifyTerminate(presentationId); + }, + + _notifyReconnect: function _notifyReconnect(presentationId) { + this._channel.notifyReconnect(presentationId); + }, + + _notifyChannelDescriptor: function _notifyChannelDescriptor(command) { + switch (command.type) { + case CommandType.ANSWER: + this._channel.notifyAnswer(command.answer); + break; + case CommandType.ICE_CANDIDATE: + this._channel.notifyIceCandidate(command.candidate); + break; + } + }, +}; + +this.ControllerStateMachine = ControllerStateMachine; // jshint ignore:line diff --git a/dom/presentation/provider/DeviceProviderHelpers.cpp b/dom/presentation/provider/DeviceProviderHelpers.cpp new file mode 100644 index 000000000..00b2c12f1 --- /dev/null +++ b/dom/presentation/provider/DeviceProviderHelpers.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "DeviceProviderHelpers.h" + +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace dom { +namespace presentation { + +static const char* const kFxTVPresentationAppUrls[] = { + "app://fling-player.gaiamobile.org/index.html", + "app://notification-receiver.gaiamobile.org/index.html", + nullptr +}; + +/* static */ bool +DeviceProviderHelpers::IsCommonlySupportedScheme(const nsAString& aUrl) +{ + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl); + if (NS_FAILED(rv) || !uri) { + return false; + } + + nsAutoCString scheme; + uri->GetScheme(scheme); + if (scheme.LowerCaseEqualsLiteral("http") || + scheme.LowerCaseEqualsLiteral("https")) { + return true; + } + + return false; +} + +/* static */ bool +DeviceProviderHelpers::IsFxTVSupportedAppUrl(const nsAString& aUrl) +{ + // Check if matched with any presentation Apps on TV. + for (uint32_t i = 0; kFxTVPresentationAppUrls[i]; i++) { + if (aUrl.EqualsASCII(kFxTVPresentationAppUrls[i])) { + return true; + } + } + + return false; +} + +} // namespace presentation +} // namespace dom +} // namespace mozilla diff --git a/dom/presentation/provider/DeviceProviderHelpers.h b/dom/presentation/provider/DeviceProviderHelpers.h new file mode 100644 index 000000000..4bde09bed --- /dev/null +++ b/dom/presentation/provider/DeviceProviderHelpers.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_presentation_DeviceProviderHelpers_h +#define mozilla_dom_presentation_DeviceProviderHelpers_h + +#include "nsString.h" + +namespace mozilla { +namespace dom { +namespace presentation { + +class DeviceProviderHelpers final +{ +public: + static bool IsCommonlySupportedScheme(const nsAString& aUrl); + static bool IsFxTVSupportedAppUrl(const nsAString& aUrl); + +private: + DeviceProviderHelpers() = delete; +}; + +} // namespace presentation +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_presentation_DeviceProviderHelpers_h diff --git a/dom/presentation/provider/DisplayDeviceProvider.cpp b/dom/presentation/provider/DisplayDeviceProvider.cpp new file mode 100644 index 000000000..3f88aba5e --- /dev/null +++ b/dom/presentation/provider/DisplayDeviceProvider.cpp @@ -0,0 +1,580 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "DisplayDeviceProvider.h" + +#include "DeviceProviderHelpers.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "nsIObserverService.h" +#include "nsIServiceManager.h" +#include "nsIWindowWatcher.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsSimpleURI.h" +#include "nsTCPDeviceInfo.h" +#include "nsThreadUtils.h" + +static mozilla::LazyLogModule gDisplayDeviceProviderLog("DisplayDeviceProvider"); + +#define LOG(format) MOZ_LOG(gDisplayDeviceProviderLog, mozilla::LogLevel::Debug, format) + +#define DISPLAY_CHANGED_NOTIFICATION "display-changed" +#define DEFAULT_CHROME_FEATURES_PREF "toolkit.defaultChromeFeatures" +#define CHROME_REMOTE_URL_PREF "b2g.multiscreen.chrome_remote_url" +#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS "dom.presentation.discoverable.retry_ms" + +namespace mozilla { +namespace dom { +namespace presentation { + +/** + * This wrapper is used to break circular-reference problem. + */ +class DisplayDeviceProviderWrappedListener final + : public nsIPresentationControlServerListener +{ +public: + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener) + + explicit DisplayDeviceProviderWrappedListener() = default; + + nsresult SetListener(DisplayDeviceProvider* aListener) + { + mListener = aListener; + return NS_OK; + } + +private: + virtual ~DisplayDeviceProviderWrappedListener() = default; + + DisplayDeviceProvider* mListener = nullptr; +}; + +NS_IMPL_ISUPPORTS(DisplayDeviceProviderWrappedListener, + nsIPresentationControlServerListener) + +NS_IMPL_ISUPPORTS(DisplayDeviceProvider::HDMIDisplayDevice, + nsIPresentationDevice, + nsIPresentationLocalDevice) + +// nsIPresentationDevice +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::GetId(nsACString& aId) +{ + aId = mWindowId; + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::GetName(nsACString& aName) +{ + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::GetType(nsACString& aType) +{ + aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::GetWindowId(nsACString& aWindowId) +{ + aWindowId = mWindowId; + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice + ::EstablishControlChannel(nsIPresentationControlChannel** aControlChannel) +{ + nsresult rv = OpenTopLevelWindow(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<DisplayDeviceProvider> provider = mProvider.get(); + if (NS_WARN_IF(!provider)) { + return NS_ERROR_FAILURE; + } + return provider->Connect(this, aControlChannel); +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::Disconnect() +{ + nsresult rv = CloseTopLevelWindow(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK;; +} + +NS_IMETHODIMP +DisplayDeviceProvider::HDMIDisplayDevice::IsRequestedUrlSupported( + const nsAString& aRequestedUrl, + bool* aRetVal) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!aRetVal) { + return NS_ERROR_INVALID_POINTER; + } + + // 1-UA device only supports HTTP/HTTPS hosted receiver page. + *aRetVal = DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl); + + return NS_OK; +} + +nsresult +DisplayDeviceProvider::HDMIDisplayDevice::OpenTopLevelWindow() +{ + MOZ_ASSERT(!mWindow); + + nsresult rv; + nsAutoCString flags(Preferences::GetCString(DEFAULT_CHROME_FEATURES_PREF)); + if (flags.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + flags.AppendLiteral(",mozDisplayId="); + flags.AppendInt(mScreenId); + + nsAutoCString remoteShellURLString(Preferences::GetCString(CHROME_REMOTE_URL_PREF)); + remoteShellURLString.AppendLiteral("#"); + remoteShellURLString.Append(mWindowId); + + // URI validation + nsCOMPtr<nsIURI> remoteShellURL; + rv = NS_NewURI(getter_AddRefs(remoteShellURL), remoteShellURLString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = remoteShellURL->GetSpec(remoteShellURLString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID); + MOZ_ASSERT(ww); + + rv = ww->OpenWindow(nullptr, + remoteShellURLString.get(), + "_blank", + flags.get(), + nullptr, + getter_AddRefs(mWindow)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DisplayDeviceProvider::HDMIDisplayDevice::CloseTopLevelWindow() +{ + MOZ_ASSERT(mWindow); + + nsCOMPtr<nsPIDOMWindowOuter> piWindow = nsPIDOMWindowOuter::From(mWindow); + nsresult rv = piWindow->Close(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(DisplayDeviceProvider, + nsIObserver, + nsIPresentationDeviceProvider, + nsIPresentationControlServerListener) + +DisplayDeviceProvider::~DisplayDeviceProvider() +{ + Uninit(); +} + +nsresult +DisplayDeviceProvider::Init() +{ + // Provider must be initialized only once. + if (mInitialized) { + return NS_OK; + } + + nsresult rv; + + mServerRetryMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS); + mServerRetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + MOZ_ASSERT(obs); + + obs->AddObserver(this, DISPLAY_CHANGED_NOTIFICATION, false); + + mDevice = new HDMIDisplayDevice(this); + + mWrappedListener = new DisplayDeviceProviderWrappedListener(); + rv = mWrappedListener->SetListener(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPresentationService = do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID, + &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StartTCPService(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInitialized = true; + return NS_OK; +} + +nsresult +DisplayDeviceProvider::Uninit() +{ + // Provider must be deleted only once. + if (!mInitialized) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, DISPLAY_CHANGED_NOTIFICATION); + } + + // Remove device from device manager when the provider is uninit + RemoveExternalScreen(); + + AbortServerRetry(); + + mInitialized = false; + mWrappedListener->SetListener(nullptr); + return NS_OK; +} + +nsresult +DisplayDeviceProvider::StartTCPService() +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + rv = mPresentationService->SetId(NS_LITERAL_CSTRING("DisplayDeviceProvider")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint16_t servicePort; + rv = mPresentationService->GetPort(&servicePort); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + /* + * If |servicePort| is non-zero, it means PresentationServer is running. + * Otherwise, we should make it start serving. + */ + if (servicePort) { + mPort = servicePort; + return NS_OK; + } + + rv = mPresentationService->SetListener(mWrappedListener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AbortServerRetry(); + + // 1-UA doesn't need encryption. + rv = mPresentationService->StartServer(/* aEncrypted = */ false, + /* aPort = */ 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +DisplayDeviceProvider::AbortServerRetry() +{ + if (mIsServerRetrying) { + mIsServerRetrying = false; + mServerRetryTimer->Cancel(); + } +} + +nsresult +DisplayDeviceProvider::AddExternalScreen() +{ + MOZ_ASSERT(mDeviceListener); + + nsresult rv; + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = listener->AddDevice(mDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DisplayDeviceProvider::RemoveExternalScreen() +{ + MOZ_ASSERT(mDeviceListener); + + nsresult rv; + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = listener->RemoveDevice(mDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mDevice->Disconnect(); + return NS_OK; +} + +// nsIPresentationDeviceProvider +NS_IMETHODIMP +DisplayDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener) +{ + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_POINTER; + } + + nsresult rv; + nsCOMPtr<nsIPresentationDeviceListener> listener = + do_QueryReferent(mDeviceListener, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + listener.forget(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener) +{ + mDeviceListener = do_GetWeakReference(aListener); + nsresult rv = mDeviceListener ? Init() : Uninit(); + if(NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::ForceDiscovery() +{ + return NS_OK; +} + +// nsIPresentationControlServerListener +NS_IMETHODIMP +DisplayDeviceProvider::OnServerReady(uint16_t aPort, + const nsACString& aCertFingerprint) +{ + MOZ_ASSERT(NS_IsMainThread()); + mPort = aPort; + + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::OnServerStopped(nsresult aResult) +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Try restart server if it is stopped abnormally. + if (NS_FAILED(aResult)) { + mIsServerRetrying = true; + mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT); + } + + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::OnSessionRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aUrl, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDeviceInfo); + MOZ_ASSERT(aControlChannel); + + nsresult rv; + + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!listener); + + rv = listener->OnSessionRequest(mDevice, + aUrl, + aPresentationId, + aControlChannel); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDeviceInfo); + MOZ_ASSERT(aControlChannel); + + nsresult rv; + + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!listener); + + rv = listener->OnTerminateRequest(mDevice, + aPresentationId, + aControlChannel, + aIsFromReceiver); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +DisplayDeviceProvider::OnReconnectRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aUrl, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDeviceInfo); + MOZ_ASSERT(aControlChannel); + + nsresult rv; + + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!listener); + + rv = listener->OnReconnectRequest(mDevice, + aUrl, + aPresentationId, + aControlChannel); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// nsIObserver +NS_IMETHODIMP +DisplayDeviceProvider::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) { + nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject); + MOZ_ASSERT(displayInfo); + + int32_t type; + bool isConnected; + displayInfo->GetConnected(&isConnected); + // XXX The ID is as same as the type of display. + // See Bug 1138287 and nsScreenManagerGonk::AddScreen() for more detail. + displayInfo->GetId(&type); + + if (type == DisplayType::DISPLAY_EXTERNAL) { + nsresult rv = isConnected ? AddExternalScreen() : RemoveExternalScreen(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject); + if (!timer) { + return NS_ERROR_UNEXPECTED; + } + + if (timer == mServerRetryTimer) { + mIsServerRetrying = false; + StartTCPService(); + } + } + + return NS_OK; +} + +nsresult +DisplayDeviceProvider::Connect(HDMIDisplayDevice* aDevice, + nsIPresentationControlChannel** aControlChannel) +{ + MOZ_ASSERT(aDevice); + MOZ_ASSERT(mPresentationService); + NS_ENSURE_ARG_POINTER(aControlChannel); + *aControlChannel = nullptr; + + nsCOMPtr<nsITCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(), + aDevice->Address(), + mPort, + EmptyCString()); + + return mPresentationService->Connect(deviceInfo, aControlChannel); +} + +} // namespace presentation +} // namespace dom +} // namespace mozilla diff --git a/dom/presentation/provider/DisplayDeviceProvider.h b/dom/presentation/provider/DisplayDeviceProvider.h new file mode 100644 index 000000000..ebd5db394 --- /dev/null +++ b/dom/presentation/provider/DisplayDeviceProvider.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_presentation_provider_DisplayDeviceProvider_h +#define mozilla_dom_presentation_provider_DisplayDeviceProvider_h + +#include "mozilla/RefPtr.h" +#include "mozilla/WeakPtr.h" +#include "nsCOMPtr.h" +#include "nsIDOMWindow.h" +#include "nsIDisplayInfo.h" +#include "nsIObserver.h" +#include "nsIPresentationDeviceProvider.h" +#include "nsIPresentationLocalDevice.h" +#include "nsIPresentationControlService.h" +#include "nsITimer.h" +#include "nsIWindowWatcher.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace dom { +namespace presentation { + +// Consistent definition with the definition in +// widget/gonk/libdisplay/GonkDisplay.h. +enum DisplayType { + DISPLAY_PRIMARY, + DISPLAY_EXTERNAL, + DISPLAY_VIRTUAL, + NUM_DISPLAY_TYPES +}; + +class DisplayDeviceProviderWrappedListener; + +class DisplayDeviceProvider final : public nsIObserver + , public nsIPresentationDeviceProvider + , public nsIPresentationControlServerListener + , public SupportsWeakPtr<DisplayDeviceProvider> +{ +private: + class HDMIDisplayDevice final : public nsIPresentationLocalDevice + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONDEVICE + NS_DECL_NSIPRESENTATIONLOCALDEVICE + + // mScreenId is as same as the definition of display type. + explicit HDMIDisplayDevice(DisplayDeviceProvider* aProvider) + : mScreenId(DisplayType::DISPLAY_EXTERNAL) + , mName("HDMI") + , mType("external") + , mWindowId("hdmi") + , mAddress("127.0.0.1") + , mProvider(aProvider) + {} + + nsresult OpenTopLevelWindow(); + nsresult CloseTopLevelWindow(); + + const nsCString& Id() const { return mWindowId; } + const nsCString& Address() const { return mAddress; } + + private: + virtual ~HDMIDisplayDevice() = default; + + // Due to the limitation of nsWinodw, mScreenId must be an integer. + // And mScreenId is also align to the display type defined in + // widget/gonk/libdisplay/GonkDisplay.h. + // HDMI display is DisplayType::DISPLAY_EXTERNAL. + uint32_t mScreenId; + nsCString mName; + nsCString mType; + nsCString mWindowId; + nsCString mAddress; + + nsCOMPtr<mozIDOMWindowProxy> mWindow; + // weak pointer + // Provider hold a strong pointer to the device. Use weak pointer to prevent + // the reference cycle. + WeakPtr<DisplayDeviceProvider> mProvider; + }; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIPRESENTATIONDEVICEPROVIDER + NS_DECL_NSIPRESENTATIONCONTROLSERVERLISTENER + // For using WeakPtr when MOZ_REFCOUNTED_LEAK_CHECKING defined + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(DisplayDeviceProvider) + + nsresult Connect(HDMIDisplayDevice* aDevice, + nsIPresentationControlChannel** aControlChannel); +private: + virtual ~DisplayDeviceProvider(); + + nsresult Init(); + nsresult Uninit(); + + nsresult AddExternalScreen(); + nsresult RemoveExternalScreen(); + + nsresult StartTCPService(); + + void AbortServerRetry(); + + // Now support HDMI display only and there should be only one HDMI display. + nsCOMPtr<nsIPresentationLocalDevice> mDevice = nullptr; + // weak pointer + // PresentationDeviceManager (mDeviceListener) hold strong pointer to + // DisplayDeviceProvider. Use nsWeakPtr to avoid reference cycle. + nsWeakPtr mDeviceListener = nullptr; + nsCOMPtr<nsIPresentationControlService> mPresentationService; + // Used to prevent reference cycle between DisplayDeviceProvider and + // TCPPresentationServer. + RefPtr<DisplayDeviceProviderWrappedListener> mWrappedListener; + + bool mInitialized = false; + uint16_t mPort; + + bool mIsServerRetrying = false; + uint32_t mServerRetryMs; + nsCOMPtr<nsITimer> mServerRetryTimer; +}; + +} // mozilla +} // dom +} // presentation + +#endif // mozilla_dom_presentation_provider_DisplayDeviceProvider_h + diff --git a/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp b/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp new file mode 100644 index 000000000..54849c9e3 --- /dev/null +++ b/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp @@ -0,0 +1,774 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "LegacyMDNSDeviceProvider.h" + +#include "DeviceProviderHelpers.h" +#include "MainThreadUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "nsIWritablePropertyBag2.h" +#include "nsServiceManagerUtils.h" +#include "nsTCPDeviceInfo.h" +#include "nsThreadUtils.h" +#include "nsIPropertyBag2.h" + +#define PREF_PRESENTATION_DISCOVERY_LEGACY "dom.presentation.discovery.legacy.enabled" +#define PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS "dom.presentation.discovery.timeout_ms" +#define PREF_PRESENTATION_DEVICE_NAME "dom.presentation.device.name" + +#define LEGACY_SERVICE_TYPE "_mozilla_papi._tcp" + +#define LEGACY_PRESENTATION_CONTROL_SERVICE_CONTACT_ID "@mozilla.org/presentation/legacy-control-service;1" + +static mozilla::LazyLogModule sLegacyMDNSProviderLogModule("LegacyMDNSDeviceProvider"); + +#undef LOG_I +#define LOG_I(...) MOZ_LOG(sLegacyMDNSProviderLogModule, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#undef LOG_E +#define LOG_E(...) MOZ_LOG(sLegacyMDNSProviderLogModule, mozilla::LogLevel::Error, (__VA_ARGS__)) + +namespace mozilla { +namespace dom { +namespace presentation { +namespace legacy { + +static const char* kObservedPrefs[] = { + PREF_PRESENTATION_DISCOVERY_LEGACY, + PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS, + PREF_PRESENTATION_DEVICE_NAME, + nullptr +}; + +namespace { + +static void +GetAndroidDeviceName(nsACString& aRetVal) +{ + nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1"); + MOZ_ASSERT(infoService, "Could not find a system info service"); + + Unused << NS_WARN_IF(NS_FAILED(infoService->GetPropertyAsACString( + NS_LITERAL_STRING("device"), aRetVal))); +} + +} //anonymous namespace + +/** + * This wrapper is used to break circular-reference problem. + */ +class DNSServiceWrappedListener final + : public nsIDNSServiceDiscoveryListener + , public nsIDNSServiceResolveListener +{ +public: + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIDNSSERVICEDISCOVERYLISTENER(mListener) + NS_FORWARD_SAFE_NSIDNSSERVICERESOLVELISTENER(mListener) + + explicit DNSServiceWrappedListener() = default; + + nsresult SetListener(LegacyMDNSDeviceProvider* aListener) + { + mListener = aListener; + return NS_OK; + } + +private: + virtual ~DNSServiceWrappedListener() = default; + + LegacyMDNSDeviceProvider* mListener = nullptr; +}; + +NS_IMPL_ISUPPORTS(DNSServiceWrappedListener, + nsIDNSServiceDiscoveryListener, + nsIDNSServiceResolveListener) + +NS_IMPL_ISUPPORTS(LegacyMDNSDeviceProvider, + nsIPresentationDeviceProvider, + nsIDNSServiceDiscoveryListener, + nsIDNSServiceResolveListener, + nsIObserver) + +LegacyMDNSDeviceProvider::~LegacyMDNSDeviceProvider() +{ + Uninit(); +} + +nsresult +LegacyMDNSDeviceProvider::Init() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitialized) { + return NS_OK; + } + + nsresult rv; + + mMulticastDNS = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mWrappedListener = new DNSServiceWrappedListener(); + if (NS_WARN_IF(NS_FAILED(rv = mWrappedListener->SetListener(this)))) { + return rv; + } + + mPresentationService = do_CreateInstance(LEGACY_PRESENTATION_CONTROL_SERVICE_CONTACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mDiscoveryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Preferences::AddStrongObservers(this, kObservedPrefs); + + mDiscoveryEnabled = Preferences::GetBool(PREF_PRESENTATION_DISCOVERY_LEGACY); + mDiscoveryTimeoutMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS); + mServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME); + + // FIXME: Bug 1185806 - Provide a common device name setting. + if (mServiceName.IsEmpty()) { + GetAndroidDeviceName(mServiceName); + Unused << Preferences::SetCString(PREF_PRESENTATION_DEVICE_NAME, mServiceName); + } + + Unused << mPresentationService->SetId(mServiceName); + + if (mDiscoveryEnabled && NS_WARN_IF(NS_FAILED(rv = ForceDiscovery()))) { + return rv; + } + + mInitialized = true; + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::Uninit() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mInitialized) { + return NS_OK; + } + + ClearDevices(); + + Preferences::RemoveObservers(this, kObservedPrefs); + + StopDiscovery(NS_OK); + + mMulticastDNS = nullptr; + + if (mWrappedListener) { + mWrappedListener->SetListener(nullptr); + mWrappedListener = nullptr; + } + + mInitialized = false; + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::StopDiscovery(nsresult aReason) +{ + LOG_I("StopDiscovery (0x%08x)", aReason); + + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mDiscoveryTimer); + + Unused << mDiscoveryTimer->Cancel(); + + if (mDiscoveryRequest) { + mDiscoveryRequest->Cancel(aReason); + mDiscoveryRequest = nullptr; + } + + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::Connect(Device* aDevice, + nsIPresentationControlChannel** aRetVal) +{ + MOZ_ASSERT(aDevice); + MOZ_ASSERT(mPresentationService); + + RefPtr<TCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(), + aDevice->Address(), + aDevice->Port(), + EmptyCString()); + + return mPresentationService->Connect(deviceInfo, aRetVal); +} + +nsresult +LegacyMDNSDeviceProvider::AddDevice(const nsACString& aId, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + RefPtr<Device> device = new Device(aId, /* ID */ + aServiceName, + aServiceType, + aAddress, + aPort, + DeviceState::eActive, + this); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->AddDevice(device); + } + + mDevices.AppendElement(device); + + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::UpdateDevice(const uint32_t aIndex, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + if (NS_WARN_IF(aIndex >= mDevices.Length())) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Device> device = mDevices[aIndex]; + device->Update(aServiceName, aServiceType, aAddress, aPort); + device->ChangeState(DeviceState::eActive); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->UpdateDevice(device); + } + + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::RemoveDevice(const uint32_t aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + if (NS_WARN_IF(aIndex >= mDevices.Length())) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Device> device = mDevices[aIndex]; + + LOG_I("RemoveDevice: %s", device->Id().get()); + mDevices.RemoveElementAt(aIndex); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->RemoveDevice(device); + } + + return NS_OK; +} + +bool +LegacyMDNSDeviceProvider::FindDeviceById(const nsACString& aId, + uint32_t& aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Device> device = new Device(aId, + /* aName = */ EmptyCString(), + /* aType = */ EmptyCString(), + /* aHost = */ EmptyCString(), + /* aPort = */ 0, + /* aState = */ DeviceState::eUnknown, + /* aProvider = */ nullptr); + size_t index = mDevices.IndexOf(device, 0, DeviceIdComparator()); + + if (index == mDevices.NoIndex) { + return false; + } + + aIndex = index; + return true; +} + +bool +LegacyMDNSDeviceProvider::FindDeviceByAddress(const nsACString& aAddress, + uint32_t& aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Device> device = new Device(/* aId = */ EmptyCString(), + /* aName = */ EmptyCString(), + /* aType = */ EmptyCString(), + aAddress, + /* aPort = */ 0, + /* aState = */ DeviceState::eUnknown, + /* aProvider = */ nullptr); + size_t index = mDevices.IndexOf(device, 0, DeviceAddressComparator()); + + if (index == mDevices.NoIndex) { + return false; + } + + aIndex = index; + return true; +} + +void +LegacyMDNSDeviceProvider::MarkAllDevicesUnknown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + for (auto& device : mDevices) { + device->ChangeState(DeviceState::eUnknown); + } +} + +void +LegacyMDNSDeviceProvider::ClearUnknownDevices() +{ + MOZ_ASSERT(NS_IsMainThread()); + + size_t i = mDevices.Length(); + while (i > 0) { + --i; + if (mDevices[i]->State() == DeviceState::eUnknown) { + Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i))); + } + } +} + +void +LegacyMDNSDeviceProvider::ClearDevices() +{ + MOZ_ASSERT(NS_IsMainThread()); + + size_t i = mDevices.Length(); + while (i > 0) { + --i; + Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i))); + } +} + +// nsIPresentationDeviceProvider +NS_IMETHODIMP +LegacyMDNSDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_POINTER; + } + + nsresult rv; + nsCOMPtr<nsIPresentationDeviceListener> listener = + do_QueryReferent(mDeviceListener, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + listener.forget(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener) +{ + MOZ_ASSERT(NS_IsMainThread()); + + mDeviceListener = do_GetWeakReference(aListener); + + nsresult rv; + if (mDeviceListener) { + if (NS_WARN_IF(NS_FAILED(rv = Init()))) { + return rv; + } + } else { + if (NS_WARN_IF(NS_FAILED(rv = Uninit()))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::ForceDiscovery() +{ + LOG_I("ForceDiscovery (%d)", mDiscoveryEnabled); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDiscoveryEnabled) { + return NS_OK; + } + + MOZ_ASSERT(mDiscoveryTimer); + MOZ_ASSERT(mMulticastDNS); + + // if it's already discovering, extend existing discovery timeout. + nsresult rv; + if (mIsDiscovering) { + Unused << mDiscoveryTimer->Cancel(); + + if (NS_WARN_IF(NS_FAILED( rv = mDiscoveryTimer->Init(this, + mDiscoveryTimeoutMs, + nsITimer::TYPE_ONE_SHOT)))) { + return rv; + } + return NS_OK; + } + + StopDiscovery(NS_OK); + + if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->StartDiscovery( + NS_LITERAL_CSTRING(LEGACY_SERVICE_TYPE), + mWrappedListener, + getter_AddRefs(mDiscoveryRequest))))) { + return rv; + } + + return NS_OK; +} + +// nsIDNSServiceDiscoveryListener +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnDiscoveryStarted(const nsACString& aServiceType) +{ + LOG_I("OnDiscoveryStarted"); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mDiscoveryTimer); + + MarkAllDevicesUnknown(); + + nsresult rv; + if (NS_WARN_IF(NS_FAILED(rv = mDiscoveryTimer->Init(this, + mDiscoveryTimeoutMs, + nsITimer::TYPE_ONE_SHOT)))) { + return rv; + } + + mIsDiscovering = true; + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnDiscoveryStopped(const nsACString& aServiceType) +{ + LOG_I("OnDiscoveryStopped"); + MOZ_ASSERT(NS_IsMainThread()); + + ClearUnknownDevices(); + + mIsDiscovering = false; + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnServiceFound(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv ; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceFound: %s", serviceName.get()); + + if (mMulticastDNS) { + if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService( + aServiceInfo, mWrappedListener)))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnServiceLost(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceLost: %s", serviceName.get()); + + nsAutoCString host; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) { + return rv; + } + + uint32_t index; + if (!FindDeviceById(host, index)) { + // given device was not found + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv = RemoveDevice(index)))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnStartDiscoveryFailed(const nsACString& aServiceType, + int32_t aErrorCode) +{ + LOG_E("OnStartDiscoveryFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnStopDiscoveryFailed(const nsACString& aServiceType, + int32_t aErrorCode) +{ + LOG_E("OnStopDiscoveryFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +// nsIDNSServiceResolveListener +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceResolved: %s", serviceName.get()); + + nsAutoCString host; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) { + return rv; + } + + nsAutoCString address; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetAddress(address)))) { + return rv; + } + + uint16_t port; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetPort(&port)))) { + return rv; + } + + nsAutoCString serviceType; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceType(serviceType)))) { + return rv; + } + + uint32_t index; + if (FindDeviceById(host, index)) { + return UpdateDevice(index, + serviceName, + serviceType, + address, + port); + } else { + return AddDevice(host, + serviceName, + serviceType, + address, + port); + } + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo, + int32_t aErrorCode) +{ + LOG_E("OnResolveFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +// nsIObserver +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + NS_ConvertUTF16toUTF8 data(aData); + LOG_I("Observe: topic = %s, data = %s", aTopic, data.get()); + + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_LEGACY)) { + OnDiscoveryChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERY_LEGACY)); + } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)) { + OnDiscoveryTimeoutChanged(Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)); + } else if (data.EqualsLiteral(PREF_PRESENTATION_DEVICE_NAME)) { + nsAdoptingCString newServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME); + if (!mServiceName.Equals(newServiceName)) { + OnServiceNameChanged(newServiceName); + } + } + } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + StopDiscovery(NS_OK); + } + + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::OnDiscoveryChanged(bool aEnabled) +{ + LOG_I("DiscoveryEnabled = %d\n", aEnabled); + MOZ_ASSERT(NS_IsMainThread()); + + mDiscoveryEnabled = aEnabled; + + if (mDiscoveryEnabled) { + return ForceDiscovery(); + } + + return StopDiscovery(NS_OK); +} + +nsresult +LegacyMDNSDeviceProvider::OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs) +{ + LOG_I("OnDiscoveryTimeoutChanged = %d\n", aTimeoutMs); + MOZ_ASSERT(NS_IsMainThread()); + + mDiscoveryTimeoutMs = aTimeoutMs; + + return NS_OK; +} + +nsresult +LegacyMDNSDeviceProvider::OnServiceNameChanged(const nsACString& aServiceName) +{ + LOG_I("serviceName = %s\n", PromiseFlatCString(aServiceName).get()); + MOZ_ASSERT(NS_IsMainThread()); + + mServiceName = aServiceName; + mPresentationService->SetId(mServiceName); + + return NS_OK; +} + +// LegacyMDNSDeviceProvider::Device +NS_IMPL_ISUPPORTS(LegacyMDNSDeviceProvider::Device, + nsIPresentationDevice) + +// nsIPresentationDevice +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::GetId(nsACString& aId) +{ + aId = mId; + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::GetName(nsACString& aName) +{ + aName = mName; + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::GetType(nsACString& aType) +{ + aType = mType; + + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::EstablishControlChannel( + nsIPresentationControlChannel** aRetVal) +{ + if (!mProvider) { + return NS_ERROR_FAILURE; + } + + return mProvider->Connect(this, aRetVal); +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::Disconnect() +{ + // No need to do anything when disconnect. + return NS_OK; +} + +NS_IMETHODIMP +LegacyMDNSDeviceProvider::Device::IsRequestedUrlSupported( + const nsAString& aRequestedUrl, + bool* aRetVal) +{ + if (!aRetVal) { + return NS_ERROR_INVALID_POINTER; + } + + // Legacy TV 2.5 device only support a fixed set of presentation Apps. + *aRetVal = DeviceProviderHelpers::IsFxTVSupportedAppUrl(aRequestedUrl); + + return NS_OK; +} + +} // namespace legacy +} // namespace presentation +} // namespace dom +} // namespace mozilla diff --git a/dom/presentation/provider/LegacyMDNSDeviceProvider.h b/dom/presentation/provider/LegacyMDNSDeviceProvider.h new file mode 100644 index 000000000..33ba877d3 --- /dev/null +++ b/dom/presentation/provider/LegacyMDNSDeviceProvider.h @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h +#define mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h + +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsICancelable.h" +#include "nsIDNSServiceDiscovery.h" +#include "nsIObserver.h" +#include "nsIPresentationDevice.h" +#include "nsIPresentationDeviceProvider.h" +#include "nsIPresentationControlService.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakPtr.h" + +namespace mozilla { +namespace dom { +namespace presentation { +namespace legacy { + +class DNSServiceWrappedListener; +class MulticastDNSService; + +class LegacyMDNSDeviceProvider final + : public nsIPresentationDeviceProvider + , public nsIDNSServiceDiscoveryListener + , public nsIDNSServiceResolveListener + , public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONDEVICEPROVIDER + NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER + NS_DECL_NSIDNSSERVICERESOLVELISTENER + NS_DECL_NSIOBSERVER + + explicit LegacyMDNSDeviceProvider() = default; + nsresult Init(); + nsresult Uninit(); + +private: + enum class DeviceState : uint32_t { + eUnknown, + eActive + }; + + class Device final : public nsIPresentationDevice + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONDEVICE + + explicit Device(const nsACString& aId, + const nsACString& aName, + const nsACString& aType, + const nsACString& aAddress, + const uint16_t aPort, + DeviceState aState, + LegacyMDNSDeviceProvider* aProvider) + : mId(aId) + , mName(aName) + , mType(aType) + , mAddress(aAddress) + , mPort(aPort) + , mState(aState) + , mProvider(aProvider) + { + } + + const nsCString& Id() const + { + return mId; + } + + const nsCString& Address() const + { + return mAddress; + } + + uint16_t Port() const + { + return mPort; + } + + DeviceState State() const + { + return mState; + } + + void ChangeState(DeviceState aState) + { + mState = aState; + } + + void Update(const nsACString& aName, + const nsACString& aType, + const nsACString& aAddress, + const uint16_t aPort) + { + mName = aName; + mType = aType; + mAddress = aAddress; + mPort = aPort; + } + + private: + virtual ~Device() = default; + + nsCString mId; + nsCString mName; + nsCString mType; + nsCString mAddress; + uint16_t mPort; + DeviceState mState; + LegacyMDNSDeviceProvider* mProvider; + }; + + struct DeviceIdComparator { + bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const { + return aA->Id() == aB->Id(); + } + }; + + struct DeviceAddressComparator { + bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const { + return aA->Address() == aB->Address(); + } + }; + + virtual ~LegacyMDNSDeviceProvider(); + nsresult StopDiscovery(nsresult aReason); + nsresult Connect(Device* aDevice, + nsIPresentationControlChannel** aRetVal); + + // device manipulation + nsresult AddDevice(const nsACString& aId, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort); + nsresult UpdateDevice(const uint32_t aIndex, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort); + nsresult RemoveDevice(const uint32_t aIndex); + bool FindDeviceById(const nsACString& aId, + uint32_t& aIndex); + + bool FindDeviceByAddress(const nsACString& aAddress, + uint32_t& aIndex); + + void MarkAllDevicesUnknown(); + void ClearUnknownDevices(); + void ClearDevices(); + + // preferences + nsresult OnDiscoveryChanged(bool aEnabled); + nsresult OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs); + nsresult OnServiceNameChanged(const nsACString& aServiceName); + + bool mInitialized = false; + nsWeakPtr mDeviceListener; + nsCOMPtr<nsIPresentationControlService> mPresentationService; + nsCOMPtr<nsIDNSServiceDiscovery> mMulticastDNS; + RefPtr<DNSServiceWrappedListener> mWrappedListener; + + nsCOMPtr<nsICancelable> mDiscoveryRequest; + + nsTArray<RefPtr<Device>> mDevices; + + bool mDiscoveryEnabled = false; + bool mIsDiscovering = false; + uint32_t mDiscoveryTimeoutMs; + nsCOMPtr<nsITimer> mDiscoveryTimer; + + nsCString mServiceName; +}; + +} // namespace legacy +} // namespace presentation +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h diff --git a/dom/presentation/provider/LegacyPresentationControlService.js b/dom/presentation/provider/LegacyPresentationControlService.js new file mode 100644 index 000000000..b27177b63 --- /dev/null +++ b/dom/presentation/provider/LegacyPresentationControlService.js @@ -0,0 +1,488 @@ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ +/* globals Components, dump */ +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +/* globals XPCOMUtils */ +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +/* globals Services */ +Cu.import("resource://gre/modules/Services.jsm"); +/* globals NetUtil */ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +const DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug"); +function log(aMsg) { + dump("-*- LegacyPresentationControlService.js: " + aMsg + "\n"); +} + +function LegacyPresentationControlService() { + DEBUG && log("LegacyPresentationControlService - ctor"); //jshint ignore:line + this._id = null; +} + +LegacyPresentationControlService.prototype = { + startServer: function() { + DEBUG && log("LegacyPresentationControlService - doesn't support receiver mode"); //jshint ignore:line + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + get id() { + return this._id; + }, + + set id(aId) { + this._id = aId; + }, + + get port() { + return 0; + }, + + get version() { + return 0; + }, + + set listener(aListener) { //jshint ignore:line + DEBUG && log("LegacyPresentationControlService - doesn't support receiver mode"); //jshint ignore:line + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + get listener() { + return null; + }, + + connect: function(aDeviceInfo) { + if (!this.id) { + DEBUG && log("LegacyPresentationControlService - Id has not initialized; requestSession fails"); //jshint ignore:line + return null; + } + DEBUG && log("LegacyPresentationControlService - requestSession to " + aDeviceInfo.id); //jshint ignore:line + + let sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + let socketTransport; + try { + socketTransport = sts.createTransport(null, + 0, + aDeviceInfo.address, + aDeviceInfo.port, + null); + } catch (e) { + DEBUG && log("LegacyPresentationControlService - createTransport throws: " + e); //jshint ignore:line + // Pop the exception to |TCPDevice.establishControlChannel| + throw Cr.NS_ERROR_FAILURE; + } + return new LegacyTCPControlChannel(this.id, + socketTransport, + aDeviceInfo); + }, + + close: function() { + DEBUG && log("LegacyPresentationControlService - close"); //jshint ignore:line + }, + + classID: Components.ID("{b21816fe-8aff-4811-86d2-85a7444c557e}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPresentationControlService]), +}; + +function ChannelDescription(aInit) { + this._type = aInit.type; + switch (this._type) { + case Ci.nsIPresentationChannelDescription.TYPE_TCP: + this._tcpAddresses = Cc["@mozilla.org/array;1"] + .createInstance(Ci.nsIMutableArray); + for (let address of aInit.tcpAddress) { + let wrapper = Cc["@mozilla.org/supports-cstring;1"] + .createInstance(Ci.nsISupportsCString); + wrapper.data = address; + this._tcpAddresses.appendElement(wrapper, false); + } + + this._tcpPort = aInit.tcpPort; + break; + case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL: + this._dataChannelSDP = aInit.dataChannelSDP; + break; + } +} + +ChannelDescription.prototype = { + _type: 0, + _tcpAddresses: null, + _tcpPort: 0, + _dataChannelSDP: "", + + get type() { + return this._type; + }, + + get tcpAddress() { + return this._tcpAddresses; + }, + + get tcpPort() { + return this._tcpPort; + }, + + get dataChannelSDP() { + return this._dataChannelSDP; + }, + + classID: Components.ID("{d69fc81c-4f40-47a3-97e6-b4cf5db2294e}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]), +}; + +// Helper function: transfer nsIPresentationChannelDescription to json +function discriptionAsJson(aDescription) { + let json = {}; + json.type = aDescription.type; + switch(aDescription.type) { + case Ci.nsIPresentationChannelDescription.TYPE_TCP: + let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray); + json.tcpAddress = []; + for (let idx = 0; idx < addresses.length; idx++) { + let address = addresses.queryElementAt(idx, Ci.nsISupportsCString); + json.tcpAddress.push(address.data); + } + json.tcpPort = aDescription.tcpPort; + break; + case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL: + json.dataChannelSDP = aDescription.dataChannelSDP; + break; + } + return json; +} + +function LegacyTCPControlChannel(id, + transport, + deviceInfo) { + DEBUG && log("create LegacyTCPControlChannel"); //jshint ignore:line + this._deviceInfo = deviceInfo; + this._transport = transport; + + this._id = id; + + let currentThread = Services.tm.currentThread; + transport.setEventSink(this, currentThread); + + this._input = this._transport.openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + this._input.asyncWait(this.QueryInterface(Ci.nsIStreamListener), + Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY, + 0, + currentThread); + + this._output = this._transport + .openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED, 0, 0); +} + +LegacyTCPControlChannel.prototype = { + _connected: false, + _pendingOpen: false, + _pendingAnswer: null, + _pendingClose: null, + _pendingCloseReason: null, + + _sendMessage: function(aJSONData, aOnThrow) { + if (!aOnThrow) { + aOnThrow = function(e) {throw e.result;}; + } + + if (!aJSONData) { + aOnThrow(); + return; + } + + if (!this._connected) { + DEBUG && log("LegacyTCPControlChannel - send" + aJSONData.type + " fails"); //jshint ignore:line + throw Cr.NS_ERROR_FAILURE; + } + + try { + this._send(aJSONData); + } catch (e) { + aOnThrow(e); + } + }, + + _sendInit: function() { + let msg = { + type: "requestSession:Init", + presentationId: this._presentationId, + url: this._url, + id: this._id, + }; + + this._sendMessage(msg, function(e) { + this.disconnect(); + this._notifyDisconnected(e.result); + }); + }, + + launch: function(aPresentationId, aUrl) { + this._presentationId = aPresentationId; + this._url = aUrl; + + this._sendInit(); + }, + + terminate: function() { + // Legacy protocol doesn't support extra terminate protocol. + // Trigger error handling for browser to shutdown all the resource locally. + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + sendOffer: function(aOffer) { + let msg = { + type: "requestSession:Offer", + presentationId: this._presentationId, + offer: discriptionAsJson(aOffer), + }; + this._sendMessage(msg); + }, + + sendAnswer: function(aAnswer) { //jshint ignore:line + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + sendIceCandidate: function(aCandidate) { + let msg = { + type: "requestSession:IceCandidate", + presentationId: this._presentationId, + iceCandidate: aCandidate, + }; + this._sendMessage(msg); + }, + // may throw an exception + _send: function(aMsg) { + DEBUG && log("LegacyTCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); //jshint ignore:line + + /** + * XXX In TCP streaming, it is possible that more than one message in one + * TCP packet. We use line delimited JSON to identify where one JSON encoded + * object ends and the next begins. Therefore, we do not allow newline + * characters whithin the whole message, and add a newline at the end. + * Please see the parser code in |onDataAvailable|. + */ + let message = JSON.stringify(aMsg).replace(["\n"], "") + "\n"; + try { + this._output.write(message, message.length); + } catch(e) { + DEBUG && log("LegacyTCPControlChannel - Failed to send message: " + e.name); //jshint ignore:line + throw e; + } + }, + + // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait) + // Only used for detecting connection refused + onInputStreamReady: function(aStream) { + try { + aStream.available(); + } catch (e) { + DEBUG && log("LegacyTCPControlChannel - onInputStreamReady error: " + e.name); //jshint ignore:line + // NS_ERROR_CONNECTION_REFUSED + this._listener.notifyDisconnected(e.result); + } + }, + + // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink) + onTransportStatus: function(aTransport, aStatus, aProg, aProgMax) { //jshint ignore:line + DEBUG && log("LegacyTCPControlChannel - onTransportStatus: " + + aStatus.toString(16)); //jshint ignore:line + if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + this._connected = true; + + if (!this._pump) { + this._createInputStreamPump(); + } + + this._notifyConnected(); + } + }, + + // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead) + onStartRequest: function() { + DEBUG && log("LegacyTCPControlChannel - onStartRequest"); //jshint ignore:line + }, + + // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead) + onStopRequest: function(aRequest, aContext, aStatus) { + DEBUG && log("LegacyTCPControlChannel - onStopRequest: " + aStatus); //jshint ignore:line + this.disconnect(aStatus); + this._notifyDisconnected(aStatus); + }, + + // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead) + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { //jshint ignore:line + let data = NetUtil.readInputStreamToString(aInputStream, + aInputStream.available()); + DEBUG && log("LegacyTCPControlChannel - onDataAvailable: " + data); //jshint ignore:line + + // Parser of line delimited JSON. Please see |_send| for more informaiton. + let jsonArray = data.split("\n"); + jsonArray.pop(); + for (let json of jsonArray) { + let msg; + try { + msg = JSON.parse(json); + } catch (e) { + DEBUG && log("LegacyTCPSignalingChannel - error in parsing json: " + e); //jshint ignore:line + } + + this._handleMessage(msg); + } + }, + + _createInputStreamPump: function() { + DEBUG && log("LegacyTCPControlChannel - create pump"); //jshint ignore:line + this._pump = Cc["@mozilla.org/network/input-stream-pump;1"]. + createInstance(Ci.nsIInputStreamPump); + this._pump.init(this._input, -1, -1, 0, 0, false); + this._pump.asyncRead(this, null); + }, + + // Handle command from remote side + _handleMessage: function(aMsg) { + DEBUG && log("LegacyTCPControlChannel - handleMessage from " + + JSON.stringify(this._deviceInfo) + ": " + JSON.stringify(aMsg)); //jshint ignore:line + switch (aMsg.type) { + case "requestSession:Answer": { + this._onAnswer(aMsg.answer); + break; + } + case "requestSession:IceCandidate": { + this._listener.onIceCandidate(aMsg.iceCandidate); + break; + } + case "requestSession:CloseReason": { + this._pendingCloseReason = aMsg.reason; + break; + } + } + }, + + get listener() { + return this._listener; + }, + + set listener(aListener) { + DEBUG && log("LegacyTCPControlChannel - set listener: " + aListener); //jshint ignore:line + if (!aListener) { + this._listener = null; + return; + } + + this._listener = aListener; + if (this._pendingOpen) { + this._pendingOpen = false; + DEBUG && log("LegacyTCPControlChannel - notify pending opened"); //jshint ignore:line + this._listener.notifyConnected(); + } + + if (this._pendingAnswer) { + let answer = this._pendingAnswer; + DEBUG && log("LegacyTCPControlChannel - notify pending answer: " + + JSON.stringify(answer)); // jshint ignore:line + this._listener.onAnswer(new ChannelDescription(answer)); + this._pendingAnswer = null; + } + + if (this._pendingClose) { + DEBUG && log("LegacyTCPControlChannel - notify pending closed"); //jshint ignore:line + this._notifyDisconnected(this._pendingCloseReason); + this._pendingClose = null; + } + }, + + /** + * These functions are designed to handle the interaction with listener + * appropriately. |_FUNC| is to handle |this._listener.FUNC|. + */ + _onAnswer: function(aAnswer) { + if (!this._connected) { + return; + } + if (!this._listener) { + this._pendingAnswer = aAnswer; + return; + } + DEBUG && log("LegacyTCPControlChannel - notify answer: " + JSON.stringify(aAnswer)); //jshint ignore:line + this._listener.onAnswer(new ChannelDescription(aAnswer)); + }, + + _notifyConnected: function() { + this._connected = true; + this._pendingClose = false; + this._pendingCloseReason = Cr.NS_OK; + + if (!this._listener) { + this._pendingOpen = true; + return; + } + + DEBUG && log("LegacyTCPControlChannel - notify opened"); //jshint ignore:line + this._listener.notifyConnected(); + }, + + _notifyDisconnected: function(aReason) { + this._connected = false; + this._pendingOpen = false; + this._pendingAnswer = null; + + // Remote endpoint closes the control channel with abnormal reason. + if (aReason == Cr.NS_OK && this._pendingCloseReason != Cr.NS_OK) { + aReason = this._pendingCloseReason; + } + + if (!this._listener) { + this._pendingClose = true; + this._pendingCloseReason = aReason; + return; + } + + DEBUG && log("LegacyTCPControlChannel - notify closed"); //jshint ignore:line + this._listener.notifyDisconnected(aReason); + }, + + disconnect: function(aReason) { + DEBUG && log("LegacyTCPControlChannel - close with reason: " + aReason); //jshint ignore:line + + if (this._connected) { + // default reason is NS_OK + if (typeof aReason !== "undefined" && aReason !== Cr.NS_OK) { + let msg = { + type: "requestSession:CloseReason", + presentationId: this._presentationId, + reason: aReason, + }; + this._sendMessage(msg); + this._pendingCloseReason = aReason; + } + + this._transport.setEventSink(null, null); + this._pump = null; + + this._input.close(); + this._output.close(); + + this._connected = false; + } + }, + + reconnect: function() { + // Legacy protocol doesn't support extra reconnect protocol. + // Trigger error handling for browser to shutdown all the resource locally. + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + classID: Components.ID("{4027ce3d-06e3-4d06-a235-df329cb0d411}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel, + Ci.nsIStreamListener]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LegacyPresentationControlService]); //jshint ignore:line diff --git a/dom/presentation/provider/LegacyProviders.manifest b/dom/presentation/provider/LegacyProviders.manifest new file mode 100644 index 000000000..9408da063 --- /dev/null +++ b/dom/presentation/provider/LegacyProviders.manifest @@ -0,0 +1,2 @@ +component {b21816fe-8aff-4811-86d2-85a7444c557e} LegacyPresentationControlService.js +contract @mozilla.org/presentation/legacy-control-service;1 {b21816fe-8aff-4811-86d2-85a7444c557e} diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp new file mode 100644 index 000000000..0cab915ac --- /dev/null +++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp @@ -0,0 +1,1249 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "MulticastDNSDeviceProvider.h" + +#include "DeviceProviderHelpers.h" +#include "MainThreadUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "nsIWritablePropertyBag2.h" +#include "nsServiceManagerUtils.h" +#include "nsTCPDeviceInfo.h" +#include "nsThreadUtils.h" + +#ifdef MOZ_WIDGET_ANDROID +#include "nsIPropertyBag2.h" +#endif // MOZ_WIDGET_ANDROID + +#define PREF_PRESENTATION_DISCOVERY "dom.presentation.discovery.enabled" +#define PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS "dom.presentation.discovery.timeout_ms" +#define PREF_PRESENTATION_DISCOVERABLE "dom.presentation.discoverable" +#define PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED "dom.presentation.discoverable.encrypted" +#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS "dom.presentation.discoverable.retry_ms" +#define PREF_PRESENTATION_DEVICE_NAME "dom.presentation.device.name" + +#define SERVICE_TYPE "_presentation-ctrl._tcp" +#define PROTOCOL_VERSION_TAG "version" +#define CERT_FINGERPRINT_TAG "certFingerprint" + +static mozilla::LazyLogModule sMulticastDNSProviderLogModule("MulticastDNSDeviceProvider"); + +#undef LOG_I +#define LOG_I(...) MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#undef LOG_E +#define LOG_E(...) MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Error, (__VA_ARGS__)) + +namespace mozilla { +namespace dom { +namespace presentation { + +static const char* kObservedPrefs[] = { + PREF_PRESENTATION_DISCOVERY, + PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS, + PREF_PRESENTATION_DISCOVERABLE, + PREF_PRESENTATION_DEVICE_NAME, + nullptr +}; + +namespace { + +#ifdef MOZ_WIDGET_ANDROID +static void +GetAndroidDeviceName(nsACString& aRetVal) +{ + nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1"); + MOZ_ASSERT(infoService, "Could not find a system info service"); + + Unused << NS_WARN_IF(NS_FAILED(infoService->GetPropertyAsACString( + NS_LITERAL_STRING("device"), aRetVal))); +} +#endif // MOZ_WIDGET_ANDROID + +} //anonymous namespace + +/** + * This wrapper is used to break circular-reference problem. + */ +class DNSServiceWrappedListener final + : public nsIDNSServiceDiscoveryListener + , public nsIDNSRegistrationListener + , public nsIDNSServiceResolveListener + , public nsIPresentationControlServerListener +{ +public: + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIDNSSERVICEDISCOVERYLISTENER(mListener) + NS_FORWARD_SAFE_NSIDNSREGISTRATIONLISTENER(mListener) + NS_FORWARD_SAFE_NSIDNSSERVICERESOLVELISTENER(mListener) + NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener) + + explicit DNSServiceWrappedListener() = default; + + nsresult SetListener(MulticastDNSDeviceProvider* aListener) + { + mListener = aListener; + return NS_OK; + } + +private: + virtual ~DNSServiceWrappedListener() = default; + + MulticastDNSDeviceProvider* mListener = nullptr; +}; + +NS_IMPL_ISUPPORTS(DNSServiceWrappedListener, + nsIDNSServiceDiscoveryListener, + nsIDNSRegistrationListener, + nsIDNSServiceResolveListener, + nsIPresentationControlServerListener) + +NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider, + nsIPresentationDeviceProvider, + nsIDNSServiceDiscoveryListener, + nsIDNSRegistrationListener, + nsIDNSServiceResolveListener, + nsIPresentationControlServerListener, + nsIObserver) + +MulticastDNSDeviceProvider::~MulticastDNSDeviceProvider() +{ + Uninit(); +} + +nsresult +MulticastDNSDeviceProvider::Init() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitialized) { + return NS_OK; + } + + nsresult rv; + + mMulticastDNS = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mWrappedListener = new DNSServiceWrappedListener(); + if (NS_WARN_IF(NS_FAILED(rv = mWrappedListener->SetListener(this)))) { + return rv; + } + + mPresentationService = do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mDiscoveryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mServerRetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + Preferences::AddStrongObservers(this, kObservedPrefs); + + mDiscoveryEnabled = Preferences::GetBool(PREF_PRESENTATION_DISCOVERY); + mDiscoveryTimeoutMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS); + mDiscoverable = Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE); + mDiscoverableEncrypted = Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED); + mServerRetryMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS); + mServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME); + +#ifdef MOZ_WIDGET_ANDROID + // FIXME: Bug 1185806 - Provide a common device name setting. + if (mServiceName.IsEmpty()) { + GetAndroidDeviceName(mServiceName); + Unused << Preferences::SetCString(PREF_PRESENTATION_DEVICE_NAME, mServiceName); + } +#endif // MOZ_WIDGET_ANDROID + + Unused << mPresentationService->SetId(mServiceName); + + if (mDiscoveryEnabled && NS_WARN_IF(NS_FAILED(rv = ForceDiscovery()))) { + return rv; + } + + if (mDiscoverable && NS_WARN_IF(NS_FAILED(rv = StartServer()))) { + return rv; + } + + mInitialized = true; + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::Uninit() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mInitialized) { + return NS_OK; + } + + ClearDevices(); + + Preferences::RemoveObservers(this, kObservedPrefs); + + StopDiscovery(NS_OK); + StopServer(); + + mMulticastDNS = nullptr; + + if (mWrappedListener) { + mWrappedListener->SetListener(nullptr); + mWrappedListener = nullptr; + } + + mInitialized = false; + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::StartServer() +{ + LOG_I("StartServer: %s (%d)", mServiceName.get(), mDiscoverable); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDiscoverable) { + return NS_OK; + } + + nsresult rv; + + uint16_t servicePort; + if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->GetPort(&servicePort)))) { + return rv; + } + + /** + * If |servicePort| is non-zero, it means PresentationControlService is running. + * Otherwise, we should make it start serving. + */ + if (servicePort) { + return RegisterMDNSService(); + } + + if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->SetListener(mWrappedListener)))) { + return rv; + } + + AbortServerRetry(); + + if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->StartServer(mDiscoverableEncrypted, 0)))) { + return rv; + } + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::StopServer() +{ + LOG_I("StopServer: %s", mServiceName.get()); + MOZ_ASSERT(NS_IsMainThread()); + + UnregisterMDNSService(NS_OK); + + AbortServerRetry(); + + if (mPresentationService) { + mPresentationService->SetListener(nullptr); + mPresentationService->Close(); + } + + return NS_OK; +} + +void +MulticastDNSDeviceProvider::AbortServerRetry() +{ + if (mIsServerRetrying) { + mIsServerRetrying = false; + mServerRetryTimer->Cancel(); + } +} + +nsresult +MulticastDNSDeviceProvider::RegisterMDNSService() +{ + LOG_I("RegisterMDNSService: %s", mServiceName.get()); + + if (!mDiscoverable) { + return NS_OK; + } + + // Cancel on going service registration. + UnregisterMDNSService(NS_OK); + + nsresult rv; + + uint16_t servicePort; + if (NS_FAILED(rv = mPresentationService->GetPort(&servicePort)) || + !servicePort) { + // Abort service registration if server port is not available. + return rv; + } + + /** + * Register the presentation control channel server as an mDNS service. + */ + nsCOMPtr<nsIDNSServiceInfo> serviceInfo = + do_CreateInstance(DNSSERVICEINFO_CONTRACT_ID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetServiceType( + NS_LITERAL_CSTRING(SERVICE_TYPE))))) { + return rv; + } + if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetServiceName(mServiceName)))) { + return rv; + } + if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetPort(servicePort)))) { + return rv; + } + + nsCOMPtr<nsIWritablePropertyBag2> propBag = + do_CreateInstance("@mozilla.org/hash-property-bag;1"); + MOZ_ASSERT(propBag); + + uint32_t version; + rv = mPresentationService->GetVersion(&version); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = propBag->SetPropertyAsUint32(NS_LITERAL_STRING(PROTOCOL_VERSION_TAG), + version); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (mDiscoverableEncrypted) { + nsAutoCString certFingerprint; + rv = mPresentationService->GetCertFingerprint(certFingerprint); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = propBag->SetPropertyAsACString(NS_LITERAL_STRING(CERT_FINGERPRINT_TAG), + certFingerprint); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetAttributes(propBag)))) { + return rv; + } + + return mMulticastDNS->RegisterService(serviceInfo, + mWrappedListener, + getter_AddRefs(mRegisterRequest)); +} + +nsresult +MulticastDNSDeviceProvider::UnregisterMDNSService(nsresult aReason) +{ + LOG_I("UnregisterMDNSService: %s (0x%08x)", mServiceName.get(), aReason); + MOZ_ASSERT(NS_IsMainThread()); + + if (mRegisterRequest) { + mRegisterRequest->Cancel(aReason); + mRegisterRequest = nullptr; + } + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::StopDiscovery(nsresult aReason) +{ + LOG_I("StopDiscovery (0x%08x)", aReason); + + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mDiscoveryTimer); + + Unused << mDiscoveryTimer->Cancel(); + + if (mDiscoveryRequest) { + mDiscoveryRequest->Cancel(aReason); + mDiscoveryRequest = nullptr; + } + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::Connect(Device* aDevice, + nsIPresentationControlChannel** aRetVal) +{ + MOZ_ASSERT(aDevice); + MOZ_ASSERT(mPresentationService); + + RefPtr<TCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(), + aDevice->Address(), + aDevice->Port(), + aDevice->CertFingerprint()); + + return mPresentationService->Connect(deviceInfo, aRetVal); +} + +bool +MulticastDNSDeviceProvider::IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(aServiceInfo); + + nsCOMPtr<nsIPropertyBag2> propBag; + if (NS_WARN_IF(NS_FAILED( + aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) || !propBag) { + return false; + } + + uint32_t remoteVersion; + if (NS_WARN_IF(NS_FAILED( + propBag->GetPropertyAsUint32(NS_LITERAL_STRING(PROTOCOL_VERSION_TAG), + &remoteVersion)))) { + return false; + } + + bool isCompatible = false; + Unused << NS_WARN_IF(NS_FAILED( + mPresentationService->IsCompatibleServer(remoteVersion, + &isCompatible))); + + return isCompatible; +} + +nsresult +MulticastDNSDeviceProvider::AddDevice(const nsACString& aId, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + RefPtr<Device> device = new Device(aId, /* ID */ + aServiceName, + aServiceType, + aAddress, + aPort, + aCertFingerprint, + DeviceState::eActive, + this); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->AddDevice(device); + } + + mDevices.AppendElement(device); + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::UpdateDevice(const uint32_t aIndex, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + if (NS_WARN_IF(aIndex >= mDevices.Length())) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Device> device = mDevices[aIndex]; + device->Update(aServiceName, aServiceType, aAddress, aPort, aCertFingerprint); + device->ChangeState(DeviceState::eActive); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->UpdateDevice(device); + } + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::RemoveDevice(const uint32_t aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPresentationService); + + if (NS_WARN_IF(aIndex >= mDevices.Length())) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Device> device = mDevices[aIndex]; + + LOG_I("RemoveDevice: %s", device->Id().get()); + mDevices.RemoveElementAt(aIndex); + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->RemoveDevice(device); + } + + return NS_OK; +} + +bool +MulticastDNSDeviceProvider::FindDeviceById(const nsACString& aId, + uint32_t& aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Device> device = new Device(aId, + /* aName = */ EmptyCString(), + /* aType = */ EmptyCString(), + /* aHost = */ EmptyCString(), + /* aPort = */ 0, + /* aCertFingerprint */ EmptyCString(), + /* aState = */ DeviceState::eUnknown, + /* aProvider = */ nullptr); + size_t index = mDevices.IndexOf(device, 0, DeviceIdComparator()); + + if (index == mDevices.NoIndex) { + return false; + } + + aIndex = index; + return true; +} + +bool +MulticastDNSDeviceProvider::FindDeviceByAddress(const nsACString& aAddress, + uint32_t& aIndex) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Device> device = new Device(/* aId = */ EmptyCString(), + /* aName = */ EmptyCString(), + /* aType = */ EmptyCString(), + aAddress, + /* aPort = */ 0, + /* aCertFingerprint */ EmptyCString(), + /* aState = */ DeviceState::eUnknown, + /* aProvider = */ nullptr); + size_t index = mDevices.IndexOf(device, 0, DeviceAddressComparator()); + + if (index == mDevices.NoIndex) { + return false; + } + + aIndex = index; + return true; +} + +void +MulticastDNSDeviceProvider::MarkAllDevicesUnknown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + for (auto& device : mDevices) { + device->ChangeState(DeviceState::eUnknown); + } +} + +void +MulticastDNSDeviceProvider::ClearUnknownDevices() +{ + MOZ_ASSERT(NS_IsMainThread()); + + size_t i = mDevices.Length(); + while (i > 0) { + --i; + if (mDevices[i]->State() == DeviceState::eUnknown) { + Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i))); + } + } +} + +void +MulticastDNSDeviceProvider::ClearDevices() +{ + MOZ_ASSERT(NS_IsMainThread()); + + size_t i = mDevices.Length(); + while (i > 0) { + --i; + Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i))); + } +} + +// nsIPresentationDeviceProvider +NS_IMETHODIMP +MulticastDNSDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_POINTER; + } + + nsresult rv; + nsCOMPtr<nsIPresentationDeviceListener> listener = + do_QueryReferent(mDeviceListener, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + listener.forget(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener) +{ + MOZ_ASSERT(NS_IsMainThread()); + + mDeviceListener = do_GetWeakReference(aListener); + + nsresult rv; + if (mDeviceListener) { + if (NS_WARN_IF(NS_FAILED(rv = Init()))) { + return rv; + } + } else { + if (NS_WARN_IF(NS_FAILED(rv = Uninit()))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::ForceDiscovery() +{ + LOG_I("ForceDiscovery (%d)", mDiscoveryEnabled); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDiscoveryEnabled) { + return NS_OK; + } + + MOZ_ASSERT(mDiscoveryTimer); + MOZ_ASSERT(mMulticastDNS); + + // if it's already discovering, extend existing discovery timeout. + nsresult rv; + if (mIsDiscovering) { + Unused << mDiscoveryTimer->Cancel(); + + if (NS_WARN_IF(NS_FAILED( rv = mDiscoveryTimer->Init(this, + mDiscoveryTimeoutMs, + nsITimer::TYPE_ONE_SHOT)))) { + return rv; + } + return NS_OK; + } + + StopDiscovery(NS_OK); + + if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->StartDiscovery( + NS_LITERAL_CSTRING(SERVICE_TYPE), + mWrappedListener, + getter_AddRefs(mDiscoveryRequest))))) { + return rv; + } + + return NS_OK; +} + +// nsIDNSServiceDiscoveryListener +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnDiscoveryStarted(const nsACString& aServiceType) +{ + LOG_I("OnDiscoveryStarted"); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mDiscoveryTimer); + + MarkAllDevicesUnknown(); + + nsresult rv; + if (NS_WARN_IF(NS_FAILED(rv = mDiscoveryTimer->Init(this, + mDiscoveryTimeoutMs, + nsITimer::TYPE_ONE_SHOT)))) { + return rv; + } + + mIsDiscovering = true; + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnDiscoveryStopped(const nsACString& aServiceType) +{ + LOG_I("OnDiscoveryStopped"); + MOZ_ASSERT(NS_IsMainThread()); + + ClearUnknownDevices(); + + mIsDiscovering = false; + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServiceFound(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv ; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceFound: %s", serviceName.get()); + + if (mMulticastDNS) { + if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService( + aServiceInfo, mWrappedListener)))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServiceLost(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceLost: %s", serviceName.get()); + + nsAutoCString host; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) { + return rv; + } + + uint32_t index; + if (!FindDeviceById(host, index)) { + // given device was not found + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv = RemoveDevice(index)))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnStartDiscoveryFailed(const nsACString& aServiceType, + int32_t aErrorCode) +{ + LOG_E("OnStartDiscoveryFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnStopDiscoveryFailed(const nsACString& aServiceType, + int32_t aErrorCode) +{ + LOG_E("OnStopDiscoveryFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +// nsIDNSRegistrationListener +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServiceRegistered(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + nsresult rv; + + nsAutoCString name; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(name)))) { + return rv; + } + + LOG_I("OnServiceRegistered (%s)", name.get()); + mRegisteredName = name; + + if (mMulticastDNS) { + if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService( + aServiceInfo, mWrappedListener)))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServiceUnregistered(nsIDNSServiceInfo* aServiceInfo) +{ + LOG_I("OnServiceUnregistered"); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnRegistrationFailed(nsIDNSServiceInfo* aServiceInfo, + int32_t aErrorCode) +{ + LOG_E("OnRegistrationFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + mRegisterRequest = nullptr; + + if (aErrorCode == nsIDNSRegistrationListener::ERROR_SERVICE_NOT_RUNNING) { + return NS_DispatchToMainThread( + NewRunnableMethod(this, &MulticastDNSDeviceProvider::RegisterMDNSService)); + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnUnregistrationFailed(nsIDNSServiceInfo* aServiceInfo, + int32_t aErrorCode) +{ + LOG_E("OnUnregistrationFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +// nsIDNSServiceResolveListener +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aServiceInfo)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + + nsAutoCString serviceName; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) { + return rv; + } + + LOG_I("OnServiceResolved: %s", serviceName.get()); + + nsAutoCString host; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) { + return rv; + } + + if (mRegisteredName == serviceName) { + LOG_I("ignore self"); + + if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->SetId(host)))) { + return rv; + } + + return NS_OK; + } + + if (!IsCompatibleServer(aServiceInfo)) { + LOG_I("ignore incompatible service: %s", serviceName.get()); + return NS_OK; + } + + nsAutoCString address; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetAddress(address)))) { + return rv; + } + + uint16_t port; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetPort(&port)))) { + return rv; + } + + nsAutoCString serviceType; + if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceType(serviceType)))) { + return rv; + } + + nsCOMPtr<nsIPropertyBag2> propBag; + if (NS_WARN_IF(NS_FAILED( + aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) || !propBag) { + return rv; + } + + nsAutoCString certFingerprint; + Unused << propBag->GetPropertyAsACString(NS_LITERAL_STRING(CERT_FINGERPRINT_TAG), + certFingerprint); + + uint32_t index; + if (FindDeviceById(host, index)) { + return UpdateDevice(index, + serviceName, + serviceType, + address, + port, + certFingerprint); + } else { + return AddDevice(host, + serviceName, + serviceType, + address, + port, + certFingerprint); + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo, + int32_t aErrorCode) +{ + LOG_E("OnResolveFailed: %d", aErrorCode); + MOZ_ASSERT(NS_IsMainThread()); + + return NS_OK; +} + +// nsIPresentationControlServerListener +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServerReady(uint16_t aPort, + const nsACString& aCertFingerprint) +{ + LOG_I("OnServerReady: %d, %s", aPort, PromiseFlatCString(aCertFingerprint).get()); + MOZ_ASSERT(NS_IsMainThread()); + + if (mDiscoverable) { + RegisterMDNSService(); + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnServerStopped(nsresult aResult) +{ + LOG_I("OnServerStopped: (0x%08x)", aResult); + + UnregisterMDNSService(aResult); + + // Try restart server if it is stopped abnormally. + if (NS_FAILED(aResult) && mDiscoverable) { + mIsServerRetrying = true; + mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT); + } + + return NS_OK; +} + +// Create a new device if we were unable to find one with the address. +already_AddRefed<MulticastDNSDeviceProvider::Device> +MulticastDNSDeviceProvider::GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo) +{ + nsAutoCString address; + Unused << aDeviceInfo->GetAddress(address); + + RefPtr<Device> device; + uint32_t index; + if (FindDeviceByAddress(address, index)) { + device = mDevices[index]; + } else { + // Create a one-time device object for non-discoverable controller. + // This device will not be in the list of available devices and cannot + // be used for requesting session. + nsAutoCString id; + Unused << aDeviceInfo->GetId(id); + uint16_t port; + Unused << aDeviceInfo->GetPort(&port); + + device = new Device(id, + /* aName = */ id, + /* aType = */ EmptyCString(), + address, + port, + /* aCertFingerprint */ EmptyCString(), + DeviceState::eActive, + /* aProvider = */ nullptr); + } + + return device.forget(); +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnSessionRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aUrl, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString address; + Unused << aDeviceInfo->GetAddress(address); + + LOG_I("OnSessionRequest: %s", address.get()); + + RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo); + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->OnSessionRequest(device, aUrl, aPresentationId, + aControlChannel); + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString address; + Unused << aDeviceInfo->GetAddress(address); + + LOG_I("OnTerminateRequest: %s", address.get()); + + RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo); + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->OnTerminateRequest(device, aPresentationId, + aControlChannel, aIsFromReceiver); + } + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnReconnectRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aUrl, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString address; + Unused << aDeviceInfo->GetAddress(address); + + LOG_I("OnReconnectRequest: %s", address.get()); + + RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo); + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->OnReconnectRequest(device, aUrl, aPresentationId, + aControlChannel); + } + + return NS_OK; +} + +// nsIObserver +NS_IMETHODIMP +MulticastDNSDeviceProvider::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + NS_ConvertUTF16toUTF8 data(aData); + LOG_I("Observe: topic = %s, data = %s", aTopic, data.get()); + + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY)) { + OnDiscoveryChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERY)); + } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)) { + OnDiscoveryTimeoutChanged(Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)); + } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERABLE)) { + OnDiscoverableChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE)); + } else if (data.EqualsLiteral(PREF_PRESENTATION_DEVICE_NAME)) { + nsAdoptingCString newServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME); + if (!mServiceName.Equals(newServiceName)) { + OnServiceNameChanged(newServiceName); + } + } + } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject); + if (!timer) { + return NS_ERROR_UNEXPECTED; + } + + if (timer == mDiscoveryTimer) { + StopDiscovery(NS_OK); + } else if (timer == mServerRetryTimer) { + mIsServerRetrying = false; + StartServer(); + } + } + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::OnDiscoveryChanged(bool aEnabled) +{ + LOG_I("DiscoveryEnabled = %d\n", aEnabled); + MOZ_ASSERT(NS_IsMainThread()); + + mDiscoveryEnabled = aEnabled; + + if (mDiscoveryEnabled) { + return ForceDiscovery(); + } + + return StopDiscovery(NS_OK); +} + +nsresult +MulticastDNSDeviceProvider::OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs) +{ + LOG_I("OnDiscoveryTimeoutChanged = %d\n", aTimeoutMs); + MOZ_ASSERT(NS_IsMainThread()); + + mDiscoveryTimeoutMs = aTimeoutMs; + + return NS_OK; +} + +nsresult +MulticastDNSDeviceProvider::OnDiscoverableChanged(bool aEnabled) +{ + LOG_I("Discoverable = %d\n", aEnabled); + MOZ_ASSERT(NS_IsMainThread()); + + mDiscoverable = aEnabled; + + if (mDiscoverable) { + return StartServer(); + } + + return StopServer(); +} + +nsresult +MulticastDNSDeviceProvider::OnServiceNameChanged(const nsACString& aServiceName) +{ + LOG_I("serviceName = %s\n", PromiseFlatCString(aServiceName).get()); + MOZ_ASSERT(NS_IsMainThread()); + + mServiceName = aServiceName; + + nsresult rv; + if (NS_WARN_IF(NS_FAILED(rv = UnregisterMDNSService(NS_OK)))) { + return rv; + } + + if (mDiscoverable) { + return RegisterMDNSService(); + } + + return NS_OK; +} + +// MulticastDNSDeviceProvider::Device +NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider::Device, + nsIPresentationDevice) + +// nsIPresentationDevice +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::GetId(nsACString& aId) +{ + aId = mId; + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::GetName(nsACString& aName) +{ + aName = mName; + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::GetType(nsACString& aType) +{ + aType = mType; + + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::EstablishControlChannel( + nsIPresentationControlChannel** aRetVal) +{ + if (!mProvider) { + return NS_ERROR_FAILURE; + } + + return mProvider->Connect(this, aRetVal); +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::Disconnect() +{ + // No need to do anything when disconnect. + return NS_OK; +} + +NS_IMETHODIMP +MulticastDNSDeviceProvider::Device::IsRequestedUrlSupported( + const nsAString& aRequestedUrl, + bool* aRetVal) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!aRetVal) { + return NS_ERROR_INVALID_POINTER; + } + + // TV 2.6 also supports presentation Apps and HTTP/HTTPS hosted receiver page. + if (DeviceProviderHelpers::IsFxTVSupportedAppUrl(aRequestedUrl) || + DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl)) { + *aRetVal = true; + } + + return NS_OK; +} + +} // namespace presentation +} // namespace dom +} // namespace mozilla diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.h b/dom/presentation/provider/MulticastDNSDeviceProvider.h new file mode 100644 index 000000000..c6a91b3d8 --- /dev/null +++ b/dom/presentation/provider/MulticastDNSDeviceProvider.h @@ -0,0 +1,225 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h +#define mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h + +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsICancelable.h" +#include "nsIDNSServiceDiscovery.h" +#include "nsIObserver.h" +#include "nsIPresentationDevice.h" +#include "nsIPresentationDeviceProvider.h" +#include "nsIPresentationControlService.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakPtr.h" + +class nsITCPDeviceInfo; + +namespace mozilla { +namespace dom { +namespace presentation { + +class DNSServiceWrappedListener; +class MulticastDNSService; + +class MulticastDNSDeviceProvider final + : public nsIPresentationDeviceProvider + , public nsIDNSServiceDiscoveryListener + , public nsIDNSRegistrationListener + , public nsIDNSServiceResolveListener + , public nsIPresentationControlServerListener + , public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONDEVICEPROVIDER + NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER + NS_DECL_NSIDNSREGISTRATIONLISTENER + NS_DECL_NSIDNSSERVICERESOLVELISTENER + NS_DECL_NSIPRESENTATIONCONTROLSERVERLISTENER + NS_DECL_NSIOBSERVER + + explicit MulticastDNSDeviceProvider() = default; + nsresult Init(); + nsresult Uninit(); + +private: + enum class DeviceState : uint32_t { + eUnknown, + eActive + }; + + class Device final : public nsIPresentationDevice + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONDEVICE + + explicit Device(const nsACString& aId, + const nsACString& aName, + const nsACString& aType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint, + DeviceState aState, + MulticastDNSDeviceProvider* aProvider) + : mId(aId) + , mName(aName) + , mType(aType) + , mAddress(aAddress) + , mPort(aPort) + , mCertFingerprint(aCertFingerprint) + , mState(aState) + , mProvider(aProvider) + { + } + + const nsCString& Id() const + { + return mId; + } + + const nsCString& Address() const + { + return mAddress; + } + + uint16_t Port() const + { + return mPort; + } + + const nsCString& CertFingerprint() const + { + return mCertFingerprint; + } + + DeviceState State() const + { + return mState; + } + + void ChangeState(DeviceState aState) + { + mState = aState; + } + + void Update(const nsACString& aName, + const nsACString& aType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint) + { + mName = aName; + mType = aType; + mAddress = aAddress; + mPort = aPort; + mCertFingerprint = aCertFingerprint; + } + + private: + virtual ~Device() = default; + + nsCString mId; + nsCString mName; + nsCString mType; + nsCString mAddress; + uint16_t mPort; + nsCString mCertFingerprint; + DeviceState mState; + MulticastDNSDeviceProvider* mProvider; + }; + + struct DeviceIdComparator { + bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const { + return aA->Id() == aB->Id(); + } + }; + + struct DeviceAddressComparator { + bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const { + return aA->Address() == aB->Address(); + } + }; + + virtual ~MulticastDNSDeviceProvider(); + nsresult StartServer(); + nsresult StopServer(); + void AbortServerRetry(); + nsresult RegisterMDNSService(); + nsresult UnregisterMDNSService(nsresult aReason); + nsresult StopDiscovery(nsresult aReason); + nsresult Connect(Device* aDevice, + nsIPresentationControlChannel** aRetVal); + bool IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo); + + // device manipulation + nsresult AddDevice(const nsACString& aId, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint); + nsresult UpdateDevice(const uint32_t aIndex, + const nsACString& aServiceName, + const nsACString& aServiceType, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint); + nsresult RemoveDevice(const uint32_t aIndex); + bool FindDeviceById(const nsACString& aId, + uint32_t& aIndex); + + bool FindDeviceByAddress(const nsACString& aAddress, + uint32_t& aIndex); + + already_AddRefed<Device> + GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo); + + void MarkAllDevicesUnknown(); + void ClearUnknownDevices(); + void ClearDevices(); + + // preferences + nsresult OnDiscoveryChanged(bool aEnabled); + nsresult OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs); + nsresult OnDiscoverableChanged(bool aEnabled); + nsresult OnServiceNameChanged(const nsACString& aServiceName); + + bool mInitialized = false; + nsWeakPtr mDeviceListener; + nsCOMPtr<nsIPresentationControlService> mPresentationService; + nsCOMPtr<nsIDNSServiceDiscovery> mMulticastDNS; + RefPtr<DNSServiceWrappedListener> mWrappedListener; + + nsCOMPtr<nsICancelable> mDiscoveryRequest; + nsCOMPtr<nsICancelable> mRegisterRequest; + + nsTArray<RefPtr<Device>> mDevices; + + bool mDiscoveryEnabled = false; + bool mIsDiscovering = false; + uint32_t mDiscoveryTimeoutMs; + nsCOMPtr<nsITimer> mDiscoveryTimer; + + bool mDiscoverable = false; + bool mDiscoverableEncrypted = false; + bool mIsServerRetrying = false; + uint32_t mServerRetryMs; + nsCOMPtr<nsITimer> mServerRetryTimer; + + nsCString mServiceName; + nsCString mRegisteredName; +}; + +} // namespace presentation +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h diff --git a/dom/presentation/provider/PresentationControlService.js b/dom/presentation/provider/PresentationControlService.js new file mode 100644 index 000000000..fe61d26d6 --- /dev/null +++ b/dom/presentation/provider/PresentationControlService.js @@ -0,0 +1,961 @@ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ +/* globals Components, dump */ +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +/* globals XPCOMUtils */ +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +/* globals Services */ +Cu.import("resource://gre/modules/Services.jsm"); +/* globals NetUtil */ +Cu.import("resource://gre/modules/NetUtil.jsm"); +/* globals setTimeout, clearTimeout */ +Cu.import("resource://gre/modules/Timer.jsm"); + +/* globals ControllerStateMachine */ +XPCOMUtils.defineLazyModuleGetter(this, "ControllerStateMachine", // jshint ignore:line + "resource://gre/modules/presentation/ControllerStateMachine.jsm"); +/* global ReceiverStateMachine */ +XPCOMUtils.defineLazyModuleGetter(this, "ReceiverStateMachine", // jshint ignore:line + "resource://gre/modules/presentation/ReceiverStateMachine.jsm"); + +const kProtocolVersion = 1; // need to review isCompatibleServer while fiddling the version number. +const kLocalCertName = "presentation"; + +const DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug"); +function log(aMsg) { + dump("-*- PresentationControlService.js: " + aMsg + "\n"); +} + +function TCPDeviceInfo(aAddress, aPort, aId, aCertFingerprint) { + this.address = aAddress; + this.port = aPort; + this.id = aId; + this.certFingerprint = aCertFingerprint || ""; +} + +function PresentationControlService() { + this._id = null; + this._port = 0; + this._serverSocket = null; +} + +PresentationControlService.prototype = { + /** + * If a user agent connects to this server, we create a control channel but + * hand it to |TCPDevice.listener| when the initial information exchange + * finishes. Therefore, we hold the control channels in this period. + */ + _controlChannels: [], + + startServer: function(aEncrypted, aPort) { + if (this._isServiceInit()) { + DEBUG && log("PresentationControlService - server socket has been initialized"); // jshint ignore:line + throw Cr.NS_ERROR_FAILURE; + } + + /** + * 0 or undefined indicates opt-out parameter, and a port will be selected + * automatically. + */ + let serverSocketPort = (typeof aPort !== "undefined" && aPort !== 0) ? aPort : -1; + + if (aEncrypted) { + let self = this; + let localCertService = Cc["@mozilla.org/security/local-cert-service;1"] + .getService(Ci.nsILocalCertService); + localCertService.getOrCreateCert(kLocalCertName, { + handleCert: function(aCert, aRv) { + DEBUG && log("PresentationControlService - handleCert"); // jshint ignore:line + if (aRv) { + self._notifyServerStopped(aRv); + } else { + self._serverSocket = Cc["@mozilla.org/network/tls-server-socket;1"] + .createInstance(Ci.nsITLSServerSocket); + + self._serverSocketInit(serverSocketPort, aCert); + } + } + }); + } else { + this._serverSocket = Cc["@mozilla.org/network/server-socket;1"] + .createInstance(Ci.nsIServerSocket); + + this._serverSocketInit(serverSocketPort, null); + } + }, + + _serverSocketInit: function(aPort, aCert) { + if (!this._serverSocket) { + DEBUG && log("PresentationControlService - create server socket fail."); // jshint ignore:line + throw Cr.NS_ERROR_FAILURE; + } + + try { + this._serverSocket.init(aPort, false, -1); + + if (aCert) { + this._serverSocket.serverCert = aCert; + this._serverSocket.setSessionCache(false); + this._serverSocket.setSessionTickets(false); + let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER; + this._serverSocket.setRequestClientCertificate(requestCert); + } + + this._serverSocket.asyncListen(this); + } catch (e) { + // NS_ERROR_SOCKET_ADDRESS_IN_USE + DEBUG && log("PresentationControlService - init server socket fail: " + e); // jshint ignore:line + throw Cr.NS_ERROR_FAILURE; + } + + this._port = this._serverSocket.port; + + DEBUG && log("PresentationControlService - service start on port: " + this._port); // jshint ignore:line + + // Monitor network interface change to restart server socket. + Services.obs.addObserver(this, "network:offline-status-changed", false); + + this._notifyServerReady(); + }, + + _notifyServerReady: function() { + Services.tm.mainThread.dispatch(() => { + if (this._listener) { + this._listener.onServerReady(this._port, this.certFingerprint); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + _notifyServerStopped: function(aRv) { + Services.tm.mainThread.dispatch(() => { + if (this._listener) { + this._listener.onServerStopped(aRv); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + isCompatibleServer: function(aVersion) { + // No compatibility issue for the first version of control protocol + return this.version === aVersion; + }, + + get id() { + return this._id; + }, + + set id(aId) { + this._id = aId; + }, + + get port() { + return this._port; + }, + + get version() { + return kProtocolVersion; + }, + + get certFingerprint() { + if (!this._serverSocket.serverCert) { + return null; + } + + return this._serverSocket.serverCert.sha256Fingerprint; + }, + + set listener(aListener) { + this._listener = aListener; + }, + + get listener() { + return this._listener; + }, + + _isServiceInit: function() { + return this._serverSocket !== null; + }, + + connect: function(aDeviceInfo) { + if (!this.id) { + DEBUG && log("PresentationControlService - Id has not initialized; connect fails"); // jshint ignore:line + return null; + } + DEBUG && log("PresentationControlService - connect to " + aDeviceInfo.id); // jshint ignore:line + + let socketTransport = this._attemptConnect(aDeviceInfo); + return new TCPControlChannel(this, + socketTransport, + aDeviceInfo, + "sender"); + }, + + _attemptConnect: function(aDeviceInfo) { + let sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + let socketTransport; + try { + if (aDeviceInfo.certFingerprint) { + let overrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + overrideService.rememberTemporaryValidityOverrideUsingFingerprint( + aDeviceInfo.address, + aDeviceInfo.port, + aDeviceInfo.certFingerprint, + Ci.nsICertOverrideService.ERROR_UNTRUSTED | Ci.nsICertOverrideService.ERROR_MISMATCH); + + socketTransport = sts.createTransport(["ssl"], + 1, + aDeviceInfo.address, + aDeviceInfo.port, + null); + } else { + socketTransport = sts.createTransport(null, + 0, + aDeviceInfo.address, + aDeviceInfo.port, + null); + } + // Shorten the connection failure procedure. + socketTransport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2); + } catch (e) { + DEBUG && log("PresentationControlService - createTransport throws: " + e); // jshint ignore:line + // Pop the exception to |TCPDevice.establishControlChannel| + throw Cr.NS_ERROR_FAILURE; + } + return socketTransport; + }, + + responseSession: function(aDeviceInfo, aSocketTransport) { + if (!this._isServiceInit()) { + DEBUG && log("PresentationControlService - should never receive remote " + + "session request before server socket initialization"); // jshint ignore:line + return null; + } + DEBUG && log("PresentationControlService - responseSession to " + + JSON.stringify(aDeviceInfo)); // jshint ignore:line + return new TCPControlChannel(this, + aSocketTransport, + aDeviceInfo, + "receiver"); + }, + + // Triggered by TCPControlChannel + onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) { + DEBUG && log("PresentationControlService - onSessionRequest: " + + aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line + if (!this.listener) { + this.releaseControlChannel(aControlChannel); + return; + } + + this.listener.onSessionRequest(aDeviceInfo, + aUrl, + aPresentationId, + aControlChannel); + this.releaseControlChannel(aControlChannel); + }, + + onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) { + DEBUG && log("TCPPresentationServer - onSessionTerminate: " + + aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line + if (!this.listener) { + this.releaseControlChannel(aControlChannel); + return; + } + + this.listener.onTerminateRequest(aDeviceInfo, + aPresentationId, + aControlChannel, + aIsFromReceiver); + this.releaseControlChannel(aControlChannel); + }, + + onSessionReconnect: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) { + DEBUG && log("TCPPresentationServer - onSessionReconnect: " + + aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line + if (!this.listener) { + this.releaseControlChannel(aControlChannel); + return; + } + + this.listener.onReconnectRequest(aDeviceInfo, + aUrl, + aPresentationId, + aControlChannel); + this.releaseControlChannel(aControlChannel); + }, + + // nsIServerSocketListener (Triggered by nsIServerSocket.init) + onSocketAccepted: function(aServerSocket, aClientSocket) { + DEBUG && log("PresentationControlService - onSocketAccepted: " + + aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line + let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port); + this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket)); + }, + + holdControlChannel: function(aControlChannel) { + this._controlChannels.push(aControlChannel); + }, + + releaseControlChannel: function(aControlChannel) { + let index = this._controlChannels.indexOf(aControlChannel); + if (index !== -1) { + delete this._controlChannels[index]; + } + }, + + // nsIServerSocketListener (Triggered by nsIServerSocket.init) + onStopListening: function(aServerSocket, aStatus) { + DEBUG && log("PresentationControlService - onStopListening: " + aStatus); // jshint ignore:line + }, + + close: function() { + DEBUG && log("PresentationControlService - close"); // jshint ignore:line + if (this._isServiceInit()) { + DEBUG && log("PresentationControlService - close server socket"); // jshint ignore:line + this._serverSocket.close(); + this._serverSocket = null; + + Services.obs.removeObserver(this, "network:offline-status-changed"); + + this._notifyServerStopped(Cr.NS_OK); + } + this._port = 0; + }, + + // nsIObserver + observe: function(aSubject, aTopic, aData) { + DEBUG && log("PresentationControlService - observe: " + aTopic); // jshint ignore:line + switch (aTopic) { + case "network:offline-status-changed": { + if (aData == "offline") { + DEBUG && log("network offline"); // jshint ignore:line + return; + } + this._restartServer(); + break; + } + } + }, + + _restartServer: function() { + DEBUG && log("PresentationControlService - restart service"); // jshint ignore:line + + // restart server socket + if (this._isServiceInit()) { + this.close(); + + try { + this.startServer(); + } catch (e) { + DEBUG && log("PresentationControlService - restart service fail: " + e); // jshint ignore:line + } + } + }, + + classID: Components.ID("{f4079b8b-ede5-4b90-a112-5b415a931deb}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIServerSocketListener, + Ci.nsIPresentationControlService, + Ci.nsIObserver]), +}; + +function ChannelDescription(aInit) { + this._type = aInit.type; + switch (this._type) { + case Ci.nsIPresentationChannelDescription.TYPE_TCP: + this._tcpAddresses = Cc["@mozilla.org/array;1"] + .createInstance(Ci.nsIMutableArray); + for (let address of aInit.tcpAddress) { + let wrapper = Cc["@mozilla.org/supports-cstring;1"] + .createInstance(Ci.nsISupportsCString); + wrapper.data = address; + this._tcpAddresses.appendElement(wrapper, false); + } + + this._tcpPort = aInit.tcpPort; + break; + case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL: + this._dataChannelSDP = aInit.dataChannelSDP; + break; + } +} + +ChannelDescription.prototype = { + _type: 0, + _tcpAddresses: null, + _tcpPort: 0, + _dataChannelSDP: "", + + get type() { + return this._type; + }, + + get tcpAddress() { + return this._tcpAddresses; + }, + + get tcpPort() { + return this._tcpPort; + }, + + get dataChannelSDP() { + return this._dataChannelSDP; + }, + + classID: Components.ID("{82507aea-78a2-487e-904a-858a6c5bf4e1}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]), +}; + +// Helper function: transfer nsIPresentationChannelDescription to json +function discriptionAsJson(aDescription) { + let json = {}; + json.type = aDescription.type; + switch(aDescription.type) { + case Ci.nsIPresentationChannelDescription.TYPE_TCP: + let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray); + json.tcpAddress = []; + for (let idx = 0; idx < addresses.length; idx++) { + let address = addresses.queryElementAt(idx, Ci.nsISupportsCString); + json.tcpAddress.push(address.data); + } + json.tcpPort = aDescription.tcpPort; + break; + case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL: + json.dataChannelSDP = aDescription.dataChannelSDP; + break; + } + return json; +} + +const kDisconnectTimeout = 5000; +const kTerminateTimeout = 5000; + +function TCPControlChannel(presentationService, + transport, + deviceInfo, + direction) { + DEBUG && log("create TCPControlChannel for : " + direction); // jshint ignore:line + this._deviceInfo = deviceInfo; + this._direction = direction; + this._transport = transport; + + this._presentationService = presentationService; + + if (direction === "receiver") { + // Need to set security observer before I/O stream operation. + this._setSecurityObserver(this); + } + + let currentThread = Services.tm.currentThread; + transport.setEventSink(this, currentThread); + + this._input = this._transport.openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + this._input.asyncWait(this.QueryInterface(Ci.nsIStreamListener), + Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY, + 0, + currentThread); + + this._output = this._transport + .openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED, 0, 0) + .QueryInterface(Ci.nsIAsyncOutputStream); + + this._outgoingMsgs = []; + + + this._stateMachine = + (direction === "sender") ? new ControllerStateMachine(this, presentationService.id) + : new ReceiverStateMachine(this); + + if (direction === "receiver" && !transport.securityInfo) { + // Since the transport created by server socket is already CONNECTED_TO. + this._outgoingEnabled = true; + this._createInputStreamPump(); + } +} + +TCPControlChannel.prototype = { + _outgoingEnabled: false, + _incomingEnabled: false, + _pendingOpen: false, + _pendingOffer: null, + _pendingAnswer: null, + _pendingClose: null, + _pendingCloseReason: null, + _pendingReconnect: false, + + sendOffer: function(aOffer) { + this._stateMachine.sendOffer(discriptionAsJson(aOffer)); + }, + + sendAnswer: function(aAnswer) { + this._stateMachine.sendAnswer(discriptionAsJson(aAnswer)); + }, + + sendIceCandidate: function(aCandidate) { + this._stateMachine.updateIceCandidate(aCandidate); + }, + + launch: function(aPresentationId, aUrl) { + this._stateMachine.launch(aPresentationId, aUrl); + }, + + terminate: function(aPresentationId) { + if (!this._terminatingId) { + this._terminatingId = aPresentationId; + this._stateMachine.terminate(aPresentationId); + + // Start a guard timer to ensure terminateAck is processed. + this._terminateTimer = setTimeout(() => { + DEBUG && log("TCPControlChannel - terminate timeout: " + aPresentationId); // jshint ignore:line + delete this._terminateTimer; + if (this._pendingDisconnect) { + this._pendingDisconnect(); + } else { + this.disconnect(Cr.NS_OK); + } + }, kTerminateTimeout); + } else { + this._stateMachine.terminateAck(aPresentationId); + delete this._terminatingId; + } + }, + + _flushOutgoing: function() { + if (!this._outgoingEnabled || this._outgoingMsgs.length === 0) { + return; + } + + this._output.asyncWait(this, 0, 0, Services.tm.currentThread); + }, + + // may throw an exception + _send: function(aMsg) { + DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); // jshint ignore:line + + /** + * XXX In TCP streaming, it is possible that more than one message in one + * TCP packet. We use line delimited JSON to identify where one JSON encoded + * object ends and the next begins. Therefore, we do not allow newline + * characters whithin the whole message, and add a newline at the end. + * Please see the parser code in |onDataAvailable|. + */ + let message = JSON.stringify(aMsg).replace(["\n"], "") + "\n"; + try { + this._output.write(message, message.length); + } catch(e) { + DEBUG && log("TCPControlChannel - Failed to send message: " + e.name); // jshint ignore:line + throw e; + } + }, + + _setSecurityObserver: function(observer) { + if (this._transport && this._transport.securityInfo) { + DEBUG && log("TCPControlChannel - setSecurityObserver: " + observer); // jshint ignore:line + let connectionInfo = this._transport.securityInfo + .QueryInterface(Ci.nsITLSServerConnectionInfo); + connectionInfo.setSecurityObserver(observer); + } + }, + + // nsITLSServerSecurityObserver + onHandshakeDone: function(socket, clientStatus) { + log("TCPControlChannel - onHandshakeDone: TLS version: " + clientStatus.tlsVersionUsed.toString(16)); + this._setSecurityObserver(null); + + // Process input/output after TLS handshake is complete. + this._outgoingEnabled = true; + this._createInputStreamPump(); + }, + + // nsIAsyncOutputStream + onOutputStreamReady: function() { + DEBUG && log("TCPControlChannel - onOutputStreamReady"); // jshint ignore:line + if (this._outgoingMsgs.length === 0) { + return; + } + + try { + this._send(this._outgoingMsgs[0]); + } catch (e) { + if (e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this._output.asyncWait(this, 0, 0, Services.tm.currentThread); + return; + } + + this._closeTransport(); + return; + } + this._outgoingMsgs.shift(); + this._flushOutgoing(); + }, + + // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait) + // Only used for detecting connection refused + onInputStreamReady: function(aStream) { + DEBUG && log("TCPControlChannel - onInputStreamReady"); // jshint ignore:line + try { + aStream.available(); + } catch (e) { + DEBUG && log("TCPControlChannel - onInputStreamReady error: " + e.name); // jshint ignore:line + // NS_ERROR_CONNECTION_REFUSED + this._notifyDisconnected(e.result); + } + }, + + // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink) + onTransportStatus: function(aTransport, aStatus) { + DEBUG && log("TCPControlChannel - onTransportStatus: " + aStatus.toString(16) + + " with role: " + this._direction); // jshint ignore:line + if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + this._outgoingEnabled = true; + this._createInputStreamPump(); + } + }, + + // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead) + onStartRequest: function() { + DEBUG && log("TCPControlChannel - onStartRequest with role: " + + this._direction); // jshint ignore:line + this._incomingEnabled = true; + }, + + // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead) + onStopRequest: function(aRequest, aContext, aStatus) { + DEBUG && log("TCPControlChannel - onStopRequest: " + aStatus + + " with role: " + this._direction); // jshint ignore:line + this._stateMachine.onChannelClosed(aStatus, true); + }, + + // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead) + onDataAvailable: function(aRequest, aContext, aInputStream) { + let data = NetUtil.readInputStreamToString(aInputStream, + aInputStream.available()); + DEBUG && log("TCPControlChannel - onDataAvailable: " + data); // jshint ignore:line + + // Parser of line delimited JSON. Please see |_send| for more informaiton. + let jsonArray = data.split("\n"); + jsonArray.pop(); + for (let json of jsonArray) { + let msg; + try { + msg = JSON.parse(json); + } catch (e) { + DEBUG && log("TCPSignalingChannel - error in parsing json: " + e); // jshint ignore:line + } + + this._handleMessage(msg); + } + }, + + _createInputStreamPump: function() { + if (this._pump) { + return; + } + + DEBUG && log("TCPControlChannel - create pump with role: " + + this._direction); // jshint ignore:line + this._pump = Cc["@mozilla.org/network/input-stream-pump;1"]. + createInstance(Ci.nsIInputStreamPump); + this._pump.init(this._input, -1, -1, 0, 0, false); + this._pump.asyncRead(this, null); + this._stateMachine.onChannelReady(); + }, + + // Handle command from remote side + _handleMessage: function(aMsg) { + DEBUG && log("TCPControlChannel - handleMessage from " + + JSON.stringify(this._deviceInfo) + ": " + JSON.stringify(aMsg)); // jshint ignore:line + this._stateMachine.onCommand(aMsg); + }, + + get listener() { + return this._listener; + }, + + set listener(aListener) { + DEBUG && log("TCPControlChannel - set listener: " + aListener); // jshint ignore:line + if (!aListener) { + this._listener = null; + return; + } + + this._listener = aListener; + if (this._pendingOpen) { + this._pendingOpen = false; + DEBUG && log("TCPControlChannel - notify pending opened"); // jshint ignore:line + this._listener.notifyConnected(); + } + + if (this._pendingOffer) { + let offer = this._pendingOffer; + DEBUG && log("TCPControlChannel - notify pending offer: " + + JSON.stringify(offer)); // jshint ignore:line + this._listener.onOffer(new ChannelDescription(offer)); + this._pendingOffer = null; + } + + if (this._pendingAnswer) { + let answer = this._pendingAnswer; + DEBUG && log("TCPControlChannel - notify pending answer: " + + JSON.stringify(answer)); // jshint ignore:line + this._listener.onAnswer(new ChannelDescription(answer)); + this._pendingAnswer = null; + } + + if (this._pendingClose) { + DEBUG && log("TCPControlChannel - notify pending closed"); // jshint ignore:line + this._notifyDisconnected(this._pendingCloseReason); + this._pendingClose = null; + } + + if (this._pendingReconnect) { + DEBUG && log("TCPControlChannel - notify pending reconnected"); // jshint ignore:line + this._notifyReconnected(); + this._pendingReconnect = false; + } + }, + + /** + * These functions are designed to handle the interaction with listener + * appropriately. |_FUNC| is to handle |this._listener.FUNC|. + */ + _onOffer: function(aOffer) { + if (!this._incomingEnabled) { + return; + } + if (!this._listener) { + this._pendingOffer = aOffer; + return; + } + DEBUG && log("TCPControlChannel - notify offer: " + + JSON.stringify(aOffer)); // jshint ignore:line + this._listener.onOffer(new ChannelDescription(aOffer)); + }, + + _onAnswer: function(aAnswer) { + if (!this._incomingEnabled) { + return; + } + if (!this._listener) { + this._pendingAnswer = aAnswer; + return; + } + DEBUG && log("TCPControlChannel - notify answer: " + + JSON.stringify(aAnswer)); // jshint ignore:line + this._listener.onAnswer(new ChannelDescription(aAnswer)); + }, + + _notifyConnected: function() { + this._pendingClose = false; + this._pendingCloseReason = Cr.NS_OK; + + if (!this._listener) { + this._pendingOpen = true; + return; + } + + DEBUG && log("TCPControlChannel - notify opened with role: " + + this._direction); // jshint ignore:line + this._listener.notifyConnected(); + }, + + _notifyDisconnected: function(aReason) { + this._pendingOpen = false; + this._pendingOffer = null; + this._pendingAnswer = null; + + // Remote endpoint closes the control channel with abnormal reason. + if (aReason == Cr.NS_OK && this._pendingCloseReason != Cr.NS_OK) { + aReason = this._pendingCloseReason; + } + + if (!this._listener) { + this._pendingClose = true; + this._pendingCloseReason = aReason; + return; + } + + DEBUG && log("TCPControlChannel - notify closed with role: " + + this._direction); // jshint ignore:line + this._listener.notifyDisconnected(aReason); + }, + + _notifyReconnected: function() { + if (!this._listener) { + this._pendingReconnect = true; + return; + } + + DEBUG && log("TCPControlChannel - notify reconnected with role: " + + this._direction); // jshint ignore:line + this._listener.notifyReconnected(); + }, + + _closeOutgoing: function() { + if (this._outgoingEnabled) { + this._output.close(); + this._outgoingEnabled = false; + } + }, + _closeIncoming: function() { + if (this._incomingEnabled) { + this._pump = null; + this._input.close(); + this._incomingEnabled = false; + } + }, + _closeTransport: function() { + if (this._disconnectTimer) { + clearTimeout(this._disconnectTimer); + delete this._disconnectTimer; + } + + if (this._terminateTimer) { + clearTimeout(this._terminateTimer); + delete this._terminateTimer; + } + + delete this._pendingDisconnect; + + this._transport.setEventSink(null, null); + + this._closeIncoming(); + this._closeOutgoing(); + this._presentationService.releaseControlChannel(this); + }, + + disconnect: function(aReason) { + DEBUG && log("TCPControlChannel - disconnect with reason: " + aReason); // jshint ignore:line + + // Pending disconnect during termination procedure. + if (this._terminateTimer) { + // Store only the first disconnect action. + if (!this._pendingDisconnect) { + this._pendingDisconnect = this.disconnect.bind(this, aReason); + } + return; + } + + if (this._outgoingEnabled && !this._disconnectTimer) { + // default reason is NS_OK + aReason = !aReason ? Cr.NS_OK : aReason; + + this._stateMachine.onChannelClosed(aReason, false); + + // Start a guard timer to ensure the transport will be closed. + this._disconnectTimer = setTimeout(() => { + DEBUG && log("TCPControlChannel - disconnect timeout"); // jshint ignore:line + this._closeTransport(); + }, kDisconnectTimeout); + } + }, + + reconnect: function(aPresentationId, aUrl) { + DEBUG && log("TCPControlChannel - reconnect with role: " + + this._direction); // jshint ignore:line + if (this._direction != "sender") { + return Cr.NS_ERROR_FAILURE; + } + + this._stateMachine.reconnect(aPresentationId, aUrl); + }, + + // callback from state machine + sendCommand: function(command) { + this._outgoingMsgs.push(command); + this._flushOutgoing(); + }, + + notifyDeviceConnected: function(deviceId) { + switch (this._direction) { + case "receiver": + this._deviceInfo.id = deviceId; + break; + } + this._notifyConnected(); + }, + + notifyDisconnected: function(reason) { + this._closeTransport(); + this._notifyDisconnected(reason); + }, + + notifyLaunch: function(presentationId, url) { + switch (this._direction) { + case "receiver": + this._presentationService.onSessionRequest(this._deviceInfo, + url, + presentationId, + this); + break; + } + }, + + notifyTerminate: function(presentationId) { + if (!this._terminatingId) { + this._terminatingId = presentationId; + this._presentationService.onSessionTerminate(this._deviceInfo, + presentationId, + this, + this._direction === "sender"); + return; + } + + // Cancel terminate guard timer after receiving terminate-ack. + if (this._terminateTimer) { + clearTimeout(this._terminateTimer); + delete this._terminateTimer; + } + + if (this._terminatingId !== presentationId) { + // Requested presentation Id doesn't matched with the one in ACK. + // Disconnect the control channel with error. + DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line + this.disconnect(Cr.NS_ERROR_FAILURE); + } + + delete this._terminatingId; + if (this._pendingDisconnect) { + this._pendingDisconnect(); + } + }, + + notifyReconnect: function(presentationId, url) { + switch (this._direction) { + case "receiver": + this._presentationService.onSessionReconnect(this._deviceInfo, + url, + presentationId, + this); + break; + case "sender": + this._notifyReconnected(); + break; + } + }, + + notifyOffer: function(offer) { + this._onOffer(offer); + }, + + notifyAnswer: function(answer) { + this._onAnswer(answer); + }, + + notifyIceCandidate: function(candidate) { + this._listener.onIceCandidate(candidate); + }, + + classID: Components.ID("{fefb8286-0bdc-488b-98bf-0c11b485c955}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel, + Ci.nsIStreamListener]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationControlService]); // jshint ignore:line diff --git a/dom/presentation/provider/PresentationDeviceProviderModule.cpp b/dom/presentation/provider/PresentationDeviceProviderModule.cpp new file mode 100644 index 000000000..9100fa49b --- /dev/null +++ b/dom/presentation/provider/PresentationDeviceProviderModule.cpp @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "DisplayDeviceProvider.h" +#include "MulticastDNSDeviceProvider.h" +#include "mozilla/ModuleUtils.h" + +#ifdef MOZ_WIDGET_ANDROID +#include "LegacyMDNSDeviceProvider.h" +#endif //MOZ_WIDGET_ANDROID + +#define MULTICAST_DNS_PROVIDER_CID \ + {0x814f947a, 0x52f7, 0x41c9, \ + { 0x94, 0xa1, 0x36, 0x84, 0x79, 0x72, 0x84, 0xac }} +#define DISPLAY_DEVICE_PROVIDER_CID \ + { 0x515d9879, 0xfe0b, 0x4d9f, \ + { 0x89, 0x49, 0x7f, 0xa7, 0x65, 0x6c, 0x01, 0x0e } } + +#ifdef MOZ_WIDGET_ANDROID +#define LEGACY_MDNS_PROVIDER_CID \ + { 0x6885ff39, 0xd98c, 0x4356, \ + { 0x9e, 0xb3, 0x56, 0x56, 0x31, 0x63, 0x0a, 0xf6 } } +#endif //MOZ_WIDGET_ANDROID + +#define DISPLAY_DEVICE_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/displaydevice-provider;1" +#define MULTICAST_DNS_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/multicastdns-provider;1" + +#ifdef MOZ_WIDGET_ANDROID +#define LEGACY_MDNS_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/legacy-mdns-provider;1" +#endif //MOZ_WIDGET_ANDROID + +using mozilla::dom::presentation::MulticastDNSDeviceProvider; +using mozilla::dom::presentation::DisplayDeviceProvider; + +#ifdef MOZ_WIDGET_ANDROID +using mozilla::dom::presentation::legacy::LegacyMDNSDeviceProvider; +#endif //MOZ_WIDGET_ANDROID + +NS_GENERIC_FACTORY_CONSTRUCTOR(MulticastDNSDeviceProvider) +NS_DEFINE_NAMED_CID(MULTICAST_DNS_PROVIDER_CID); + +NS_GENERIC_FACTORY_CONSTRUCTOR(DisplayDeviceProvider) +NS_DEFINE_NAMED_CID(DISPLAY_DEVICE_PROVIDER_CID); + +#ifdef MOZ_WIDGET_ANDROID +NS_GENERIC_FACTORY_CONSTRUCTOR(LegacyMDNSDeviceProvider) +NS_DEFINE_NAMED_CID(LEGACY_MDNS_PROVIDER_CID); +#endif //MOZ_WIDGET_ANDROID + +static const mozilla::Module::CIDEntry kPresentationDeviceProviderCIDs[] = { + { &kMULTICAST_DNS_PROVIDER_CID, false, nullptr, MulticastDNSDeviceProviderConstructor }, + { &kDISPLAY_DEVICE_PROVIDER_CID, false, nullptr, DisplayDeviceProviderConstructor }, +#ifdef MOZ_WIDGET_ANDROID + { &kLEGACY_MDNS_PROVIDER_CID, false, nullptr, LegacyMDNSDeviceProviderConstructor }, +#endif //MOZ_WIDGET_ANDROID + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kPresentationDeviceProviderContracts[] = { + { MULTICAST_DNS_PROVIDER_CONTRACT_ID, &kMULTICAST_DNS_PROVIDER_CID }, + { DISPLAY_DEVICE_PROVIDER_CONTRACT_ID, &kDISPLAY_DEVICE_PROVIDER_CID }, +#ifdef MOZ_WIDGET_ANDROID + { LEGACY_MDNS_PROVIDER_CONTRACT_ID, &kLEGACY_MDNS_PROVIDER_CID }, +#endif //MOZ_WIDGET_ANDROID + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kPresentationDeviceProviderCategories[] = { +#if defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_ANDROID) || (defined(MOZ_WIDGET_GONK) && ANDROID_VERSION >= 16) + { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "MulticastDNSDeviceProvider", MULTICAST_DNS_PROVIDER_CONTRACT_ID }, +#endif +#if defined(MOZ_WIDGET_GONK) + { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "DisplayDeviceProvider", DISPLAY_DEVICE_PROVIDER_CONTRACT_ID }, +#endif +#ifdef MOZ_WIDGET_ANDROID + { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "LegacyMDNSDeviceProvider", LEGACY_MDNS_PROVIDER_CONTRACT_ID }, +#endif //MOZ_WIDGET_ANDROID + { nullptr } +}; + +static const mozilla::Module kPresentationDeviceProviderModule = { + mozilla::Module::kVersion, + kPresentationDeviceProviderCIDs, + kPresentationDeviceProviderContracts, + kPresentationDeviceProviderCategories +}; + +NSMODULE_DEFN(PresentationDeviceProviderModule) = &kPresentationDeviceProviderModule; diff --git a/dom/presentation/provider/ReceiverStateMachine.jsm b/dom/presentation/provider/ReceiverStateMachine.jsm new file mode 100644 index 000000000..23ebb5a4e --- /dev/null +++ b/dom/presentation/provider/ReceiverStateMachine.jsm @@ -0,0 +1,238 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ +/* globals Components, dump */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ReceiverStateMachine"]; // jshint ignore:line + +const { utils: Cu } = Components; + +/* globals State, CommandType */ +Cu.import("resource://gre/modules/presentation/StateMachineHelper.jsm"); + +const DEBUG = false; +function debug(str) { + dump("-*- ReceiverStateMachine: " + str + "\n"); +} + +var handlers = [ + function _initHandler(stateMachine, command) { + // shouldn't receive any command at init state + DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line + }, + function _connectingHandler(stateMachine, command) { + switch (command.type) { + case CommandType.CONNECT: + stateMachine._sendCommand({ + type: CommandType.CONNECT_ACK + }); + stateMachine.state = State.CONNECTED; + stateMachine._notifyDeviceConnected(command.deviceId); + break; + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command + break; + } + }, + function _connectedHandler(stateMachine, command) { + switch (command.type) { + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + case CommandType.LAUNCH: + stateMachine._notifyLaunch(command.presentationId, + command.url); + stateMachine._sendCommand({ + type: CommandType.LAUNCH_ACK, + presentationId: command.presentationId + }); + break; + case CommandType.TERMINATE: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.TERMINATE_ACK: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.OFFER: + case CommandType.ICE_CANDIDATE: + stateMachine._notifyChannelDescriptor(command); + break; + case CommandType.RECONNECT: + stateMachine._notifyReconnect(command.presentationId, + command.url); + stateMachine._sendCommand({ + type: CommandType.RECONNECT_ACK, + presentationId: command.presentationId + }); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command + break; + } + }, + function _closingHandler(stateMachine, command) { + switch (command.type) { + case CommandType.DISCONNECT: + stateMachine.state = State.CLOSED; + stateMachine._notifyDisconnected(command.reason); + break; + default: + debug("unexpected command: " + JSON.stringify(command)); + // ignore unexpected command + break; + } + }, + function _closedHandler(stateMachine, command) { + // ignore every command in closed state. + DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line + }, +]; + +function ReceiverStateMachine(channel) { + this.state = State.INIT; + this._channel = channel; +} + +ReceiverStateMachine.prototype = { + launch: function _launch() { + // presentation session can only be launched by controlling UA. + debug("receiver shouldn't trigger launch"); + }, + + terminate: function _terminate(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE, + presentationId: presentationId, + }); + } + }, + + terminateAck: function _terminateAck(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE_ACK, + presentationId: presentationId, + }); + } + }, + + reconnect: function _reconnect() { + debug("receiver shouldn't trigger reconnect"); + }, + + sendOffer: function _sendOffer() { + // offer can only be sent by controlling UA. + debug("receiver shouldn't generate offer"); + }, + + sendAnswer: function _sendAnswer(answer) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.ANSWER, + answer: answer, + }); + } + }, + + updateIceCandidate: function _updateIceCandidate(candidate) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.ICE_CANDIDATE, + candidate: candidate, + }); + } + }, + + onCommand: function _onCommand(command) { + handlers[this.state](this, command); + }, + + onChannelReady: function _onChannelReady() { + if (this.state === State.INIT) { + this.state = State.CONNECTING; + } + }, + + onChannelClosed: function _onChannelClose(reason, isByRemote) { + switch (this.state) { + case State.CONNECTED: + if (isByRemote) { + this.state = State.CLOSED; + this._notifyDisconnected(reason); + } else { + this._sendCommand({ + type: CommandType.DISCONNECT, + reason: reason + }); + this.state = State.CLOSING; + this._closeReason = reason; + } + break; + case State.CLOSING: + if (isByRemote) { + this.state = State.CLOSED; + if (this._closeReason) { + reason = this._closeReason; + delete this._closeReason; + } + this._notifyDisconnected(reason); + } else { + // do nothing and wait for remote channel closed. + } + break; + default: + DEBUG && debug("unexpected channel close: " + reason + ", " + isByRemote); // jshint ignore:line + break; + } + }, + + _sendCommand: function _sendCommand(command) { + this._channel.sendCommand(command); + }, + + _notifyDeviceConnected: function _notifyDeviceConnected(deviceName) { + this._channel.notifyDeviceConnected(deviceName); + }, + + _notifyDisconnected: function _notifyDisconnected(reason) { + this._channel.notifyDisconnected(reason); + }, + + _notifyLaunch: function _notifyLaunch(presentationId, url) { + this._channel.notifyLaunch(presentationId, url); + }, + + _notifyTerminate: function _notifyTerminate(presentationId) { + this._channel.notifyTerminate(presentationId); + }, + + _notifyReconnect: function _notifyReconnect(presentationId, url) { + this._channel.notifyReconnect(presentationId, url); + }, + + _notifyChannelDescriptor: function _notifyChannelDescriptor(command) { + switch (command.type) { + case CommandType.OFFER: + this._channel.notifyOffer(command.offer); + break; + case CommandType.ICE_CANDIDATE: + this._channel.notifyIceCandidate(command.candidate); + break; + } + }, +}; + +this.ReceiverStateMachine = ReceiverStateMachine; // jshint ignore:line diff --git a/dom/presentation/provider/StateMachineHelper.jsm b/dom/presentation/provider/StateMachineHelper.jsm new file mode 100644 index 000000000..6e07863d4 --- /dev/null +++ b/dom/presentation/provider/StateMachineHelper.jsm @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["State", "CommandType"]; // jshint ignore:line + +const State = Object.freeze({ + INIT: 0, + CONNECTING: 1, + CONNECTED: 2, + CLOSING: 3, + CLOSED: 4, +}); + +const CommandType = Object.freeze({ + // control channel life cycle + CONNECT: "connect", // { deviceId: <string> } + CONNECT_ACK: "connect-ack", // { presentationId: <string> } + DISCONNECT: "disconnect", // { reason: <int> } + // presentation session life cycle + LAUNCH: "launch", // { presentationId: <string>, url: <string> } + LAUNCH_ACK: "launch-ack", // { presentationId: <string> } + TERMINATE: "terminate", // { presentationId: <string> } + TERMINATE_ACK: "terminate-ack", // { presentationId: <string> } + RECONNECT: "reconnect", // { presentationId: <string> } + RECONNECT_ACK: "reconnect-ack", // { presentationId: <string> } + // session transport establishment + OFFER: "offer", // { offer: <json> } + ANSWER: "answer", // { answer: <json> } + ICE_CANDIDATE: "ice-candidate", // { candidate: <string> } +}); + +this.State = State; // jshint ignore:line +this.CommandType = CommandType; // jshint ignore:line diff --git a/dom/presentation/provider/moz.build b/dom/presentation/provider/moz.build new file mode 100644 index 000000000..18428b50e --- /dev/null +++ b/dom/presentation/provider/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_COMPONENTS += [ + 'BuiltinProviders.manifest', + 'PresentationControlService.js' +] + +UNIFIED_SOURCES += [ + 'DeviceProviderHelpers.cpp', + 'DisplayDeviceProvider.cpp', + 'MulticastDNSDeviceProvider.cpp', + 'PresentationDeviceProviderModule.cpp', +] + +EXTRA_JS_MODULES.presentation += [ + 'ControllerStateMachine.jsm', + 'ReceiverStateMachine.jsm', + 'StateMachineHelper.jsm', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android': + EXTRA_COMPONENTS += [ + # For android presentation device + 'AndroidCastDeviceProvider.js', + 'AndroidCastDeviceProvider.manifest', + # for TV 2.5 device backward capability + 'LegacyPresentationControlService.js', + 'LegacyProviders.manifest', + ] + + UNIFIED_SOURCES += [ + 'LegacyMDNSDeviceProvider.cpp', + ] + +include('/ipc/chromium/chromium-config.mozbuild') +FINAL_LIBRARY = 'xul' diff --git a/dom/presentation/provider/nsTCPDeviceInfo.h b/dom/presentation/provider/nsTCPDeviceInfo.h new file mode 100644 index 000000000..118f6c8ac --- /dev/null +++ b/dom/presentation/provider/nsTCPDeviceInfo.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef __TCPDeviceInfo_h__ +#define __TCPDeviceInfo_h__ + +namespace mozilla { +namespace dom { +namespace presentation { + +class TCPDeviceInfo final : public nsITCPDeviceInfo +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSITCPDEVICEINFO + + explicit TCPDeviceInfo(const nsACString& aId, + const nsACString& aAddress, + const uint16_t aPort, + const nsACString& aCertFingerprint) + : mId(aId) + , mAddress(aAddress) + , mPort(aPort) + , mCertFingerprint(aCertFingerprint) + { + } + +private: + virtual ~TCPDeviceInfo() {} + + nsCString mId; + nsCString mAddress; + uint16_t mPort; + nsCString mCertFingerprint; +}; + +NS_IMPL_ISUPPORTS(TCPDeviceInfo, + nsITCPDeviceInfo) + +// nsITCPDeviceInfo +NS_IMETHODIMP +TCPDeviceInfo::GetId(nsACString& aId) +{ + aId = mId; + return NS_OK; +} + +NS_IMETHODIMP +TCPDeviceInfo::GetAddress(nsACString& aAddress) +{ + aAddress = mAddress; + return NS_OK; +} + +NS_IMETHODIMP +TCPDeviceInfo::GetPort(uint16_t* aPort) +{ + *aPort = mPort; + return NS_OK; +} + +NS_IMETHODIMP +TCPDeviceInfo::GetCertFingerprint(nsACString& aCertFingerprint) +{ + aCertFingerprint = mCertFingerprint; + return NS_OK; +} + +} // namespace presentation +} // namespace dom +} // namespace mozilla + +#endif /* !__TCPDeviceInfo_h__ */ + |