Bug 1120308 - tcp control channel for presentation api. r=fabrice
authorJunior Hsu <juhsu@mozilla.com>
Mon, 17 Nov 2014 13:34:10 -0800
changeset 266824 11960a87b91854cc352e02903631d021607e44e0
parent 266823 57d347aa07018e74ebaa0c6e129f924c3a17270b
child 266825 f5782f959c94f016afe66ff4523c4a8227e9fb53
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice
bugs1120308
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1120308 - tcp control channel for presentation api. r=fabrice
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/presentation/interfaces/moz.build
dom/presentation/interfaces/nsIPresentationControlChannel.idl
dom/presentation/interfaces/nsIPresentationDevice.idl
dom/presentation/interfaces/nsITCPPresentationServer.idl
dom/presentation/moz.build
dom/presentation/provider/BuiltinProviders.manifest
dom/presentation/provider/TCPPresentationServer.js
dom/presentation/provider/moz.build
dom/presentation/tests/xpcshell/test_tcp_control_channel.js
dom/presentation/tests/xpcshell/xpcshell.ini
mobile/android/installer/package-manifest.in
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -425,16 +425,19 @@
 @RESPATH@/components/nsSidebar.manifest
 @RESPATH@/components/nsSidebar.js
 @RESPATH@/components/nsAsyncShutdown.manifest
 @RESPATH@/components/nsAsyncShutdown.js
 @RESPATH@/components/htmlMenuBuilder.js
 @RESPATH@/components/htmlMenuBuilder.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.js
+@RESPATH@/components/BuiltinProviders.manifest
+@RESPATH@/components/TCPPresentationServer.js
+
 #ifdef MOZ_SECUREELEMENT
 @RESPATH@/components/SecureElement.js
 @RESPATH@/components/SecureElement.manifest
 @RESPATH@/components/UiccConnector.js
 @RESPATH@/components/UiccConnector.manifest
 #endif
 
 ; WiFi, NetworkManager, NetworkStats
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -604,16 +604,18 @@
 @RESPATH@/components/dom_webspeechsynth.xpt
 #endif
 
 @RESPATH@/components/nsAsyncShutdown.manifest
 @RESPATH@/components/nsAsyncShutdown.js
 
 @RESPATH@/components/PresentationDeviceInfoManager.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.js
+@RESPATH@/components/BuiltinProviders.manifest
+@RESPATH@/components/TCPPresentationServer.js
 
 ; InputMethod API
 @RESPATH@/components/MozKeyboard.js
 @RESPATH@/components/InputMethod.manifest
 
 #ifdef MOZ_DEBUG
 @RESPATH@/components/TestInterfaceJS.js
 @RESPATH@/components/TestInterfaceJS.manifest
--- a/dom/presentation/interfaces/moz.build
+++ b/dom/presentation/interfaces/moz.build
@@ -6,12 +6,13 @@
 
 XPIDL_SOURCES += [
     'nsIPresentationControlChannel.idl',
     'nsIPresentationDevice.idl',
     'nsIPresentationDeviceManager.idl',
     'nsIPresentationDevicePrompt.idl',
     'nsIPresentationDeviceProvider.idl',
     'nsIPresentationSessionRequest.idl',
+    'nsITCPPresentationServer.idl',
 ]
 
 XPIDL_MODULE = 'dom_presentation'
 
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -69,23 +69,25 @@ interface nsIPresentationControlChannel:
   // The listener for handling events of this control channel.
   // All the events should be pending until listener is assigned.
   attribute nsIPresentationControlChannelListener listener;
 
   /*
    * Send offer to remote endpiont. |onOffer| should be invoked
    * on remote endpoint.
    * @param offer The offer to send.
+   * @throws  NS_ERROR_FAILURE on failure
    */
   void sendOffer(in nsIPresentationChannelDescription offer);
 
   /*
    * Send answer to remote endpiont. |onAnswer| should
    * be invoked on remote endpoint.
    * @param answer The answer to send.
+   * @throws  NS_ERROR_FAILURE on failure
    */
   void sendAnswer(in nsIPresentationChannelDescription answer);
 
   /*
    * Close the transport channel.
    */
   void close();
 };
