Bug 1272197 - Part 2, implement start presentation procedure. r=junior
authorShih-Chiang Chien <schien@mozilla.com>
Mon, 04 Jul 2016 18:12:04 +0800
changeset 304641 2ff7f8ad4bddf42d7b90447be0a72db49cb29ca3
parent 304640 ae8dbcd27f34f39f82fb7b1dcad096c125b3d787
child 304642 6c64cab758d8286c6d7721823b4be233f1fd3420
push idunknown
push userunknown
push dateunknown
reviewersjunior
bugs1272197
milestone50.0a1
Bug 1272197 - Part 2, implement start presentation procedure. r=junior MozReview-Commit-ID: 6RwrwfPpCuR
dom/presentation/PresentationService.cpp
dom/presentation/PresentationSessionInfo.cpp
dom/presentation/interfaces/nsIPresentationControlChannel.idl
dom/presentation/interfaces/nsIPresentationControlService.idl
dom/presentation/interfaces/nsIPresentationDevice.idl
dom/presentation/provider/ControllerStateMachine.jsm
dom/presentation/provider/DisplayDeviceProvider.cpp
dom/presentation/provider/DisplayDeviceProvider.h
dom/presentation/provider/MulticastDNSDeviceProvider.cpp
dom/presentation/provider/MulticastDNSDeviceProvider.h
dom/presentation/provider/PresentationControlService.js
dom/presentation/provider/ReceiverStateMachine.jsm
dom/presentation/provider/StateMachineHelper.jsm
dom/presentation/provider/moz.build
dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
dom/presentation/tests/xpcshell/test_presentation_device_manager.js
dom/presentation/tests/xpcshell/test_presentation_state_machine.js
dom/presentation/tests/xpcshell/test_tcp_control_channel.js
dom/presentation/tests/xpcshell/xpcshell.ini
--- a/dom/presentation/PresentationService.cpp
+++ b/dom/presentation/PresentationService.cpp
@@ -125,17 +125,17 @@ PresentationDeviceRequest::CreateSession
   if (NS_WARN_IF(!info)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   info->SetDevice(aDevice);
 
   // Establish a control channel. If we failed to do so, the callback is called
   // with an error message.
   nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
-  nsresult rv = aDevice->EstablishControlChannel(mRequestUrl, mId, getter_AddRefs(ctrlChannel));
+  nsresult rv = aDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
 
   // Initialize the session info with the control channel.
   rv = info->Init(ctrlChannel);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
@@ -278,85 +278,85 @@ PresentationService::HandleSessionReques
   nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel));
   if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) {
     return rv;
   }
 
   nsAutoString url;
   rv = aRequest->GetUrl(url);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return rv;
   }
 
   nsAutoString sessionId;
   rv = aRequest->GetPresentationId(sessionId);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return rv;
   }
 
   nsCOMPtr<nsIPresentationDevice> device;
   rv = aRequest->GetDevice(getter_AddRefs(device));
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return rv;
   }
 
 #ifdef MOZ_WIDGET_GONK
   // Verify the existence of the app if necessary.
   nsCOMPtr<nsIURI> uri;
   rv = NS_NewURI(getter_AddRefs(uri), url);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(NS_ERROR_DOM_BAD_URI);
+    ctrlChannel->Disconnect(NS_ERROR_DOM_BAD_URI);
     return rv;
   }
 
   bool isApp;
   rv = uri->SchemeIs("app", &isApp);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return rv;
   }
 
   if (NS_WARN_IF(isApp && !IsAppInstalled(uri))) {
-    ctrlChannel->Close(NS_ERROR_DOM_NOT_FOUND_ERR);
+    ctrlChannel->Disconnect(NS_ERROR_DOM_NOT_FOUND_ERR);
     return NS_OK;
   }
 #endif
 
   // Create or reuse session info.
   RefPtr<PresentationSessionInfo> info =
     GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
   if (NS_WARN_IF(info)) {
     // TODO Bug 1195605. Update here after session join/resume becomes supported.
-    ctrlChannel->Close(NS_ERROR_DOM_OPERATION_ERR);
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
     return NS_ERROR_DOM_ABORT_ERR;
   }
 
   info = new PresentationPresentingInfo(url, sessionId, device);
   rv = info->Init(ctrlChannel);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return rv;
   }
 
   mSessionInfoAtReceiver.Put(sessionId, info);
 
   // Notify the receiver to launch.
   nsCOMPtr<nsIPresentationRequestUIGlue> glue =
     do_CreateInstance(PRESENTATION_REQUEST_UI_GLUE_CONTRACTID);
   if (NS_WARN_IF(!glue)) {
-    ctrlChannel->Close(NS_ERROR_DOM_OPERATION_ERR);
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
   nsCOMPtr<nsISupports> promise;
   rv = glue->SendRequest(url, sessionId, device, getter_AddRefs(promise));
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    ctrlChannel->Close(rv);
+    ctrlChannel->Disconnect(rv);
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
   nsCOMPtr<Promise> realPromise = do_QueryInterface(promise);
   static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(realPromise);
 
   return NS_OK;
 }
 