--- a/dom/presentation/interfaces/nsIPresentationDevice.idl
+++ b/dom/presentation/interfaces/nsIPresentationDevice.idl
@@ -43,13 +43,14 @@ interface nsIPresentationDevice : nsISup
 
   // The listener for handling remote session request.
   attribute nsIPresentationDeviceEventListener listener;
 
   /*
    * Establish a control channel to this device.
    * @param url The URL requested to open by remote device.
    * @param presentationId The Id for representing this session.
-   * @return The control channel for this session.
+   * @returns The control channel for this session.
+   * @throws  NS_ERROR_FAILURE if the establishment fails
    */
   nsIPresentationControlChannel establishControlChannel(in DOMString url,
                                                         in DOMString presentationId);
 };
new file mode 100644
--- /dev/null
+++ b/dom/presentation/interfaces/nsITCPPresentationServer.idl
@@ -0,0 +1,103 @@
+/* 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 "nsISupports.idl"
+
+interface nsIPresentationDevice;
+
+[scriptable, uuid(b0dc6b1f-5f6f-455d-a917-90d0ad37186b)]
+interface nsITCPPresentationServerListener: nsISupports
+{
+  /**
+   * Callback while the server socket stops listening.
+   * @param   aReason
+   *          The reason of the socket close. NS_OK for manually |close|.
+   *          <other-error> on failure.
+   */
+  void onClose(in nsresult aReason);
+};
+
+/**
+ * TCP presentation server which can be used by discovery services.
+ */
+[scriptable, uuid(4fc57682-33d5-4793-b149-e2cc4714d70f)]
+interface nsITCPPresentationServer: nsISupports
+{
+  /**
+   * This method initializes a TCP presentation server.
+   * @param   aId
+   *          The unique Id for the device within the discovery scope. If aId
+   *          is null, empty string or opt-out, the TCP presentation server
+   *          should not work until the |id| is set appropriately.
+   * @param   aPort
+   *          The port of the server socket.  Pass 0 or opt-out to indicate no
+   *          preference, and a port will be selected automatically.
+   * @throws  NS_ERROR_FAILURE if the server socket has been inited or the
+   *          server socket can not be inited.
+   */
+  void init([optional] in AUTF8String aId, [optional] in uint16_t aPort);
+
+  /**
+   * Close server socket and call |listener.onClose(NS_OK)|
+   */
+  void close();
+
+  /**
+   * Create TCPDevice for this server.
+   * @param   aId
+   *          The unique Id for the discovered device
+   * @param   aName
+   *          The human-readable name of the discovered device
+   * @param   aType
+   *          The category of the discovered device
+   * @param   aHost
+   *          The host of the provided control channel of the discovered device
+   * @param   aPort
+   *          The port of the provided control channel of the discovered device
+   * @returns The created device
+   * @throws  NS_ERROR_INVALID_ARG if a TCPDevice with |aId| have existed.
+   */
+  nsIPresentationDevice createTCPDevice(in AUTF8String aId,
+                                        in AUTF8String aName,
+                                        in AUTF8String aType,
+                                        in AUTF8String aHost,
+                                        in uint16_t aPort);
+
+  /**
+   * Get TCPDevice with |aID|.
+   * @param   aId
+   *          The unique Id for the query device
+   * @returns The queried device; return |undefined|
+   * @throws  NS_ERROR_INVALID_ARG if a TCPDevice with |aId| does not exist.
+   */
+  nsIPresentationDevice getTCPDevice(in AUTF8String aId);
+
+  /**
+   * Remove TCPDevice with |aID|.
+   * @param   aId
+   *          The unique Id for the device which needs to be removed
+   * @throws  NS_ERROR_INVALID_ARG if a TCPDevice with |aId| does not exist.
+   */
+  void removeTCPDevice(in AUTF8String aId);
+
+  /**
+   * Get the listen port of the TCP socket, valid after |init|. 0 indicates
+   * the server socket is not inited or closed.
+   */
+  readonly attribute uint16_t port;
+
+  /**
+   * The id of the TCP presentation server. The setter should be use if the |id|
+   * is not set by the |init|. Moreover, if the |id| is not set by |init|, the
+   * TCP presentation server should not work until the |id| is set.
+   * @throws  NS_ERROR_FAILURE if the non-null id has been set by |init| or this
+   *          setter
+   */
+  attribute AUTF8String id;
+
+  /**
+   * the listener for handling events of this TCP presentation server
+   */
+  attribute nsITCPPresentationServerListener listener;
+};
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -1,15 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; 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/.
 
-DIRS += ['interfaces']
+DIRS += ['interfaces', 'provider']
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
 
 EXPORTS.mozilla.dom.presentation += [
     'PresentationDeviceManager.h',
 ]
 
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/BuiltinProviders.manifest
@@ -0,0 +1,2 @@
+component {f4079b8b-ede5-4b90-a112-5b415a931deb} TCPPresentationServer.js
+contract @mozilla.org/presentation-device/tcp-presentation-server;1 {f4079b8b-ede5-4b90-a112-5b415a931deb}
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/TCPPresentationServer.js
@@ -0,0 +1,712 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const DEBUG = false;
+function log(aMsg) {
+  dump("-*- TCPPresentationServer.js: " + aMsg + "\n");
+}
+
+function TCPDeviceInfo(aHost, aPort, aId, aName, aType) {
+  this.host = aHost;
+  this.port = aPort;
+  this.id = aId;
+  this.name = aName;
+  this.type = aType;
+}
+
+function TCPPresentationServer() {
+  this._id = null;
+  this._port = 0;
+  this._serverSocket = null;
+  this._devices = new Map(); // id -> device
+}
+
+TCPPresentationServer.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: [],
+
+  init: function(aId, aPort) {
+    if (this._isInit()) {
+      DEBUG && log("TCPPresentationServer - server socket has been initialized");
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    if (typeof aId === "undefined" || typeof aPort === "undefined") {
+      DEBUG && log("TCPPresentationServer - aId/aPort should not be undefined");
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    DEBUG && log("TCPPresentationServer - init id: " + aId + " port: " + aPort);
+
+    /**
+     * 0 or undefined indicates opt-out parameter, and a port will be selected
+     * automatically.
+     */
+    let serverSocketPort = (aPort !== 0) ? aPort : -1;
+
+    this._serverSocket = Cc["@mozilla.org/network/server-socket;1"]
+                         .createInstance(Ci.nsIServerSocket);
+    try {
+      this._serverSocket.init(serverSocketPort, false, -1);
+    } catch (e) {
+      // NS_ERROR_SOCKET_ADDRESS_IN_USE
+      DEBUG && log("TCPPresentationServer - init server socket fail: " + e);
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    /**
+     * The setter may trigger |_serverSocket.asyncListen| if the |id| setting
+     * successes.
+     */
+    this.id = aId;
+    this._port = this._serverSocket.port;
+  },
+
+  get id() {
+    return this._id;
+  },
+
+  set id(aId) {
+    if (!aId || aId.length == 0 || aId === this._id) {
+      return;
+    } else if (this._id) {
+      throw Cr.NS_ERROR_FAILURE;
+    }
+    this._id = aId;
+
+    if (this._serverSocket) {
+      this._serverSocket.asyncListen(this);
+    }
+  },
+
+  get port() {
+    return this._port;
+  },
+
+  set listener(aListener) {
+    this._listener = aListener;
+  },
+
+  get listener() {
+    return this._listener;
+  },
+
+  _isInit: function() {
+    return this._id !== null && this._serverSocket !== null;
+  },
+
+  close: function() {
+    DEBUG && log("TCPPresentationServer - close");
+    if (this._serverSocket) {
+      this._serverSocket.close();
+    }
+    this._id = null;
+    this._port = 0;
+  },
+
+  createTCPDevice: function(aId, aName, aType, aHost, aPort) {
+    if (this._devices.has(aId)) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    this._devices.set(aId, new TCPDevice(this, {id: aId,
+                                                name: aName,
+                                                type: aType,
+                                                host: aHost,
+                                                port: aPort}));
+    return this._devices.get(aId);
+  },
+
+  getTCPDevice: function(aId) {
+    if (!this._devices.has(aId)) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+    return this._devices.get(aId);
+  },
+
+  removeTCPDevice: function(aId) {
+    if (!this._devices.has(aId)) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+    this._devices.delete(aId);
+  },
+
+  requestSession: function(aDevice, aUrl, aPresentationId) {
+    if (!this._isInit()) {
+      DEBUG && log("TCPPresentationServer - has not initialized; requestSession fails");
+      return null;
+    }
+    DEBUG && log("TCPPresentationServer - requestSession to " + aDevice.name
+                 + ": " + aUrl + ", " + aPresentationId);
+
+    let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+                .getService(Ci.nsISocketTransportService)
+
+    let socketTransport;
+    try {
+      socketTransport = sts.createTransport(null,
+                                            0,
+                                            aDevice.host,
+                                            aDevice.port,
+                                            null);
+    } catch (e) {
+      DEBUG && log("TCPPresentationServer - createTransport throws: " + e);
+      // Pop the exception to |TCPDevice.establishControlChannel|
+      throw Cr.NS_ERROR_FAILURE;
+    }
+    return new TCPControlChannel(this,
+                                 socketTransport,
+                                 aDevice,
+                                 aPresentationId,
+                                 "sender",
+                                 aUrl);
+  },
+
+  responseSession: function(aDevice, aSocketTransport) {
+    if (!this._isInit()) {
+      DEBUG && log("TCPPresentationServer - has not initialized; responseSession fails");
+      return null;
+    }
+    DEBUG && log("TCPPresentationServer - responseSession to "
+                 + JSON.stringify(aDevice));
+    return new TCPControlChannel(this,
+                                 aSocketTransport,
+                                 aDevice,
+                                 null, // presentation ID
+                                 "receiver",
+                                 null // url
+                                 );
+  },
+
+  // Triggered by TCPControlChannel
+  onSessionRequest: function(aId, aUrl, aPresentationId, aControlChannel) {
+    let device = this._devices.get(aId);
+    if (!device) {
+      //XXX Bug 1136565 - should have a way to recovery
+      DEBUG && log("TCPPresentationServer - onSessionRequest not found device for id: "
+                   + aId );
+      return;
+    }
+    device.listener.onSessionRequest(device,
+                                     aUrl,
+                                     aPresentationId,
+                                     aControlChannel);
+    this.releaseControlChannel(aControlChannel);
+  },
+
+  // nsIServerSocketListener (Triggered by nsIServerSocket.init)
+  onSocketAccepted: function(aServerSocket, aClientSocket) {
+    DEBUG && log("TCPPresentationServer - onSocketAccepted: "
+                 + aClientSocket.host + ":" + aClientSocket.port);
+    let device = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
+    this.holdControlChannel(this.responseSession(device, 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("TCPPresentationServer - onStopListening: " + aStatus);
+
+    // manually closed
+    if (aStatus === Cr.NS_BINDING_ABORTED) {
+      aStatus = Cr.NS_OK;
+    }
+    this._listener && this._listener.onClose(aStatus);
+    this._serverSocket = null;
+  },
+
+  close: function() {
+    DEBUG && log("TCPPresentationServer - close signalling channel");
+    if (this._serverSocket) {
+      this._serverSocket.close();
+      this._serverSocket = null;
+    }
+    this._id = null;
+    this._port = 0;
+    this._devices && this._devices.clear();
+    this._devices = null;
+  },
+
+  classID: Components.ID("{f4079b8b-ede5-4b90-a112-5b415a931deb}"),
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsIServerSocketListener,
+                                          Ci.nsITCPPresentationServer]),
+};
+
+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-string;1"]
+                      .createInstance(Ci.nsISupportsString);
+        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.nsISupportsString);
+        json.tcpAddress.push(address.data);
+      }
+      json.tcpPort = aDescription.tcpPort;
+      break;
+    case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+      json.dataChannelSDP = aDescription.dataChannelSDP;
+      break;
+  }
+  return json;
+}
+
+function TCPControlChannel(presentationServer,
+                           transport,
+                           device,
+                           presentationId,
+                           direction,
+                           url) {
+  DEBUG && log("create TCPControlChannel: " + presentationId + " with role: "
+               + direction);
+  this._device = device;
+  this._presentationId = presentationId;
+  this._direction = direction;
+  this._transport = transport;
+  this._url = url;
+
+  this._presentationServer =  presentationServer;
+
+  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);
+
+  // Since the transport created by server socket is already CONNECTED_TO
+  if (this._direction === "receiver") {
+    this._createInputStreamPump();
+  }
+}
+
+TCPControlChannel.prototype = {
+  _connected: false,
+  _pendingOpen: false,
+  _pendingOffer: null,
+  _pendingAnswer: null,
+  _pendingClose: null,
+  _pendingCloseReason: null,
+  _sendingMessageType: null,
+
+  _sendMessage: function(aType, aJSONData, aOnThrow) {
+    if (!aOnThrow) {
+      aOnThrow = function(e) {throw e.result;}
+    }
+
+    if (!aType || !aJSONData) {
+      aOnThrow();
+      return;
+    }
+
+    if (!this._connected) {
+      DEBUG && log("TCPControlChannel - send" + aType + " fails");
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    DEBUG && log("TCPControlChannel - send" + aType + ": "
+                 + JSON.stringify(aJSONData));
+    try {
+      this._send(aJSONData);
+    } catch (e) {
+      aOnThrow(e);
+    }
+    this._sendingMessageType = aType;
+  },
+
+  _sendInit: function() {
+    let msg = {
+      type: "requestSession:Init",
+      presentationId: this._presentationId,
+      url: this._url,
+      id: this._presentationServer.id,
+    };
+
+    this._sendMessage("init", msg, function(e) {
+      this.close();
+      this._notifyClosed(e.result);
+    });
+  },
+
+  sendOffer: function(aOffer) {
+    let msg = {
+      type: "requestSession:Offer",
+      presentationId: this.presentationId,
+      offer: discriptionAsJson(aOffer),
+    };
+    this._sendMessage("offer", msg);
+  },
+
+  sendAnswer: function(aAnswer) {
+    let msg = {
+      type: "requestSession:Answer",
+      presentationId: this.presentationId,
+      answer: discriptionAsJson(aAnswer),
+    };
+    this._sendMessage("answer", msg);
+  },
+
+  // may throw an exception
+  _send: function(aMsg) {
+    DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2));
+
+    /**
+     * 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);
+      throw e;
+    }
+  },
+
+  // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait)
+  // Only used for detecting connection refused
+  onInputStreamReady: function(aStream) {
+    try {
+      aStream.available();
+    } catch (e) {
+      DEBUG && log("TCPControlChannel - onInputStreamReady error: " + e.name);
+      // NS_ERROR_CONNECTION_REFUSED
+      this._listener.notifyClosed(e.result);
+    }
+  },
+
+  // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink)
+  onTransportStatus: function(aTransport, aStatus, aProg, aProgMax) {
+    DEBUG && log("TCPControlChannel - onTransportStatus: "
+                 + aStatus.toString(16) + " with role: " + this._direction);
+    if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+      this._connected = true;
+
+      if (!this._pump) {
+        this._createInputStreamPump();
+      }
+
+      if (this._direction === "sender") {
+        this._sendInit();
+      }
+    } else if (aStatus === Ci.nsISocketTransport.STATUS_SENDING_TO) {
+      if (this._sendingMessageType === "init") {
+        this._notifyOpened();
+      }
+      this._sendingMessageType = null;
+    }
+  },
+
+  // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+  onStartRequest: function() {
+    DEBUG && log("TCPControlChannel - onStartRequest with role: "
+                 + this._direction);
+  },
+
+  // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+  onStopRequest: function(aRequest, aContext, aStatus) {
+    DEBUG && log("TCPControlChannel - onStopRequest: " + aStatus
+                 + " with role: " + this._direction);
+    this.close();
+    this._notifyClosed(aStatus);
+  },
+
+  // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead)
+  onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
+    let data = NetUtil.readInputStreamToString(aInputStream,
+                                               aInputStream.available());
+    DEBUG && log("TCPControlChannel - onDataAvailable: " + data);
+
+    // 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);
+      }
+
+      this._handleMessage(msg);
+    }
+  },
+
+  _createInputStreamPump: function() {
+    DEBUG && log("TCPControlChannel - create pump with role: "
+                 + this._direction);
+    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("TCPControlChannel - handleMessage from "
+                 + JSON.stringify(this._device) + ": " + JSON.stringify(aMsg));
+    switch (aMsg.type) {
+      case "requestSession:Init": {
+        this._device.id = aMsg.id;
+        this._url = aMsg.url;
+        this._presentationId = aMsg.presentationId;
+        this._presentationServer.onSessionRequest(aMsg.id,
+                                                  aMsg.url,
+                                                  aMsg.presentationId,
+                                                  this);
+        this._notifyOpened();
+        break;
+      }
+      case "requestSession:Offer": {
+        this._listener.onOffer(new ChannelDescription(aMsg.offer));
+        break;
+      }
+      case "requestSession:Answer": {
+        this._listener.onAnswer(new ChannelDescription(aMsg.answer));
+        break;
+      }
+    }
+  },
+
+  get listener() {
+    return this._listener;
+  },
+
+  set listener(aListener) {
+    DEBUG && log("TCPControlChannel - set listener: " + aListener);
+    if (!aListener) {
+      this._listener = null;
+      return;
+    }
+
+    this._listener = aListener;
+    if (this._pendingOpen) {
+      this._pendingOpen = false;
+      DEBUG && log("TCPControlChannel - notify pending opened");
+      this._listener.notifyOpened();
+    }
+
+    if (this._pendingOffer) {
+      let offer = this._pendingOffer;
+      DEBUG && log("TCPControlChannel - notify pending offer: "
+                   + JSON.stringify(offer));
+      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));
+      this._listener._onAnswer(new ChannelDescription(answer));
+      this._pendingAnswer = null;
+    }
+
+    if (this._pendingClose) {
+      DEBUG && log("TCPControlChannel - notify pending closed");
+      this._notifyClosed(this._pendingCloseReason);
+      this._pendingClose = null;
+    }
+  },
+
+  /**
+   * These functions are designed to handle the interaction with listener
+   * appropriately. |_FUNC| is to handle |this._listener.FUNC|.
+   */
+  _onOffer: function(aOffer) {
+    if (!this._connected) {
+      return;
+    }
+    if (!this._listener) {
+      this._pendingOffer = offer;
+      return;
+    }
+    DEBUG && log("TCPControlChannel - notify offer: "
+                 + JSON.stringify(aOffer));
+    this._listener.onOffer(new ChannelDescription(aOffer));
+  },
+
+  _onAnswer: function(aAnswer) {
+    if (!this._connected) {
+      return;
+    }
+    if (!this._listener) {
+      this._pendingAnswer = aAnswer;
+      return;
+    }
+    DEBUG && log("TCPControlChannel - notify answer: "
+                 + JSON.stringify(aAnswer));
+    this._listener.onAnswer(new ChannelDescription(aAnswer));
+  },
+
+  _notifyOpened: function() {
+    this._connected = true;
+    this._pendingClose = false;
+    this._pendingCloseReason = null;
+
+    if (!this._listener) {
+      this._pendingOpen = true;
+      return;
+    }
+
+    DEBUG && log("TCPControlChannel - notify opened with role: "
+                 + this._direction);
+    this._listener.notifyOpened();
+  },
+
+  _notifyClosed: function(aReason) {
+    this._connected = false;
+    this._pendingOpen = false;
+    this._pendingOffer = null;
+    this._pendingAnswer = null;
+
+    if (!this._listener) {
+     this._pendingClose = true;
+     this._pendingCloseReason = aReason;
+     return;
+    }
+
+    DEBUG && log("TCPControlChannel - notify closed with role: "
+                 + this._direction);
+    this._listener.notifyClosed(aReason);
+  },
+
+  close: function() {
+    DEBUG && log("TCPControlChannel - close");
+    if (this._connected) {
+      this._transport.setEventSink(null, null);
+      this._pump = null;
+
+      this._input.close();
+      this._output.close();
+      this._presentationServer.releaseControlChannel(this);
+
+      this._connected = false;
+    }
+  },
+
+  classID: Components.ID("{fefb8286-0bdc-488b-98bf-0c11b485c955}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel,
+                                         Ci.nsIStreamListener]),
+};
+
+function TCPDevice(aPresentationServer, aInfo) {
+  DEBUG && log("create TCPDevice");
+  this.id = aInfo.id;
+  this.name = aInfo.name;
+  this.type = aInfo.type
+  this.host = aInfo.host;
+  this.port = aInfo.port;
+
+  this._presentationServer = aPresentationServer;
+  this._listener = null;
+}
+
+TCPDevice.prototype = {
+  establishControlChannel: function(aUrl, aPresentationId) {
+    DEBUG && log("TCPDevice - establishControlChannel: " + aUrl + ", "
+                 + aPresentationId);
+    return this._presentationServer
+               .requestSession(this._getDeviceInfo(), aUrl, aPresentationId);
+  },
+  get listener() {
+    return this._listener;
+  },
+  set listener(aListener) {
+    DEBUG && log("TCPDevice - set listener");
+    this._listener = aListener;
+  },
+
+  _getDeviceInfo: function() {
+    return new TCPDeviceInfo(this.host,
+                             this.port,
+                             this.id,
+                             this.name,
+                             this.type);
+  },
+
+  classID: Components.ID("{d6492549-a4f2-4a0c-9a93-00f0e9918b0a}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TCPPresentationServer]);
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; 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',
+    'TCPPresentationServer.js'
+]
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -0,0 +1,188 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+let tps;
+
+// Call |run_next_test| if all functions in |names| are called
+function makeJointSuccess(names) {
+  let funcs = {}, successCount = 0;
+  names.forEach(function(name) {
+    funcs[name] = function() {
+      do_print('got expected: ' + name);
+      if (++successCount === names.length)
+        run_next_test();
+    };
+  });
+  return funcs;
+}
+
+function TestDescription(aType, aTcpAddress, aTcpPort) {
+  this.type = aType;
+  this.tcpAddress = Cc["@mozilla.org/array;1"]
+                      .createInstance(Ci.nsIMutableArray);
+  for (let address of aTcpAddress) {
+    let wrapper = Cc["@mozilla.org/supports-string;1"]
+                    .createInstance(Ci.nsISupportsString);
+    wrapper.data = address;
+    this.tcpAddress.appendElement(wrapper, false);
+  }
+  this.tcpPort = aTcpPort;
+}
+
+TestDescription.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+}
+
+function loopOfferAnser()
+{
+  const CONTROLLER_CONTROL_CHANNEL_PORT = 36777;
+  const PRESENTER_CONTROL_CHANNEL_PORT = 36888;
+
+  // presenter's presentation channel description
+  const OFFER_ADDRESS = '192.168.123.123';
+  const OFFER_PORT = 123;
+
+  // controller's presentation channel description
+  const ANSWER_ADDRESS = '192.168.321.321';
+  const ANSWER_PORT = 321;
+
+  let yayFuncs = makeJointSuccess(['controllerControlChannelClose',
+                                   'presenterControlChannelClose']);
+  let controllerDevice, controllerControlChannel;
+  let presenterDevice, presenterControlChannel;
+
+  tps = Cc["@mozilla.org/presentation-device/tcp-presentation-server;1"]
+        .createInstance(Ci.nsITCPPresentationServer);
+  tps.init(null, PRESENTER_CONTROL_CHANNEL_PORT);
+  tps.id = 'controllerID';
+  controllerDevice = tps.createTCPDevice('controllerID',
+                                         'controllerName',
+                                         'testType',
+                                         '127.0.0.1',
+                                         CONTROLLER_CONTROL_CHANNEL_PORT)
+                        .QueryInterface(Ci.nsIPresentationDevice);
+
+  controllerDevice.listener = {
+
+    onSessionRequest: function(device, url, presentationId, controlChannel) {
+      controllerControlChannel = controlChannel;
+      Assert.strictEqual(device, controllerDevice, 'expected device object');
+      Assert.equal(url, 'http://example.com', 'expected url');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+
+      controllerControlChannel.listener = {
+        status: 'created',
+        onOffer: function(aOffer) {
+          Assert.equal(this.status, 'opened', '1. controllerControlChannel: get offer, send answer');
+          this.status = 'onOffer';
+
+          let offer = aOffer.QueryInterface(Ci.nsIPresentationChannelDescription);
+          Assert.strictEqual(offer.tcpAddress.queryElementAt(0,Ci.nsISupportsString).data,
+                             OFFER_ADDRESS,
+                             'expected offer address array');
+          Assert.equal(offer.tcpPort, OFFER_PORT, 'expected offer port');
+          try {
+            let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+            let answer = new TestDescription(tcpType, [ANSWER_ADDRESS], ANSWER_PORT);
+            controllerControlChannel.sendAnswer(answer);
+          } catch (e) {
+            Assert.ok(false, 'sending answer fails' + e);
+          }
+        },
+        onAnswer: function(aAnswer) {
+          Assert.ok(false, 'get answer');
+        },
+        notifyOpened: function() {
+          Assert.equal(this.status, 'created', '0. controllerControlChannel: opened');
+          this.status = 'opened';
+        },
+        notifyClosed: function(aReason) {
+          Assert.equal(this.status, 'onOffer', '3. controllerControlChannel: closed');
+          Assert.equal(aReason, Cr.NS_OK, 'presenterControlChannel notify closed NS_OK');
+          this.status = 'closed';
+          yayFuncs.controllerControlChannelClose();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceEventListener]),
+      };
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+
+  presenterDevice = tps.createTCPDevice('presentatorID',
+                                        'presentatorName',
+                                        'testType',
+                                        '127.0.0.1',
+                                        PRESENTER_CONTROL_CHANNEL_PORT)
+                       .QueryInterface(Ci.nsIPresentationDevice);
+
+  presenterDevice.listener = {
+    onSessionRequest: function(device, url, presentationId, controlChannel) {
+      Assert.ok(false, 'presenterDevice.listener.onSessionRequest should not be called');
+    },
+  }
+
+  presenterControlChannel =
+  presenterDevice.establishControlChannel('http://example.com', 'testPresentationId');
+
+  presenterControlChannel.listener = {
+    status: 'created',
+    onOffer: function(offer) {
+      Assert.ok(false, 'get offer');
+    },
+    onAnswer: function(aAnswer) {
+      Assert.equal(this.status, 'opened', '2. presenterControlChannel: get answer, close channel');
+
+      let answer = aAnswer.QueryInterface(Ci.nsIPresentationChannelDescription);
+      Assert.strictEqual(answer.tcpAddress.queryElementAt(0,Ci.nsISupportsString).data,
+                         ANSWER_ADDRESS,
+                         'expected answer address array');
+      Assert.equal(answer.tcpPort, ANSWER_PORT, 'expected answer port');
+
+      presenterControlChannel.close(Cr.NS_OK);
+    },
+    notifyOpened: function() {
+      Assert.equal(this.status, 'created', '0. presenterControlChannel: opened, send offer');
+      this.status = 'opened';
+      try {
+        let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+        let offer = new TestDescription(tcpType, [OFFER_ADDRESS], OFFER_PORT)
+        presenterControlChannel.sendOffer(offer);
+      } catch (e) {
+        Assert.ok(false, 'sending offer fails:' + e);
+      }
+    },
+    notifyClosed: function(aReason) {
+      this.status = 'closed';
+      Assert.equal(aReason, Cr.NS_OK, '3. presenterControlChannel notify closed NS_OK');
+      yayFuncs.presenterControlChannelClose();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function shutdown()
+{
+  tps.listener = {
+    onClose: function(aReason) {
+      Assert.equal(aReason, Cr.NS_OK, 'TCPPresentationServer close success');
+      run_next_test();
+    },
+  }
+  tps.close();
+}
+
+add_test(loopOfferAnser);
+add_test(shutdown);
+
+function run_test() {
+  run_next_test();
+}
--- a/dom/presentation/tests/xpcshell/xpcshell.ini
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 head =
 tail =
 
 [test_presentation_device_manager.js]
+[test_tcp_control_channel.js]
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -454,16 +454,18 @@
 @BINPATH@/components/nsAsyncShutdown.manifest
 @BINPATH@/components/nsAsyncShutdown.js
 
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 
 @BINPATH@/components/PresentationDeviceInfoManager.manifest
 @BINPATH@/components/PresentationDeviceInfoManager.js
+@BINPATH@/components/BuiltinProviders.manifest
+@BINPATH@/components/TCPPresentationServer.js
 
 @BINPATH@/components/PACGenerator.js
 @BINPATH@/components/PACGenerator.manifest
 
 ; Modules
 @BINPATH@/modules/*
 
 #ifdef MOZ_SAFE_BROWSING