--- a/dom/presentation/PresentationSessionInfo.cpp
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -221,17 +221,17 @@ PresentationSessionInfo::Init(nsIPresent
 
 /* virtual */ void
 PresentationSessionInfo::Shutdown(nsresult aReason)
 {
   NS_WARN_IF(NS_FAILED(aReason));
 
   // Close the control channel if any.
   if (mControlChannel) {
-    NS_WARN_IF(NS_FAILED(mControlChannel->Close(aReason)));
+    NS_WARN_IF(NS_FAILED(mControlChannel->Disconnect(aReason)));
   }
 
   // Close the data transport channel if any.
   if (mTransport) {
     // |mIsTransportReady| will be unset once |NotifyTransportClosed| is called.
     NS_WARN_IF(NS_FAILED(mTransport->Close(aReason)));
   }
 
@@ -462,17 +462,17 @@ NS_IMETHODIMP
 PresentationSessionInfo::SendIceCandidate(const nsAString& candidate)
 {
   return mControlChannel->SendIceCandidate(candidate);
 }
 
 NS_IMETHODIMP
 PresentationSessionInfo::Close(nsresult reason)
 {
-  return mControlChannel->Close(reason);
+  return mControlChannel->Disconnect(reason);
 }
 
 /**
  * Implementation of PresentationControllingInfo
  *
  * During presentation session establishment, the sender expects the following
  * after trying to establish the control channel: (The order between step 3 and
  * 4 is not guaranteed.)
@@ -668,17 +668,17 @@ PresentationControllingInfo::OnAnswer(ns
     }
 
     return builder->OnAnswer(aDescription);
   }
 
   mIsResponderReady = true;
 
   // Close the control channel since it's no longer needed.
-  nsresult rv = mControlChannel->Close(NS_OK);
+  nsresult rv = mControlChannel->Disconnect(NS_OK);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
 
   // Session might not be ready at this moment (waiting for the establishment of
   // the data transport channel).
   if (IsSessionReady()){
     return ReplySuccess();
@@ -687,16 +687,21 @@ PresentationControllingInfo::OnAnswer(ns
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationControllingInfo::NotifyOpened()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  nsresult rv = mControlChannel->Launch(GetSessionId(), GetUrl());
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
   if (!Preferences::GetBool("dom.presentation.session_transport.data_channel.enable")) {
     // Build TCP session transport
     return GetAddress();
   }
 
   nsPIDOMWindowInner* window = nullptr;
   /**
    * Generally transport is maintained by the chrome process. However, data
@@ -726,20 +731,20 @@ PresentationControllingInfo::NotifyOpene
   // OOP case
   mTransportType = nsIPresentationChannelDescription::TYPE_DATACHANNEL;
 
   nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder>
     dataChannelBuilder(do_QueryInterface(mBuilder));
   if (NS_WARN_IF(!dataChannelBuilder)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
-  nsresult rv = dataChannelBuilder->
-                  BuildDataChannelTransport(nsIPresentationService::ROLE_CONTROLLER,
-                                            window,
-                                            this);
+  rv = dataChannelBuilder->
+         BuildDataChannelTransport(nsIPresentationService::ROLE_CONTROLLER,
+                                   window,
+                                   this);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationControllingInfo::NotifyClosed(nsresult aReason)
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -98,13 +98,21 @@ interface nsIPresentationControlChannel:
    * Send ICE candidate to remote endpoint. |onIceCandidate| should be invoked
    * on remote endpoint.
    * @param candidate The candidate to send
    * @throws NS_ERROR_FAILURE on failure
    */
   void sendIceCandidate(in DOMString candidate);
 
   /*
-   * Close the transport channel.
-   * @param reason The reason of channel close; NS_OK represents normal.
+   * Launch a presentation on remote endpoint.
+   * @param presentationId The Id for representing this session.
+   * @param url The URL requested to open by remote device.
+   * @throws NS_ERROR_FAILURE on failure
    */
-  void close(in nsresult reason);
+  void launch(in DOMString presentationId, in DOMString url);
+
+  /*
+   * Disconnect the control channel.
+   * @param reason The reason of disconnecting channel; NS_OK represents normal.
+   */
+  void disconnect(in nsresult reason);
 };
--- a/dom/presentation/interfaces/nsIPresentationControlService.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlService.idl
@@ -60,29 +60,23 @@ interface nsIPresentationControlService:
    *          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 startServer([optional] in uint16_t aPort);
 
   /**
-   * Request session to designated remote presentation control receiver.
+   * Request connection to designated remote presentation control receiver.
    * @param   aDeviceInfo
    *          The remtoe device info for establish connection.
-   * @param   aUrl
-   *          The URL requested to open by remote device.
-   * @param   aPresentationId
-   *          The Id for representing this session.
    * @returns The control channel for this session.
    * @throws  NS_ERROR_FAILURE if the Id hasn't been inited.
    */
-  nsIPresentationControlChannel requestSession(in nsITCPDeviceInfo aDeviceInfo,
-                                               in DOMString aUrl,
-                                               in DOMString aPresentationId);
+  nsIPresentationControlChannel connect(in nsITCPDeviceInfo aDeviceInfo);
 
   /**
    * Check the compatibility to remote presentation control server.
    * @param  aVersion
    *         The version of remote server.
    */
   boolean isCompatibleServer(in uint32_t aVersion);
 
--- a/dom/presentation/interfaces/nsIPresentationDevice.idl
+++ b/dom/presentation/interfaces/nsIPresentationDevice.idl
@@ -19,21 +19,18 @@ interface nsIPresentationDevice : nsISup
   readonly attribute AUTF8String name;
 
   // TODO expose more info in order to fulfill UX spec
   // The category of this device, could be "wifi", "bluetooth", "hdmi", etc.
   readonly attribute AUTF8String type;
 
   /*
    * 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.
    * @returns The control channel for this session.
    * @throws  NS_ERROR_FAILURE if the establishment fails
    */
-  nsIPresentationControlChannel establishControlChannel(in DOMString url,
-                                                        in DOMString presentationId);
+  nsIPresentationControlChannel establishControlChannel();
 
   // Do something when presentation session is disconnected.
   void disconnect();
 };
 
 
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/ControllerStateMachine.jsm
@@ -0,0 +1,187 @@
+/* -*- 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._notifyClosed(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._notifyClosed(command.reason);
+        break;
+      case CommandType.LAUNCH_ACK:
+        stateMachine._notifyLaunch(command.presentationId);
+        break;
+      case CommandType.ANSWER:
+      case CommandType.ICE_CANDIDATE:
+        stateMachine._notifyChannelDescriptor(command);
+        break;
+      default:
+        debug("unexpected command: " + JSON.stringify(command));
+        // ignore unexpected command.
+        break;
+    }
+  },
+  function _closingHandler(stateMachine, command) {
+    // ignore every command in closing state.
+    DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+  },
+  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,
+      });
+    }
+  },
+
+  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._notifyClosed(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._notifyClosed(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();
+  },
+
+  _notifyClosed: function _notifyClosed(reason) {
+    this._channel.notifyClosed(reason);
+  },
+
+  _notifyLaunch: function _notifyLaunch(presentationId) {
+    this._channel.notifyLaunch(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
--- a/dom/presentation/provider/DisplayDeviceProvider.cpp
+++ b/dom/presentation/provider/DisplayDeviceProvider.cpp
@@ -87,30 +87,28 @@ NS_IMETHODIMP
 DisplayDeviceProvider::HDMIDisplayDevice::GetWindowId(nsACString& aWindowId)
 {
   aWindowId = mWindowId;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 DisplayDeviceProvider::HDMIDisplayDevice
-                     ::EstablishControlChannel(const nsAString& aUrl,
-                                               const nsAString& aPresentationId,
-                                               nsIPresentationControlChannel** aControlChannel)
+                     ::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->RequestSession(this, aUrl, aPresentationId, aControlChannel);
+  return provider->Connect(this, aControlChannel);
 }
 
 NS_IMETHODIMP
 DisplayDeviceProvider::HDMIDisplayDevice::Disconnect()
 {
   nsresult rv = CloseTopLevelWindow();
   if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
@@ -431,31 +429,26 @@ DisplayDeviceProvider::Observe(nsISuppor
       }
     }
   }
 
   return NS_OK;
 }
 
 nsresult
-DisplayDeviceProvider::RequestSession(HDMIDisplayDevice* aDevice,
-                                      const nsAString& aUrl,
-                                      const nsAString& aPresentationId,
-                                      nsIPresentationControlChannel** aControlChannel)
+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);
 
-  return mPresentationService->RequestSession(deviceInfo,
-                                              aUrl,
-                                              aPresentationId,
-                                              aControlChannel);
+  return mPresentationService->Connect(deviceInfo, aControlChannel);
 }
 
 } // namespace presentation
 } // namespace dom
 } // namespace mozilla
--- a/dom/presentation/provider/DisplayDeviceProvider.h
+++ b/dom/presentation/provider/DisplayDeviceProvider.h
@@ -88,20 +88,18 @@ private:
 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 RequestSession(HDMIDisplayDevice* aDevice,
-                          const nsAString& aUrl,
-                          const nsAString& aPresentationId,
-                          nsIPresentationControlChannel** aControlChannel);
+  nsresult Connect(HDMIDisplayDevice* aDevice,
+                   nsIPresentationControlChannel** aControlChannel);
 private:
   virtual ~DisplayDeviceProvider();
 
   nsresult Init();
   nsresult Uninit();
 
   nsresult AddExternalScreen();
   nsresult RemoveExternalScreen();
--- a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ -309,29 +309,27 @@ MulticastDNSDeviceProvider::StopDiscover
     mDiscoveryRequest->Cancel(aReason);
     mDiscoveryRequest = nullptr;
   }
 
   return NS_OK;
 }
 
 nsresult
-MulticastDNSDeviceProvider::RequestSession(Device* aDevice,
-                                           const nsAString& aUrl,
-                                           const nsAString& aPresentationId,
-                                           nsIPresentationControlChannel** aRetVal)
+MulticastDNSDeviceProvider::Connect(Device* aDevice,
+                                    nsIPresentationControlChannel** aRetVal)
 {
   MOZ_ASSERT(aDevice);
   MOZ_ASSERT(mPresentationService);
 
   RefPtr<TCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(),
                                                        aDevice->Address(),
                                                        aDevice->Port());
 
-  return mPresentationService->RequestSession(deviceInfo, aUrl, aPresentationId, aRetVal);
+  return mPresentationService->Connect(deviceInfo, aRetVal);
 }
 
 bool
 MulticastDNSDeviceProvider::IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo)
 {
   MOZ_ASSERT(aServiceInfo);
 
   nsCOMPtr<nsIPropertyBag2> propBag;
@@ -1036,25 +1034,24 @@ NS_IMETHODIMP
 MulticastDNSDeviceProvider::Device::GetType(nsACString& aType)
 {
   aType = mType;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
-MulticastDNSDeviceProvider::Device::EstablishControlChannel(const nsAString& aUrl,
-                                                            const nsAString& aPresentationId,
-                                                            nsIPresentationControlChannel** aRetVal)
+MulticastDNSDeviceProvider::Device::EstablishControlChannel(
+                                        nsIPresentationControlChannel** aRetVal)
 {
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
-  return mProvider->RequestSession(this, aUrl, aPresentationId, aRetVal);
+  return mProvider->Connect(this, aRetVal);
 }
 
 NS_IMETHODIMP
 MulticastDNSDeviceProvider::Device::Disconnect()
 {
   // No need to do anything when disconnect.
   return NS_OK;
 }
--- a/dom/presentation/provider/MulticastDNSDeviceProvider.h
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.h
@@ -135,20 +135,18 @@ private:
       return aA->Address() == aB->Address();
     }
   };
 
   virtual ~MulticastDNSDeviceProvider();
   nsresult RegisterService();
   nsresult UnregisterService(nsresult aReason);
   nsresult StopDiscovery(nsresult aReason);
-  nsresult RequestSession(Device* aDevice,
-                          const nsAString& aUrl,
-                          const nsAString& aPresentationId,
-                          nsIPresentationControlChannel** aRetVal);
+  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);
--- a/dom/presentation/provider/PresentationControlService.js
+++ b/dom/presentation/provider/PresentationControlService.js
@@ -9,16 +9,23 @@ const {classes: Cc, interfaces: Ci, util
 
 /* 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 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 DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug");
 function log(aMsg) {
   dump("-*- PresentationControlService.js: " + aMsg + "\n");
 }
 
 function TCPDeviceInfo(aAddress, aPort, aId) {
@@ -108,22 +115,22 @@ PresentationControlService.prototype = {
   get listener() {
     return this._listener;
   },
 
   _isServiceInit: function() {
     return this._serverSocket !== null;
   },
 
-  requestSession: function(aDeviceInfo, aUrl, aPresentationId) {
+  connect: function(aDeviceInfo) {
     if (!this.id) {
-      DEBUG && log("PresentationControlService - Id has not initialized; requestSession fails"); // jshint ignore:line
+      DEBUG && log("PresentationControlService - Id has not initialized; connect fails"); // jshint ignore:line
       return null;
     }
-    DEBUG && log("PresentationControlService - requestSession to " + aDeviceInfo.id); // jshint ignore:line
+    DEBUG && log("PresentationControlService - connect 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,
@@ -133,36 +140,31 @@ PresentationControlService.prototype = {
     } catch (e) {
       DEBUG && log("PresentationControlService - createTransport throws: " + e);  // jshint ignore:line
       // Pop the exception to |TCPDevice.establishControlChannel|
       throw Cr.NS_ERROR_FAILURE;
     }
     return new TCPControlChannel(this,
                                  socketTransport,
                                  aDeviceInfo,
-                                 aPresentationId,
-                                 "sender",
-                                 aUrl);
+                                 "sender");
   },
 
   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,
-                                 null, // presentation ID
-                                 "receiver",
-                                 null // url
-                                 );
+                                 "receiver");
   },
 
   // Triggered by TCPControlChannel
   onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
     DEBUG && log("PresentationControlService - onSessionRequest: " +
                  aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
     this.listener.onSessionRequest(aDeviceInfo,
                                    aUrl,
@@ -329,122 +331,70 @@ function discriptionAsJson(aDescription)
       break;
   }
   return json;
 }
 
 function TCPControlChannel(presentationService,
                            transport,
                            deviceInfo,
-                           presentationId,
-                           direction,
-                           url) {
-  DEBUG && log("create TCPControlChannel: " + presentationId + " with role: " +
-               direction); // jshint ignore:line
+                           direction) {
+  DEBUG && log("create TCPControlChannel for : " + direction); // jshint ignore:line
   this._deviceInfo = deviceInfo;
-  this._presentationId = presentationId;
   this._direction = direction;
   this._transport = transport;
-  this._url = url;
 
   this._presentationService = presentationService;
 
   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);
 
+  this._stateMachine =
+    (direction === "sender") ? new ControllerStateMachine(this, presentationService.id)
+                             : new ReceiverStateMachine(this);
   // 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"); // jshint ignore:line
-      throw Cr.NS_ERROR_FAILURE;
-    }
-
-    DEBUG && log("TCPControlChannel - send" + aType + ": " +
-                 JSON.stringify(aJSONData)); // jshint ignore:line
-    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._presentationService.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);
+    this._stateMachine.sendOffer(discriptionAsJson(aOffer));
   },
 
   sendAnswer: function(aAnswer) {
-    let msg = {
-      type: "requestSession:Answer",
-      presentationId: this.presentationId,
-      answer: discriptionAsJson(aAnswer),
-    };
-    this._sendMessage("answer", msg);
+    this._stateMachine.sendAnswer(discriptionAsJson(aAnswer));
   },
 
   sendIceCandidate: function(aCandidate) {
-    let msg = {
-      type: "requestSession:IceCandidate",
-      presentationId: this.presentationId,
-      iceCandidate: aCandidate,
-    };
-    this._sendMessage("iceCandidate", msg);
+    this._stateMachine.updateIceCandidate(aCandidate);
   },
+
+  launch: function(aPresentationId, aUrl) {
+    this._stateMachine.launch(aPresentationId, aUrl);
+  },
+
   // 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
@@ -477,40 +427,30 @@ TCPControlChannel.prototype = {
     DEBUG && log("TCPControlChannel - onTransportStatus: " + aStatus.toString(16) +
                  " with role: " + this._direction); // jshint ignore:line
     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); // jshint ignore:line
   },
 
   // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
   onStopRequest: function(aRequest, aContext, aStatus) {
-    this.close(aStatus);
-    this._notifyClosed(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
 
@@ -531,51 +471,24 @@ TCPControlChannel.prototype = {
 
   _createInputStreamPump: function() {
     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) {
-    switch (aMsg.type) {
-      case "requestSession:Init": {
-        this._deviceInfo.id = aMsg.id;
-        this._url = aMsg.url;
-        this._presentationId = aMsg.presentationId;
-        this._presentationService.onSessionRequest(this._deviceInfo,
-                                                   aMsg.url,
-                                                   aMsg.presentationId,
-                                                   this);
-        this._notifyOpened();
-        break;
-      }
-      case "requestSession:Offer": {
-        this._onOffer(aMsg.offer);
-        break;
-      }
-      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;
-      }
-    }
     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
@@ -676,40 +589,82 @@ TCPControlChannel.prototype = {
       return;
     }
 
     DEBUG && log("TCPControlChannel - notify closed with role: " +
                  this._direction); // jshint ignore:line
     this._listener.notifyClosed(aReason);
   },
 
-  close: function(aReason) {
-    DEBUG && log("TCPControlChannel - close with reason: " + aReason); // jshint ignore:line
-
+  _closeTransport: function() {
     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("close", msg);
-        this._pendingCloseReason = aReason;
-      }
-
       this._transport.setEventSink(null, null);
       this._pump = null;
 
       this._input.close();
       this._output.close();
       this._presentationService.releaseControlChannel(this);
+    }
+  },
+
+  disconnect: function(aReason) {
+    DEBUG && log("TCPControlChannel - disconnect with reason: " + aReason); // jshint ignore:line
+
+    if (this._connected) {
+      // default reason is NS_OK
+      aReason = !aReason ? Cr.NS_OK : aReason;
+      this._stateMachine.onChannelClosed(aReason, false);
+
+      this._closeTransport();
 
       this._connected = false;
     }
   },
 
+  // callback from state machine
+  sendCommand: function(command) {
+    this._send(command);
+  },
+
+  notifyDeviceConnected: function(deviceId) {
+    switch (this._direction) {
+      case "receiver":
+        this._deviceInfo.id = deviceId;
+        break;
+    }
+    this._notifyOpened();
+  },
+
+  notifyClosed: function(reason) {
+    this._notifyClosed(reason);
+    this._closeTransport();
+    this._connected = false;
+  },
+
+  notifyLaunch: function(presentationId, url) {
+    switch (this._direction) {
+      case "receiver":
+        this._presentationService.onSessionRequest(this._deviceInfo,
+                                                   url,
+                                                   presentationId,
+                                                   this);
+      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
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
@@ -0,0 +1,186 @@
+/* -*- 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._notifyClosed(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._notifyClosed(command.reason);
+        break;
+      case CommandType.LAUNCH:
+        stateMachine._notifyLaunch(command.presentationId,
+                                   command.url);
+        stateMachine._sendCommand({
+          type: CommandType.LAUNCH_ACK,
+          presentationId: command.presentationId
+        });
+        break;
+      case CommandType.OFFER:
+      case CommandType.ICE_CANDIDATE:
+        stateMachine._notifyChannelDescriptor(command);
+        break;
+      default:
+        debug("unexpected command: " + JSON.stringify(command));
+        // ignore unexpected command
+        break;
+    }
+  },
+  function _closingHandler(stateMachine, command) {
+    // ignore every command in closing state.
+    DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+  },
+  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");
+  },
+
+  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._notifyClosed(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._notifyClosed(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);
+  },
+
+  _notifyClosed: function _notifyClosed(reason) {
+    this._channel.notifyClosed(reason);
+  },
+
+  _notifyLaunch: function _notifyLaunch(presentationId, url) {
+    this._channel.notifyLaunch(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
new file mode 100644
--- /dev/null
+++ b/dom/presentation/provider/StateMachineHelper.jsm
@@ -0,0 +1,35 @@
+/* -*- 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> }
+  // 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
--- a/dom/presentation/provider/moz.build
+++ b/dom/presentation/provider/moz.build
@@ -10,10 +10,16 @@ EXTRA_COMPONENTS += [
 ]
 
 UNIFIED_SOURCES += [
     'DisplayDeviceProvider.cpp',
     'MulticastDNSDeviceProvider.cpp',
     'PresentationDeviceProviderModule.cpp',
 ]
 
+EXTRA_JS_MODULES.presentation += [
+    'ControllerStateMachine.jsm',
+    'ReceiverStateMachine.jsm',
+    'StateMachineHelper.jsm',
+]
+
 include('/ipc/chromium/chromium-config.mozbuild')
 FINAL_LIBRARY = 'xul'
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -136,17 +136,19 @@ const mockedControlChannel = {
       } catch (e) {
         isValid = false;
       }
     } else if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL) {
       isValid = (aSDP.dataChannelSDP == "test-sdp");
     }
     return isValid;
   },
-  close: function(reason) {
+  launch: function(presentationId, url) {
+  },
+  disconnect: function(reason) {
     sendAsyncMessage('control-channel-closed', reason);
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyClosed(reason);
   },
   simulateReceiverReady: function() {
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady();
   },
   simulateOnOffer: function() {
     sendAsyncMessage('offer-received');
@@ -402,17 +404,17 @@ addMessageListener('trigger-incoming-tra
   mockedServerSocket.simulateOnSocketAccepted(mockedServerSocket, mockedSocketTransport);
 });
 
 addMessageListener('trigger-control-channel-open', function(reason) {
   mockedControlChannel.simulateNotifyOpened();
 });
 
 addMessageListener('trigger-control-channel-close', function(reason) {
-  mockedControlChannel.close(reason);
+  mockedControlChannel.disconnect(reason);
 });
 
 addMessageListener('trigger-data-transport-close', function(reason) {
   mockedSessionTransport.close(reason);
 });
 
 addMessageListener('trigger-incoming-message', function(message) {
   mockedSessionTransport.simulateIncomingMessage(message);
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -178,57 +178,69 @@ const mockControlChannelOfSender = {
   sendOffer: function(offer) {
     sendAsyncMessage('offer-sent');
   },
   onAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .onAnswer(answer);
   },
-  close: function(reason) {
+  launch: function(presentationId, url) {
+    sendAsyncMessage('sender-launch', url);
+  },
+  disconnect: function(reason) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyClosed(reason);
-    mockControlChannelOfReceiver.close();
+    mockControlChannelOfReceiver.disconnect();
   }
 };
 
 // control channel of receiver
 const mockControlChannelOfReceiver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   set listener(listener) {
     // PresentationPresentingInfo::SetControlChannel
     if (listener) {
       debug('set listener for mockControlChannelOfReceiver without null');
     } else {
       debug('set listener for mockControlChannelOfReceiver with null');
     }
     this._listener = listener;
+
+    if (this._pendingOpened) {
+      this._pendingOpened = false;
+      this.notifyOpened();
+    }
   },
   get listener() {
     return this._listener;
   },
   notifyOpened: function() {
     // do nothing
+    if (!this._listener) {
+      this._pendingOpened = true;
+      return;
+    }
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyOpened();
   },
   onOffer: function(offer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .onOffer(offer);
   },
   sendAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
         .notifyTransportReady();
     sendAsyncMessage('answer-sent');
   },
-  close: function(reason) {
+  disconnect: function(reason) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyClosed(reason);
     sendAsyncMessage('control-channel-receiver-closed', reason);
   }
 };
 
 const mockDevice = {
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -21,30 +21,35 @@ function setup() {
 
   gScript.addMessageListener('device-prompt', function devicePromptHandler() {
     debug('Got message: device-prompt');
     gScript.removeMessageListener('device-prompt', devicePromptHandler);
     gScript.sendAsyncMessage('trigger-device-prompt-select');
   });
 
   gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
-    debug('Got message: control-channel-established');
     gScript.removeMessageListener('control-channel-established',
                                   controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage("trigger-control-channel-open");
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
     receiverIframe = document.createElement('iframe');
     receiverIframe.setAttribute("mozbrowser", "true");
     receiverIframe.setAttribute("mozpresentation", receiverUrl);
     var oop = location.pathname.indexOf('_inproc') == -1;
     receiverIframe.setAttribute("remote", oop);
 
     receiverIframe.setAttribute('src', receiverUrl);
     receiverIframe.addEventListener("mozbrowserloadend", function mozbrowserloadendHander() {
       receiverIframe.removeEventListener("mozbrowserloadend", mozbrowserloadendHander);
       info("Receiver loaded.");
-      gScript.sendAsyncMessage("trigger-control-channel-open");
     });
 
     // This event is triggered when the iframe calls "alert".
     receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) {
       var message = evt.detail.message;
       if (/^OK /.exec(message)) {
         ok(true, message.replace(/^OK /, ""));
       } else if (/^KO /.exec(message)) {
@@ -91,17 +96,17 @@ function setup() {
   });
 
   return Promise.resolve();
 }
 
 function testCreateRequest() {
   return new Promise(function(aResolve, aReject) {
     info('Sender: --- testCreateRequest ---');
-    request = new PresentationRequest("http://example.com");
+    request = new PresentationRequest(receiverUrl);
     request.getAvailability().then((aAvailability) => {
       aAvailability.onchange = function() {
         aAvailability.onchange = null;
         ok(aAvailability.value, "Sender: Device should be available.");
         aResolve();
       }
     }).catch((aError) => {
       ok(false, "Sender: Error occurred when getting availability: " + aError);
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
@@ -35,26 +35,29 @@ function setup() {
 
   gScript.addMessageListener('device-prompt', function devicePromptHandler() {
     debug('Got message: device-prompt');
     gScript.removeMessageListener('device-prompt', devicePromptHandler);
     gScript.sendAsyncMessage('trigger-device-prompt-select');
   });
 
   gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
-    debug('Got message: control-channel-established');
-    gScript.removeMessageListener('control-channel-established', controlChannelEstablishedHandler);
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage("trigger-control-channel-open");
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
     receiverIframe = document.createElement('iframe');
     receiverIframe.setAttribute('src', receiverUrl);
     receiverIframe.setAttribute("mozbrowser", "true");
     receiverIframe.setAttribute("mozpresentation", receiverUrl);
-    receiverIframe.onload = function() {
-      info('Receiver loaded.');
-      gScript.sendAsyncMessage('trigger-control-channel-open');
-    };
 
     // This event is triggered when the iframe calls "alert".
     receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) {
       var message = evt.detail.message;
       debug('Got iframe message: ' + message);
       if (/^OK /.exec(message)) {
         ok(true, message.replace(/^OK /, ""));
       } else if (/^KO /.exec(message)) {
@@ -100,17 +103,17 @@ function setup() {
   });
 
   return Promise.resolve();
 }
 
 function testCreateRequest() {
   return new Promise(function(aResolve, aReject) {
     info('Sender: --- testCreateRequest ---');
-    request = new PresentationRequest("http://example.com");
+    request = new PresentationRequest(receiverUrl);
     request.getAvailability().then((aAvailability) => {
       aAvailability.onchange = function() {
         aAvailability.onchange = null;
         ok(aAvailability.value, "Sender: Device should be available.");
         aResolve();
       }
     }).catch((aError) => {
       ok(false, "Sender: Error occurred when getting availability: " + aError);
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
@@ -35,28 +35,33 @@ function setup() {
 
   gScript.addMessageListener('device-prompt', function devicePromptHandler() {
     debug('Got message: device-prompt');
     gScript.removeMessageListener('device-prompt', devicePromptHandler);
     gScript.sendAsyncMessage('trigger-device-prompt-select');
   });
 
   gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
-    debug('Got message: control-channel-established');
     gScript.removeMessageListener('control-channel-established',
                                   controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage("trigger-control-channel-open");
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
     receiverIframe = document.createElement('iframe');
     receiverIframe.setAttribute("remote", "true");
     receiverIframe.setAttribute("mozbrowser", "true");
     receiverIframe.setAttribute("mozpresentation", receiverUrl);
     receiverIframe.setAttribute('src', receiverUrl);
     receiverIframe.addEventListener("mozbrowserloadend", function mozbrowserloadendHander() {
       receiverIframe.removeEventListener("mozbrowserloadend", mozbrowserloadendHander);
       info("Receiver loaded.");
-      gScript.sendAsyncMessage("trigger-control-channel-open");
     });
 
     // This event is triggered when the iframe calls "alert".
     receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) {
       var message = evt.detail.message;
       if (/^OK /.exec(message)) {
         ok(true, message.replace(/^OK /, ""));
       } else if (/^KO /.exec(message)) {
@@ -103,17 +108,17 @@ function setup() {
   });
 
   return Promise.resolve();
 }
 
 function testCreateRequest() {
   return new Promise(function(aResolve, aReject) {
     info('Sender: --- testCreateRequest ---');
-    request = new PresentationRequest("http://example.com");
+    request = new PresentationRequest(receiverUrl);
     request.getAvailability()
       .then((aAvailability) => {
         aAvailability.onchange = function() {
           aAvailability.onchange = null;
           ok(aAvailability.value, "Sender: Device should be available.");
           aResolve();
         }
       })
--- a/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
+++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
@@ -104,17 +104,17 @@ const clientListener = {
     setTimeout(()=>this._remoteBuilder.onOffer(aOffer), 0);
   },
   sendAnswer: function(aAnswer) {
     setTimeout(()=>this._remoteBuilder.onAnswer(aAnswer), 0);
   },
   sendIceCandidate: function(aCandidate) {
     setTimeout(()=>this._remoteBuilder.onIceCandidate(aCandidate), 0);
   },
-  close: function(aReason) {
+  disconnect: function(aReason) {
     setTimeout(()=>this._localBuilder.notifyClosed(aReason), 0);
     setTimeout(()=>this._remoteBuilder.notifyClosed(aReason), 0);
   },
   set remoteBuilder(aRemoteBuilder) {
     this._remoteBuilder = aRemoteBuilder;
   },
   set localBuilder(aLocalBuilder) {
     this._localBuilder = aLocalBuilder;
@@ -136,17 +136,17 @@ const serverListener = {
     setTimeout(()=>this._remoteBuilder.onOffer(aOffer), 0);
   },
   sendAnswer: function(aAnswer) {
     setTimeout(()=>this._remoteBuilder.onAnswer(aAnswer), 0);
   },
   sendIceCandidate: function(aCandidate) {
     setTimeout(()=>this._remoteBuilder.onIceCandidate(aCandidate), 0);
   },
-  close: function(aReason) {
+  disconnect: function(aReason) {
     setTimeout(()=>this._localBuilder.notifyClosed(aReason), 0);
     setTimeout(()=>this._remoteBuilder.notifyClosed(aReason), 0);
   },
   set remoteBuilder(aRemoteBuilder) {
     this._remoteBuilder = aRemoteBuilder;
   },
   set localBuilder(aLocalBuilder) {
     this._localBuilder = aLocalBuilder;
--- a/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
+++ b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
@@ -420,21 +420,19 @@ function handleSessionRequest() {
                                               mockDevice.port,
                                               mockDevice.serviceName,
                                               mockDevice.serviceType));
     }
   };
 
   let mockServerObj = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
-    requestSession: function(deviceInfo, url, presentationId) {
+    connect: function(deviceInfo) {
       this.request = {
         deviceInfo: deviceInfo,
-        url: url,
-        presentationId: presentationId,
       };
       return {
         QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
       };
     },
     id: "",
     version: LATEST_VERSION,
     isCompatibleServer: function(version) {
@@ -450,23 +448,21 @@ function handleSessionRequest() {
                                            Ci.nsISupportsWeakReference]),
     addDevice: function(device) {
       this.device = device;
     },
   };
 
   provider.listener = listener;
 
-  let controlChannel = listener.device.establishControlChannel(testUrl, testPresentationId);
+  let controlChannel = listener.device.establishControlChannel();
 
   Assert.equal(mockServerObj.request.deviceInfo.id, mockDevice.host);
   Assert.equal(mockServerObj.request.deviceInfo.address, mockDevice.host);
   Assert.equal(mockServerObj.request.deviceInfo.port, mockDevice.port);
-  Assert.equal(mockServerObj.request.url, testUrl);
-  Assert.equal(mockServerObj.request.presentationId, testPresentationId);
   Assert.equal(mockServerObj.id, testDeviceName);
 
   provider.listener = null;
 
   run_next_test();
 }
 
 function handleOnSessionRequest() {
--- a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -16,17 +16,18 @@ function TestPresentationDevice() {}
 
 
 function TestPresentationControlChannel() {}
 
 TestPresentationControlChannel.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   sendOffer: function(offer) {},
   sendAnswer: function(answer) {},
-  close: function() {},
+  disconnect: function() {},
+  launch: function() {},
   set listener(listener) {},
   get listener() {},
 };
 
 var testProvider = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
 
   forceDiscovery: function() {
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -0,0 +1,195 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components,Assert,run_next_test,add_test,do_execute_soon */
+
+'use strict';
+
+const { utils: Cu, results: Cr } = Components;
+
+/* globals ControllerStateMachine */
+Cu.import('resource://gre/modules/presentation/ControllerStateMachine.jsm');
+/* globals ReceiverStateMachine */
+Cu.import('resource://gre/modules/presentation/ReceiverStateMachine.jsm');
+/* globals State */
+Cu.import('resource://gre/modules/presentation/StateMachineHelper.jsm');
+
+const testControllerId = 'test-controller-id';
+const testPresentationId = 'test-presentation-id';
+const testUrl = 'http://example.org';
+
+let mockControllerChannel = {};
+let mockReceiverChannel = {};
+
+let controllerState = new ControllerStateMachine(mockControllerChannel, testControllerId);
+let receiverState = new ReceiverStateMachine(mockReceiverChannel);
+
+mockControllerChannel.sendCommand = function(command) {
+  do_execute_soon(function() {
+    receiverState.onCommand(command);
+  });
+};
+
+mockReceiverChannel.sendCommand = function(command) {
+  do_execute_soon(function() {
+    controllerState.onCommand(command);
+  });
+};
+
+function connect() {
+  Assert.equal(controllerState.state, State.INIT, 'controller in init state');
+  Assert.equal(receiverState.state, State.INIT, 'receiver in init state');
+  // step 1: underlying connection is ready
+  controllerState.onChannelReady();
+  Assert.equal(controllerState.state, State.CONNECTING, 'controller in connecting state');
+  receiverState.onChannelReady();
+  Assert.equal(receiverState.state, State.CONNECTING, 'receiver in connecting state');
+
+  // step 2: receiver reply to connect command
+  mockReceiverChannel.notifyDeviceConnected = function(deviceId) {
+    Assert.equal(deviceId, testControllerId, 'receiver connect to mock controller');
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+    // step 3: controller receive connect-ack command
+    mockControllerChannel.notifyDeviceConnected = function() {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      run_next_test();
+    };
+  };
+}
+
+function launch() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  controllerState.launch(testPresentationId, testUrl);
+  mockReceiverChannel.notifyLaunch = function(presentationId, url) {
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+    Assert.equal(url, testUrl, 'expected url received');
+
+    mockControllerChannel.notifyLaunch = function(presentationId) {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+      run_next_test();
+    };
+  };
+}
+
+function exchangeSDP() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  const testOffer = 'test-offer';
+  const testAnswer = 'test-answer';
+  const testIceCandidate = 'test-ice-candidate';
+  controllerState.sendOffer(testOffer);
+  mockReceiverChannel.notifyOffer = function(offer) {
+    Assert.equal(offer, testOffer, 'expected offer received');
+
+    receiverState.sendAnswer(testAnswer);
+    mockControllerChannel.notifyAnswer = function(answer) {
+      Assert.equal(answer, testAnswer, 'expected answer received');
+
+      controllerState.updateIceCandidate(testIceCandidate);
+      mockReceiverChannel.notifyIceCandidate = function(candidate) {
+        Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in receiver');
+
+        receiverState.updateIceCandidate(testIceCandidate);
+        mockControllerChannel.notifyIceCandidate = function(candidate) {
+          Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in controller');
+
+          run_next_test();
+        };
+      };
+    };
+  };
+}
+
+function disconnect() {
+  // step 1: controller send disconnect command
+  controllerState.onChannelClosed(Cr.NS_OK, false);
+  Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+  mockReceiverChannel.notifyClosed = function(reason) {
+    Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    mockControllerChannel.notifyClosed = function(reason) {
+      Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+      Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+      run_next_test();
+    };
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+function receiverDisconnect() {
+  // initial state: controller and receiver are connected
+  controllerState.state = State.CONNECTED;
+  receiverState.state = State.CONNECTED;
+
+  // step 1: controller send disconnect command
+  receiverState.onChannelClosed(Cr.NS_OK, false);
+  Assert.equal(receiverState.state, State.CLOSING, 'receiver in closing state');
+
+  mockControllerChannel.notifyClosed = function(reason) {
+    Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+    Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+    mockReceiverChannel.notifyClosed = function(reason) {
+      Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+      Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+      run_next_test();
+    };
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+function abnormalDisconnect() {
+  // initial state: controller and receiver are connected
+  controllerState.state = State.CONNECTED;
+  receiverState.state = State.CONNECTED;
+
+  const testErrorReason = Cr.NS_ERROR_FAILURE;
+  // step 1: controller send disconnect command
+  controllerState.onChannelClosed(testErrorReason, false);
+  Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+  mockReceiverChannel.notifyClosed = function(reason) {
+    Assert.equal(reason, testErrorReason, 'receive abnormal close reason');
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    mockControllerChannel.notifyClosed = function(reason) {
+      Assert.equal(reason, testErrorReason, 'receive abnormal close reason');
+      Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+      run_next_test();
+    };
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+add_test(connect);
+add_test(launch);
+add_test(exchangeSDP);
+add_test(disconnect);
+add_test(receiverDisconnect);
+add_test(abnormalDisconnect);
+
+function run_test() { // jshint ignore:line
+  run_next_test();
+}
--- a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -104,25 +104,25 @@ function testPresentationServer() {
         onIceCandidate: function(aCandidate) {
           Assert.ok(true, '3. controllerControlChannel: get ice candidate, close channel');
           let recvCandidate = JSON.parse(aCandidate);
           for (let key in recvCandidate) {
             if (typeof(recvCandidate[key]) !== "function") {
               Assert.equal(recvCandidate[key], candidate[key], "key " + key + " should match.");
             }
           }
-          controllerControlChannel.close(CLOSE_CONTROL_CHANNEL_REASON);
+          controllerControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
         },
         notifyOpened: function() {
           Assert.equal(this.status, 'created', '0. controllerControlChannel: opened');
           this.status = 'opened';
         },
         notifyClosed: function(aReason) {
           Assert.equal(this.status, 'onOffer', '4. controllerControlChannel: closed');
-          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'presenterControlChannel notify closed');
+          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify closed');
           this.status = 'closed';
           yayFuncs.controllerControlChannelClose();
         },
         QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
       };
     },
 
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlServerListener]),
@@ -130,19 +130,17 @@ function testPresentationServer() {
 
   let presenterDeviceInfo = {
     id: 'presentatorID',
     address: '127.0.0.1',
     port: PRESENTER_CONTROL_CHANNEL_PORT,
     QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
   };
 
-  let presenterControlChannel = tps.requestSession(presenterDeviceInfo,
-                                                   'http://example.com',
-                                                   'testPresentationId');
+  let presenterControlChannel = tps.connect(presenterDeviceInfo);
 
   presenterControlChannel.listener = {
     status: 'created',
     onOffer: function(offer) {
       Assert.ok(false, 'get offer');
     },
     onAnswer: function(aAnswer) {
       Assert.equal(this.status, 'opened', '2. presenterControlChannel: get answer, send ICE candidate');
@@ -159,16 +157,17 @@ function testPresentationServer() {
       };
       presenterControlChannel.sendIceCandidate(JSON.stringify(candidate));
     },
     onIceCandidate: function(aCandidate) {
       Assert.ok(false, 'get ICE candidate');
     },
     notifyOpened: function() {
       Assert.equal(this.status, 'created', '0. presenterControlChannel: opened, send offer');
+      presenterControlChannel.launch('testPresentationId', 'http://example.com');
       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);
       }
--- a/dom/presentation/tests/xpcshell/xpcshell.ini
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 head =
 tail =
 
 [test_multicast_dns_device_provider.js]
 [test_presentation_device_manager.js]
 [test_presentation_session_transport.js]
 [test_tcp_control_channel.js]
+[test_presentation_state_machine.js]