Closed Bug 1276378 Opened 3 years ago Closed 3 years ago

[Presentation WebAPI] support terminating a presentation in controlling presentation context

Categories

(Core :: DOM: Core & HTML, defect)

defect
Not set

Tracking

()

RESOLVED FIXED
mozilla50
blocking-b2g 2.6+
Tracking Status
b2g-v2.6 --- fixed
firefox50 --- fixed

People

(Reporter: schien, Assigned: schien)

References

()

Details

(Whiteboard: [ETA 6/30] btpp-fixlater)

Attachments

(3 files, 7 obsolete files)

43.53 KB, patch
schien
: review+
Details | Diff | Splinter Review
63.83 KB, patch
schien
: review+
Details | Diff | Splinter Review
48 bytes, text/x-github-pull-request
Details | Review
Support terminating the receiver page via calling connection.terminate() in controlling page.
blocking-b2g: 2.6? → 2.6+
Whiteboard: [ETA 6/30] → [ETA 6/30] btpp-fixlater
Assignee: nobody → schien
Attached patch terminate-webapi.patch (obsolete) — Splinter Review
Allow control page to terminate receiver page via connection.terminate().
Attachment #8766747 - Flags: feedback?(kechang)
During my test, I found closing tab of the controller page will trigger 'wentaway' procedure, which will only close transport channel. Do we want the receiver page be terminated as well in this scenario?
Comment on attachment 8766747 [details] [diff] [review]
terminate-webapi.patch

Review of attachment 8766747 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/presentation/PresentationService.cpp
@@ +57,5 @@
> +
> +  return true;
> +}
> +
> +} // anonymous namespace

According to https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Coding_Style#Anonymous_namespaces, static function is preferred.

@@ +435,5 @@
> +  }
> +
> +  //check if terminate request comes from known device
> +  RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
> +  if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {

Not sure if we really need to do this.
Is it possible that this request is from an unknown device?

@@ +822,5 @@
> +          window->Close();
> +        }
> +      }));
> +    }
> +

It seems not right to close the window in UntrackSessionInfo.
A possible code path to UntrackSessionInfo is PresentationConnection::ProcessStateChanged -> PresentationService::UnregisterSessionListener -> PresentationService::UntrackSessionInfo.
If we close the window here, the window will be closed before onterminate event.
Attachment #8766747 - Flags: feedback?(kechang)
Comment on attachment 8766747 [details] [diff] [review]
terminate-webapi.patch

Review of attachment 8766747 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/presentation/PresentationService.cpp
@@ +57,5 @@
> +
> +  return true;
> +}
> +
> +} // anonymous namespace

done.

@@ +435,5 @@
> +  }
> +
> +  //check if terminate request comes from known device
> +  RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
> +  if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {

session termination command can be triggered via network and I'm paranoid about allowing this command from other devices.

@@ +822,5 @@
> +          window->Close();
> +        }
> +      }));
> +    }
> +

Got it. How about we do |asyncDispatcher->PostDOMEvent| before UnregisterSessionListener? In this case the event sequence will be guaranteed.
@kershaw might want to check my response at comment #4.
Flags: needinfo?(kechang)
Comment on attachment 8766747 [details] [diff] [review]
terminate-webapi.patch

Review of attachment 8766747 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/presentation/PresentationService.cpp
@@ +435,5 @@
> +  }
> +
> +  //check if terminate request comes from known device
> +  RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
> +  if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {

knownDevice is actually coming from the sessionId in the request and the other device is also from the same request. That's why I think these two device should not be different.
However, I am fine with this check if you think this is necessary.

@@ +822,5 @@
> +          window->Close();
> +        }
> +      }));
> +    }
> +

Sounds good to me.
Please see comment #6.
(In reply to Kershaw Chang [:kershaw] from comment #7)
> Please see comment #6.

Clear ni.
Flags: needinfo?(kechang)
Attached patch part 2, terminate-webapi.patch (obsolete) — Splinter Review
Attachment #8766747 - Attachment is obsolete: true
Attachment #8768647 - Attachment is obsolete: true
Attachment #8770102 - Flags: review?(juhsu)
Attached patch part 2, terminate-webapi.patch (obsolete) — Splinter Review
1. handle on-terminate-request notification
2. re-establish control channel and trigger terminate protocol
3. trigger window.close after onterminate is fired on receiver window.
Attachment #8768648 - Attachment is obsolete: true
Attachment #8770104 - Flags: review?(bugs)
Comment on attachment 8770104 [details] [diff] [review]
part 2, terminate-webapi.patch

>     case PresentationConnectionState::Terminated: {
>+      // ensure onterminate event before closing web page.
>+      RefPtr<AsyncEventDispatcher> asyncDispatcher =
>+        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
>+      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
>+
>       nsCOMPtr<nsIPresentationService> service =
>         do_GetService(PRESENTATION_SERVICE_CONTRACTID);
>       if (NS_WARN_IF(!service)) {
>         return NS_ERROR_NOT_AVAILABLE;
>       }
> 
>       nsresult rv = service->UnregisterSessionListener(mId, mRole);
>       if(NS_WARN_IF(NS_FAILED(rv))) {
>         return rv;
>       }
> 
>-      RefPtr<AsyncEventDispatcher> asyncDispatcher =
>-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
>-      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
>-
This change is unclear to me. I mean, I don't know why it has any effect. "terminate" will be dispatched asynchronously anyhow.
Please explain and improve the comment a bit.

>+    case nsIPresentationSessionListener::STATE_TERMINATED: {
>+      if (!mControlChannel) {
>+        nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
>+        nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
>+        if (NS_SUCCEEDED(rv)) {
>+          SetControlChannel(ctrlChannel);
>+        }
>+        return rv;
>+      }
Hmm, this needs some comment. What is the case when we don't have control channel but need to still terminate?



>+    gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() {
>+      gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate);
>+      connection.terminate();
I think there should be some tests for weird cases too, like
calling connection.close() right after terminate() or using send() after terminate() etc.


r- mostly because I'd like to see some comments in the code and I feel like I should re-read the patch anyhow, but I also think unusual ordering of terminate() with other functions should be tested a bit.
Attachment #8770104 - Flags: review?(bugs) → review-
(In reply to Olli Pettay [:smaug] from comment #14)
> Comment on attachment 8770104 [details] [diff] [review]
> part 2, terminate-webapi.patch
> 
> >     case PresentationConnectionState::Terminated: {
> >+      // ensure onterminate event before closing web page.
> >+      RefPtr<AsyncEventDispatcher> asyncDispatcher =
> >+        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
> >+      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
> >+
> >       nsCOMPtr<nsIPresentationService> service =
> >         do_GetService(PRESENTATION_SERVICE_CONTRACTID);
> >       if (NS_WARN_IF(!service)) {
> >         return NS_ERROR_NOT_AVAILABLE;
> >       }
> > 
> >       nsresult rv = service->UnregisterSessionListener(mId, mRole);
> >       if(NS_WARN_IF(NS_FAILED(rv))) {
> >         return rv;
> >       }
> > 
> >-      RefPtr<AsyncEventDispatcher> asyncDispatcher =
> >-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
> >-      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
> >-
> This change is unclear to me. I mean, I don't know why it has any effect.
> "terminate" will be dispatched asynchronously anyhow.
> Please explain and improve the comment a bit.
UnregisterSessionListener is the timing we can use for triggering window close (asynchronously) on receiver page after session termination. In order to make sure "onterminate" event handler is always invoked before window closed, I reorder this code section so that "terminate" event is always enqueued in front of window.close.
> 
> >+    case nsIPresentationSessionListener::STATE_TERMINATED: {
> >+      if (!mControlChannel) {
> >+        nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
> >+        nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
> >+        if (NS_SUCCEEDED(rv)) {
> >+          SetControlChannel(ctrlChannel);
> >+        }
> >+        return rv;
> >+      }
> Hmm, this needs some comment. What is the case when we don't have control
> channel but need to still terminate?
In our current design, control channel is short-lived and will be close soon after successful session launched. Therefore, we'll need to re-establish control channel for session termination in most cases. 
> 
> 
> 
> >+    gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() {
> >+      gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate);
> >+      connection.terminate();
> I think there should be some tests for weird cases too, like
> calling connection.close() right after terminate() or using send() after
> terminate() etc.
sure, will provide more test on edge cases in next revision.
> 
> 
> r- mostly because I'd like to see some comments in the code and I feel like
> I should re-read the patch anyhow, but I also think unusual ordering of
> terminate() with other functions should be tested a bit.
(In reply to Shih-Chiang Chien [:schien] (UTC+8) (use ni? plz) from comment #15)
> (In reply to Olli Pettay [:smaug] from comment #14)
> > Comment on attachment 8770104 [details] [diff] [review]
> > part 2, terminate-webapi.patch
> > 
> > >     case PresentationConnectionState::Terminated: {
> > >+      // ensure onterminate event before closing web page.
> > >+      RefPtr<AsyncEventDispatcher> asyncDispatcher =
> > >+        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
> > >+      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
> > >+
> > >       nsCOMPtr<nsIPresentationService> service =
> > >         do_GetService(PRESENTATION_SERVICE_CONTRACTID);
> > >       if (NS_WARN_IF(!service)) {
> > >         return NS_ERROR_NOT_AVAILABLE;
> > >       }
> > > 
> > >       nsresult rv = service->UnregisterSessionListener(mId, mRole);
> > >       if(NS_WARN_IF(NS_FAILED(rv))) {
> > >         return rv;
> > >       }
> > > 
> > >-      RefPtr<AsyncEventDispatcher> asyncDispatcher =
> > >-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
> > >-      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
> > >-
> > This change is unclear to me. I mean, I don't know why it has any effect.
> > "terminate" will be dispatched asynchronously anyhow.
> > Please explain and improve the comment a bit.
> UnregisterSessionListener is the timing we can use for triggering window
> close (asynchronously) on receiver page after session termination. In order
> to make sure "onterminate" event handler is always invoked before window
> closed, I reorder this code section so that "terminate" event is always
> enqueued in front of window.close.
After revisiting the UnregisterSessionListener usage and discussed with @kershaw, we found it's not necessary to call UntrackSessionInfo in this step because session info object will untrack itself after terminate complete.
So, there will be no restriction on the invocation order. But I think it'll be better to enqueue the terminate event before any early return.
1. remove unnecessary UntrackSessionInfo usage
2. add more test
Attachment #8770104 - Attachment is obsolete: true
Attachment #8770497 - Flags: review?(bugs)
Comment on attachment 8770102 [details] [diff] [review]
part 1, support-terminate-command.patch

Review of attachment 8770102 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/presentation/PresentationTerminateRequest.cpp
@@ +27,5 @@
> +{
> +}
> +
> +// nsIPresentationTerminateRequest
> +

nit: remove this empty line

::: dom/presentation/provider/LegacyPresentationControlService.js
@@ +232,5 @@
>    },
>  
> +  terminate: function() {
> +    // Legacy protocol doesn't support extra terminate protocol.
> +    // Simply close the transport channel.

"Transport channel" is ambiguous. And it's not closed here.
The caller could determine whatever it want to do.

Annotation the intention.

::: dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ +933,5 @@
> +  uint32_t index;
> +  if (FindDeviceByAddress(address, index)) {
> +    device = mDevices[index];
> +  } else {
> +    // create a one-time device object for non-discoverable controller

To make it easy to read, capitalize the first letter and end with a period. Also apply to the next sentence.

@@ +934,5 @@
> +  if (FindDeviceByAddress(address, index)) {
> +    device = mDevices[index];
> +  } else {
> +    // create a one-time device object for non-discoverable controller
> +    // this device will not be listed in available device list and cannot

How about this: "in the list of available devices"?

::: dom/presentation/provider/PresentationControlService.js
@@ +166,5 @@
>    onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
>      DEBUG && log("PresentationControlService - onSessionRequest: " +
>                   aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
> +    if (!this.listener) {
> +      return;

Is there a leakage for controlChannel?

@@ +179,5 @@
>  
> +  onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel) {
> +    DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
> +                 aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
> +    if (!this.listener) {

same here

::: dom/presentation/provider/ReceiverStateMachine.jsm
@@ +94,5 @@
>  
> +  terminate: function _terminate(presentationId) {
> +    if (this.state === State.CONNECTED) {
> +      this._sendCommand({
> +        type: CommandType.TERMINATE_ACK,

Is there a spec or something for this behavior? Since sending an ACK for the first message is weird to me.

::: dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ +193,5 @@
> +      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
> +      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
> +
> +      controllerControlChannel.listener = {
> +        notifyConnected: function() {

It would be better if we check |notifyConnected| is called
Attachment #8770102 - Flags: review?(juhsu) → review+
Comment on attachment 8770102 [details] [diff] [review]
part 1, support-terminate-command.patch

Review of attachment 8770102 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/presentation/PresentationTerminateRequest.cpp
@@ +27,5 @@
> +{
> +}
> +
> +// nsIPresentationTerminateRequest
> +

done.

::: dom/presentation/provider/LegacyPresentationControlService.js
@@ +232,5 @@
>    },
>  
> +  terminate: function() {
> +    // Legacy protocol doesn't support extra terminate protocol.
> +    // Simply close the transport channel.

This is based on our error handling of terminate procedure. If there is any error happened during terminate procedure, browser should simply shutdown all the resource locally.

update the comment as following:
// Legacy protocol doesn't support extra terminate protocol.
// Trigger error handling for browser to shutdown all the resource locally.

::: dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ +933,5 @@
> +  uint32_t index;
> +  if (FindDeviceByAddress(address, index)) {
> +    device = mDevices[index];
> +  } else {
> +    // create a one-time device object for non-discoverable controller

done.

@@ +934,5 @@
> +  if (FindDeviceByAddress(address, index)) {
> +    device = mDevices[index];
> +  } else {
> +    // create a one-time device object for non-discoverable controller
> +    // this device will not be listed in available device list and cannot

done.

::: dom/presentation/provider/PresentationControlService.js
@@ +166,5 @@
>    onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
>      DEBUG && log("PresentationControlService - onSessionRequest: " +
>                   aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
> +    if (!this.listener) {
> +      return;

nice catch and fixed.

@@ +179,5 @@
>  
> +  onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel) {
> +    DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
> +                 aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
> +    if (!this.listener) {

fixed.

::: dom/presentation/provider/ReceiverStateMachine.jsm
@@ +94,5 @@
>  
> +  terminate: function _terminate(presentationId) {
> +    if (this.state === State.CONNECTED) {
> +      this._sendCommand({
> +        type: CommandType.TERMINATE_ACK,

Have write done any spec of it. Simply want to distinguish terminate from controller or receiver in protocol level without adding new command.
Or do you think throwing an exception for local release will be more logical since we don't fully support terminate by receiver?

::: dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ +193,5 @@
> +      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
> +      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
> +
> +      controllerControlChannel.listener = {
> +        notifyConnected: function() {

done.
> ::: dom/presentation/provider/ReceiverStateMachine.jsm
> @@ +94,5 @@
> >  
> > +  terminate: function _terminate(presentationId) {
> > +    if (this.state === State.CONNECTED) {
> > +      this._sendCommand({
> > +        type: CommandType.TERMINATE_ACK,
> 
> Have write done any spec of it. Simply want to distinguish terminate from
> controller or receiver in protocol level without adding new command.
> Or do you think throwing an exception for local release will be more logical
> since we don't fully support terminate by receiver?
Nope, both controller and receiver should be able to terminate.
What I concern is that FOO_ACK is kinda a confirmation of receiving FOO message.

Generally speaking, each endpoint are equal after the connection handshake with respect to control protocol. What would happen if both side sends TERMINATE and replies TERMINATE_ACK?

If you really want to tell the controller and receiver, please consider more commands or attributes.
Comment on attachment 8770497 [details] [diff] [review]
part 2, terminate-webapi.patch, v2

Nits
>+    // Cannot terminate non-existed session
You could be consistent with comments. Like, end sentences with .

>+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
>+    return NS_ERROR_DOM_ABORT_ERR;
>+  }
>+
>+  //check if terminate request comes from known device
And space after // and capital C
Attachment #8770497 - Flags: review?(bugs) → review+
(In reply to Olli Pettay [:smaug] from comment #22)
> Comment on attachment 8770497 [details] [diff] [review]
> part 2, terminate-webapi.patch, v2
> 
> Nits
> >+    // Cannot terminate non-existed session
> You could be consistent with comments. Like, end sentences with .
> 
> >+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
> >+    return NS_ERROR_DOM_ABORT_ERR;
> >+  }
> >+
> >+  //check if terminate request comes from known device
> And space after // and capital C

My bad. Fixed!
(In reply to Junior [:junior] from comment #21)
> > ::: dom/presentation/provider/ReceiverStateMachine.jsm
> > @@ +94,5 @@
> > >  
> > > +  terminate: function _terminate(presentationId) {
> > > +    if (this.state === State.CONNECTED) {
> > > +      this._sendCommand({
> > > +        type: CommandType.TERMINATE_ACK,
> > 
> > Have write done any spec of it. Simply want to distinguish terminate from
> > controller or receiver in protocol level without adding new command.
> > Or do you think throwing an exception for local release will be more logical
> > since we don't fully support terminate by receiver?
> Nope, both controller and receiver should be able to terminate.
> What I concern is that FOO_ACK is kinda a confirmation of receiving FOO
> message.
> 
> Generally speaking, each endpoint are equal after the connection handshake
> with respect to control protocol. What would happen if both side sends
> TERMINATE and replies TERMINATE_ACK?
> 
> If you really want to tell the controller and receiver, please consider more
> commands or attributes.

Ok, I'll make both controller and receiver to send TERMINATE while initiating terminate procedure and add one "isFromReceiver" in nsIPresentationTerminateRequest. PresentationService can based on this information to trigger termination procedure on correct session info object.
update according to review comment and rebase to latest m-c, carry r+.
Attachment #8770102 - Attachment is obsolete: true
Attachment #8771231 - Flags: review+
update according to review comment and rebase to latest m-c, carry r+.
Attachment #8770497 - Attachment is obsolete: true
Attachment #8771232 - Flags: review+
Address the previous comments.
Attachment #8771231 - Attachment is obsolete: true
Attachment #8771367 - Flags: review?(juhsu)
Comment on attachment 8771231 [details] [diff] [review]
part 1, support-terminate-command.patch

># HG changeset patch
># User Shih-Chiang Chien <schien@mozilla.com>
># Date 1464304947 25200
>#      Thu May 26 16:22:27 2016 -0700
># Node ID 5e22cc693a2549dffe0a824e4eb488e935ec2626
># Parent  44371bdabb22d94337089e55d9e36a70dcff2eef
>Bug 1276378 - Part 1, add terminate command in control protocol. r=junior.
>
>MozReview-Commit-ID: BwfJKcXmN07
>
>diff --git a/dom/presentation/PresentationDeviceManager.cpp b/dom/presentation/PresentationDeviceManager.cpp
>--- a/dom/presentation/PresentationDeviceManager.cpp
>+++ b/dom/presentation/PresentationDeviceManager.cpp
>@@ -9,16 +9,17 @@
> #include "mozilla/Services.h"
> #include "MainThreadUtils.h"
> #include "nsCategoryCache.h"
> #include "nsCOMPtr.h"
> #include "nsIMutableArray.h"
> #include "nsIObserverService.h"
> #include "nsXULAppAPI.h"
> #include "PresentationSessionRequest.h"
>+#include "PresentationTerminateRequest.h"
> 
> namespace mozilla {
> namespace dom {
> 
> NS_IMPL_ISUPPORTS(PresentationDeviceManager,
>                   nsIPresentationDeviceManager,
>                   nsIPresentationDeviceListener,
>                   nsIObserver,
>@@ -234,16 +235,38 @@ PresentationDeviceManager::OnSessionRequ
>     new PresentationSessionRequest(aDevice, aUrl, aPresentationId, aControlChannel);
>   obs->NotifyObservers(request,
>                        PRESENTATION_SESSION_REQUEST_TOPIC,
>                        nullptr);
> 
>   return NS_OK;
> }
> 
>+NS_IMETHODIMP
>+PresentationDeviceManager::OnTerminateRequest(nsIPresentationDevice* aDevice,
>+                                              const nsAString& aPresentationId,
>+                                              nsIPresentationControlChannel* aControlChannel,
>+                                              bool aIsFromReceiver)
>+{
>+  NS_ENSURE_ARG(aDevice);
>+  NS_ENSURE_ARG(aControlChannel);
>+
>+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
>+  NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
>+
>+  RefPtr<PresentationTerminateRequest> request =
>+    new PresentationTerminateRequest(aDevice, aPresentationId,
>+                                     aControlChannel, aIsFromReceiver);
>+  obs->NotifyObservers(request,
>+                       PRESENTATION_TERMINATE_REQUEST_TOPIC,
>+                       nullptr);
>+
>+  return NS_OK;
>+}
>+
> // nsIObserver
> NS_IMETHODIMP
> PresentationDeviceManager::Observe(nsISupports *aSubject,
>                                    const char *aTopic,
>                                    const char16_t *aData)
> {
>   if (!strcmp(aTopic, "profile-after-change")) {
>     Init();
>diff --git a/dom/presentation/PresentationTerminateRequest.cpp b/dom/presentation/PresentationTerminateRequest.cpp
>new file mode 100644
>--- /dev/null
>+++ b/dom/presentation/PresentationTerminateRequest.cpp
>@@ -0,0 +1,73 @@
>+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
>+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
>+/* This Source Code Form is subject to the terms of the Mozilla Public
>+ * License, v. 2.0. If a copy of the MPL was not distributed with this
>+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
>+
>+#include "PresentationTerminateRequest.h"
>+#include "nsIPresentationControlChannel.h"
>+#include "nsIPresentationDevice.h"
>+
>+namespace mozilla {
>+namespace dom {
>+
>+NS_IMPL_ISUPPORTS(PresentationTerminateRequest, nsIPresentationTerminateRequest)
>+
>+PresentationTerminateRequest::PresentationTerminateRequest(
>+                                 nsIPresentationDevice* aDevice,
>+                                 const nsAString& aPresentationId,
>+                                 nsIPresentationControlChannel* aControlChannel,
>+                                 bool aIsFromReceiver)
>+  : mPresentationId(aPresentationId)
>+  , mDevice(aDevice)
>+  , mControlChannel(aControlChannel)
>+  , mIsFromReceiver(aIsFromReceiver)
>+{
>+}
>+
>+PresentationTerminateRequest::~PresentationTerminateRequest()
>+{
>+}
>+
>+// nsIPresentationTerminateRequest
>+NS_IMETHODIMP
>+PresentationTerminateRequest::GetDevice(nsIPresentationDevice** aRetVal)
>+{
>+  NS_ENSURE_ARG_POINTER(aRetVal);
>+
>+  nsCOMPtr<nsIPresentationDevice> device = mDevice;
>+  device.forget(aRetVal);
>+
>+  return NS_OK;
>+}
>+
>+NS_IMETHODIMP
>+PresentationTerminateRequest::GetPresentationId(nsAString& aRetVal)
>+{
>+  aRetVal = mPresentationId;
>+
>+  return NS_OK;
>+}
>+
>+NS_IMETHODIMP
>+PresentationTerminateRequest::GetControlChannel(
>+                                        nsIPresentationControlChannel** aRetVal)
>+{
>+  NS_ENSURE_ARG_POINTER(aRetVal);
>+
>+  nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel;
>+  controlChannel.forget(aRetVal);
>+
>+  return NS_OK;
>+}
>+
>+NS_IMETHODIMP
>+PresentationTerminateRequest::GetIsFromReceiver(bool* aRetVal)
>+{
>+  *aRetVal = mIsFromReceiver;
>+
>+  return NS_OK;
>+}
>+
>+} // namespace dom
>+} // namespace mozilla
>diff --git a/dom/presentation/PresentationTerminateRequest.h b/dom/presentation/PresentationTerminateRequest.h
>new file mode 100644
>--- /dev/null
>+++ b/dom/presentation/PresentationTerminateRequest.h
>@@ -0,0 +1,41 @@
>+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
>+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
>+/* This Source Code Form is subject to the terms of the Mozilla Public
>+ * License, v. 2.0. If a copy of the MPL was not distributed with this
>+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
>+
>+#ifndef mozilla_dom_PresentationTerminateRequest_h__
>+#define mozilla_dom_PresentationTerminateRequest_h__
>+
>+#include "nsIPresentationTerminateRequest.h"
>+#include "nsCOMPtr.h"
>+#include "nsString.h"
>+
>+namespace mozilla {
>+namespace dom {
>+
>+class PresentationTerminateRequest final : public nsIPresentationTerminateRequest
>+{
>+public:
>+  NS_DECL_ISUPPORTS
>+  NS_DECL_NSIPRESENTATIONTERMINATEREQUEST
>+
>+  PresentationTerminateRequest(nsIPresentationDevice* aDevice,
>+                               const nsAString& aPresentationId,
>+                               nsIPresentationControlChannel* aControlChannel,
>+                               bool aIsFromReceiver);
>+
>+private:
>+  virtual ~PresentationTerminateRequest();
>+
>+  nsString mPresentationId;
>+  nsCOMPtr<nsIPresentationDevice> mDevice;
>+  nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
>+  bool mIsFromReceiver;
>+};
>+
>+} // namespace dom
>+} // namespace mozilla
>+
>+#endif /* mozilla_dom_PresentationTerminateRequest_h__ */
>+
>diff --git a/dom/presentation/interfaces/moz.build b/dom/presentation/interfaces/moz.build
>--- a/dom/presentation/interfaces/moz.build
>+++ b/dom/presentation/interfaces/moz.build
>@@ -13,16 +13,17 @@ XPIDL_SOURCES += [
>     'nsIPresentationDeviceProvider.idl',
>     'nsIPresentationListener.idl',
>     'nsIPresentationLocalDevice.idl',
>     'nsIPresentationRequestUIGlue.idl',
>     'nsIPresentationService.idl',
>     'nsIPresentationSessionRequest.idl',
>     'nsIPresentationSessionTransport.idl',
>     'nsIPresentationSessionTransportBuilder.idl',
>+    'nsIPresentationTerminateRequest.idl',
> ]
> 
> if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
>     XPIDL_SOURCES += [
>         'nsIPresentationNetworkHelper.idl',
>     ]
> 
> XPIDL_MODULE = 'dom_presentation'
>diff --git a/dom/presentation/interfaces/nsIPresentationControlChannel.idl b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
>--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
>+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
>@@ -106,13 +106,20 @@ interface nsIPresentationControlChannel:
>    * 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 launch(in DOMString presentationId, in DOMString url);
> 
>   /*
>+   * Terminate a presentation on remote endpoint.
>+   * @param presentationId The Id for representing this session.
>+   * @throws NS_ERROR_FAILURE on failure
>+   */
>+  void terminate(in DOMString presentationId);
>+
>+  /*
>    * Disconnect the control channel.
>    * @param reason The reason of disconnecting channel; NS_OK represents normal.
>    */
>   void disconnect(in nsresult reason);
> };
>diff --git a/dom/presentation/interfaces/nsIPresentationControlService.idl b/dom/presentation/interfaces/nsIPresentationControlService.idl
>--- a/dom/presentation/interfaces/nsIPresentationControlService.idl
>+++ b/dom/presentation/interfaces/nsIPresentationControlService.idl
>@@ -40,16 +40,28 @@ interface nsIPresentationControlServerLi
>    * @param aUrl The URL requested to open by remote device.
>    * @param aPresentationId The Id for representing this session.
>    * @param aControlChannel The control channel for this session.
>    */
>   void onSessionRequest(in nsITCPDeviceInfo aDeviceInfo,
>                         in DOMString aUrl,
>                         in DOMString aPresentationId,
>                         in nsIPresentationControlChannel aControlChannel);
>+
>+  /**
>+   * Callback while the remote host is requesting to terminate a presentation session.
>+   * @param aDeviceInfo The device information related to the remote host.
>+   * @param aPresentationId The Id for representing this session.
>+   * @param aControlChannel The control channel for this session.
>+   * @param aIsFromReceiver true if termination is initiated by receiver.
>+   */
>+  void onTerminateRequest(in nsITCPDeviceInfo aDeviceInfo,
>+                          in DOMString aPresentationId,
>+                          in nsIPresentationControlChannel aControlChannel,
>+                          in boolean aIsFromReceiver);
> };
> 
> /**
>  * Presentation control service which can be used for both presentation
>  * control client and server.
>  */
> [scriptable, uuid(55d6b605-2389-4aae-a8fe-60d4440540ea)]
> interface nsIPresentationControlService: nsISupports
>diff --git a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
>--- a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
>+++ b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
>@@ -27,16 +27,28 @@ interface nsIPresentationDeviceListener:
>    * @param url The URL requested to open by remote device.
>    * @param presentationId The Id for representing this session.
>    * @param controlChannel The control channel for this session.
>    */
>   void onSessionRequest(in nsIPresentationDevice device,
>                         in DOMString url,
>                         in DOMString presentationId,
>                         in nsIPresentationControlChannel controlChannel);
>+
>+  /*
>+   * Callback while the remote device is requesting to terminate a presentation session.
>+   * @param device The remote device that sent session request.
>+   * @param presentationId The Id for representing this session.
>+   * @param controlChannel The control channel for this session.
>+   * @param aIsFromReceiver true if termination is initiated by receiver.
>+   */
>+  void onTerminateRequest(in nsIPresentationDevice device,
>+                          in DOMString presentationId,
>+                          in nsIPresentationControlChannel controlChannel,
>+                          in boolean aIsFromReceiver);
> };
> 
> /*
>  * Device provider for any device protocol, can be registered as default
>  * providers by adding its contractID to category "presentation-device-provider".
>  */
> [scriptable, uuid(3db2578a-0f50-44ad-b01b-28427b71b7bf)]
> interface nsIPresentationDeviceProvider: nsISupports
>diff --git a/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
>new file mode 100644
>--- /dev/null
>+++ b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
>@@ -0,0 +1,33 @@
>+/* 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;
>+interface nsIPresentationControlChannel;
>+
>+%{C++
>+#define PRESENTATION_TERMINATE_REQUEST_TOPIC "presentation-terminate-request"
>+%}
>+
>+/*
>+ * The event of a device requesting for terminating a presentation session. User can
>+ * monitor the terminate request on every device by observing "presentation-terminate-request".
>+ */
>+[scriptable, uuid(3ddbf3a4-53ee-4b70-9bbc-58ac90dce6b5)]
>+interface nsIPresentationTerminateRequest: nsISupports
>+{
>+  // The device which requesting to terminate presentation session.
>+  readonly attribute nsIPresentationDevice device;
>+
>+  // The Id for representing this session.
>+  readonly attribute DOMString presentationId;
>+
>+  // The control channel for this session.
>+  // Should only use this channel to complete session termination.
>+  readonly attribute nsIPresentationControlChannel controlChannel;
>+
>+  // True if termination is initiated by receiver.
>+  readonly attribute boolean isFromReceiver;
>+};
>diff --git a/dom/presentation/moz.build b/dom/presentation/moz.build
>--- a/dom/presentation/moz.build
>+++ b/dom/presentation/moz.build
>@@ -47,16 +47,17 @@ UNIFIED_SOURCES += [
>     'PresentationDeviceManager.cpp',
>     'PresentationReceiver.cpp',
>     'PresentationRequest.cpp',
>     'PresentationService.cpp',
>     'PresentationServiceBase.cpp',
>     'PresentationSessionInfo.cpp',
>     'PresentationSessionRequest.cpp',
>     'PresentationTCPSessionTransport.cpp',
>+    'PresentationTerminateRequest.cpp',
> ]
> 
> EXTRA_COMPONENTS += [
>     'PresentationDataChannelSessionTransport.js',
>     'PresentationDataChannelSessionTransport.manifest',
>     'PresentationDeviceInfoManager.js',
>     'PresentationDeviceInfoManager.manifest',
> ]
>diff --git a/dom/presentation/provider/ControllerStateMachine.jsm b/dom/presentation/provider/ControllerStateMachine.jsm
>--- a/dom/presentation/provider/ControllerStateMachine.jsm
>+++ b/dom/presentation/provider/ControllerStateMachine.jsm
>@@ -45,16 +45,22 @@ var handlers = [
>     switch (command.type) {
>       case CommandType.DISCONNECT:
>         stateMachine.state = State.CLOSED;
>         stateMachine._notifyDisconnected(command.reason);
>         break;
>       case CommandType.LAUNCH_ACK:
>         stateMachine._notifyLaunch(command.presentationId);
>         break;
>+      case CommandType.TERMINATE:
>+        stateMachine._notifyTerminate(command.presentationId);
>+        break;
>+      case CommandType.TERMINATE_ACK:
>+        stateMachine._notifyTerminate(command.presentationId);
>+        break;
>       case CommandType.ANSWER:
>       case CommandType.ICE_CANDIDATE:
>         stateMachine._notifyChannelDescriptor(command);
>         break;
>       default:
>         debug("unexpected command: " + JSON.stringify(command));
>         // ignore unexpected command.
>         break;
>@@ -82,16 +88,34 @@ ControllerStateMachine.prototype = {
>       this._sendCommand({
>         type: CommandType.LAUNCH,
>         presentationId: presentationId,
>         url: url,
>       });
>     }
>   },
> 
>+  terminate: function _terminate(presentationId) {
>+    if (this.state === State.CONNECTED) {
>+      this._sendCommand({
>+        type: CommandType.TERMINATE,
>+        presentationId: presentationId,
>+      });
>+    }
>+  },
>+
>+  terminateAck: function _terminateAck(presentationId) {
>+    if (this.state === State.CONNECTED) {
>+      this._sendCommand({
>+        type: CommandType.TERMINATE_ACK,
>+        presentationId: presentationId,
>+      });
>+    }
>+  },
>+
>   sendOffer: function _sendOffer(offer) {
>     if (this.state === State.CONNECTED) {
>       this._sendCommand({
>         type: CommandType.OFFER,
>         offer: offer,
>       });
>     }
>   },
>@@ -167,16 +191,20 @@ ControllerStateMachine.prototype = {
>   _notifyDisconnected: function _notifyDisconnected(reason) {
>     this._channel.notifyDisconnected(reason);
>   },
> 
>   _notifyLaunch: function _notifyLaunch(presentationId) {
>     this._channel.notifyLaunch(presentationId);
>   },
> 
>+  _notifyTerminate: function _notifyTerminate(presentationId) {
>+    this._channel.notifyTerminate(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;
>diff --git a/dom/presentation/provider/DisplayDeviceProvider.cpp b/dom/presentation/provider/DisplayDeviceProvider.cpp
>--- a/dom/presentation/provider/DisplayDeviceProvider.cpp
>+++ b/dom/presentation/provider/DisplayDeviceProvider.cpp
>@@ -400,16 +400,47 @@ DisplayDeviceProvider::OnSessionRequest(
>                                   aControlChannel);
>   if (NS_WARN_IF(NS_FAILED(rv))) {
>     return rv;
>   }
> 
>   return NS_OK;
> }
> 
>+NS_IMETHODIMP
>+DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
>+                                          const nsAString& aPresentationId,
>+                                          nsIPresentationControlChannel* aControlChannel,
>+                                          bool aIsFromReceiver)
>+{
>+  MOZ_ASSERT(NS_IsMainThread());
>+  MOZ_ASSERT(aDeviceInfo);
>+  MOZ_ASSERT(aControlChannel);
>+
>+  nsresult rv;
>+
>+  nsCOMPtr<nsIPresentationDeviceListener> listener;
>+  rv = GetListener(getter_AddRefs(listener));
>+  if (NS_WARN_IF(NS_FAILED(rv))) {
>+    return rv;
>+  }
>+
>+  MOZ_ASSERT(!listener);
>+
>+  rv = listener->OnTerminateRequest(mDevice,
>+                                    aPresentationId,
>+                                    aControlChannel,
>+                                    aIsFromReceiver);
>+  if (NS_WARN_IF(NS_FAILED(rv))) {
>+    return rv;
>+  }
>+
>+  return NS_OK;
>+}
>+
> // nsIObserver
> NS_IMETHODIMP
> DisplayDeviceProvider::Observe(nsISupports* aSubject,
>                                const char* aTopic,
>                                const char16_t* aData)
> {
>   if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) {
>     nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
>diff --git a/dom/presentation/provider/LegacyPresentationControlService.js b/dom/presentation/provider/LegacyPresentationControlService.js
>--- a/dom/presentation/provider/LegacyPresentationControlService.js
>+++ b/dom/presentation/provider/LegacyPresentationControlService.js
>@@ -226,16 +226,22 @@ LegacyTCPControlChannel.prototype = {
> 
>   launch: function(aPresentationId, aUrl) {
>     this._presentationId = aPresentationId;
>     this._url = aUrl;
> 
>     this._sendInit();
>   },
> 
>+  terminate: function() {
>+    // Legacy protocol doesn't support extra terminate protocol.
>+    // Trigger error handling for browser to shutdown all the resource locally.
>+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
>+  },
>+
>   sendOffer: function(aOffer) {
>     let msg = {
>       type: "requestSession:Offer",
>       presentationId: this._presentationId,
>       offer: discriptionAsJson(aOffer),
>     };
>     this._sendMessage(msg);
>   },
>diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
>--- a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
>+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
>@@ -938,16 +938,60 @@ MulticastDNSDeviceProvider::OnSessionReq
>   if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
>     Unused << listener->OnSessionRequest(device, aUrl, aPresentationId,
>                                          aControlChannel);
>   }
> 
>   return NS_OK;
> }
> 
>+NS_IMETHODIMP
>+MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
>+                                               const nsAString& aPresentationId,
>+                                               nsIPresentationControlChannel* aControlChannel,
>+                                               bool aIsFromReceiver)
>+{
>+  MOZ_ASSERT(NS_IsMainThread());
>+
>+  nsAutoCString address;
>+  Unused << aDeviceInfo->GetAddress(address);
>+
>+  LOG_I("OnTerminateRequest: %s", address.get());
>+
>+  RefPtr<Device> device;
>+  uint32_t index;
>+  if (FindDeviceByAddress(address, index)) {
>+    device = mDevices[index];
>+  } else {
>+    // Create a one-time device object for non-discoverable controller.
>+    // This device will not be in the list of available devices and cannot
>+    // be used for requesting session.
>+    nsAutoCString id;
>+    Unused << aDeviceInfo->GetId(id);
>+    uint16_t port;
>+    Unused << aDeviceInfo->GetPort(&port);
>+
>+    device = new Device(id,
>+                        /* aName = */ id,
>+                        /* aType = */ EmptyCString(),
>+                        address,
>+                        port,
>+                        DeviceState::eActive,
>+                        /* aProvider = */ nullptr);
>+  }
>+
>+  nsCOMPtr<nsIPresentationDeviceListener> listener;
>+  if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
>+    Unused << listener->OnTerminateRequest(device, aPresentationId,
>+                                           aControlChannel, aIsFromReceiver);
>+  }
>+
>+  return NS_OK;
>+}
>+
> // nsIObserver
> NS_IMETHODIMP
> MulticastDNSDeviceProvider::Observe(nsISupports* aSubject,
>                                     const char* aTopic,
>                                     const char16_t* aData)
> {
>   MOZ_ASSERT(NS_IsMainThread());
> 
>diff --git a/dom/presentation/provider/PresentationControlService.js b/dom/presentation/provider/PresentationControlService.js
>--- a/dom/presentation/provider/PresentationControlService.js
>+++ b/dom/presentation/provider/PresentationControlService.js
>@@ -161,23 +161,43 @@ PresentationControlService.prototype = {
>                                  aDeviceInfo,
>                                  "receiver");
>   },
> 
>   // Triggered by TCPControlChannel
>   onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
>     DEBUG && log("PresentationControlService - onSessionRequest: " +
>                  aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
>+    if (!this.listener) {
>+      this.releaseControlChannel(aControlChannel);
>+      return;
>+    }
>+
>     this.listener.onSessionRequest(aDeviceInfo,
>                                    aUrl,
>                                    aPresentationId,
>                                    aControlChannel);
>     this.releaseControlChannel(aControlChannel);
>   },
> 
>+  onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) {
>+    DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
>+                 aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
>+    if (!this.listener) {
>+      this.releaseControlChannel(aControlChannel);
>+      return;
>+    }
>+
>+    this.listener.onTerminateRequest(aDeviceInfo,
>+                                     aPresentationId,
>+                                     aControlChannel,
>+                                     aIsFromReceiver);
>+    this.releaseControlChannel(aControlChannel);
>+  },
>+
>   // nsIServerSocketListener (Triggered by nsIServerSocket.init)
>   onSocketAccepted: function(aServerSocket, aClientSocket) {
>     DEBUG && log("PresentationControlService - onSocketAccepted: " +
>                  aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line
>     let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
>     this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket));
>   },
> 
>@@ -385,16 +405,26 @@ TCPControlChannel.prototype = {
>   sendIceCandidate: function(aCandidate) {
>     this._stateMachine.updateIceCandidate(aCandidate);
>   },
> 
>   launch: function(aPresentationId, aUrl) {
>     this._stateMachine.launch(aPresentationId, aUrl);
>   },
> 
>+  terminate: function(aPresentationId) {
>+    if (!this._terminatingId) {
>+      this._terminatingId = aPresentationId;
>+      this._stateMachine.terminate(aPresentationId);
>+    } else {
>+      this._stateMachine.terminateAck(aPresentationId);
>+      delete this._terminatingId;
>+    }
>+  },
>+
>   // 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
>@@ -645,16 +675,36 @@ TCPControlChannel.prototype = {
>         this._presentationService.onSessionRequest(this._deviceInfo,
>                                                    url,
>                                                    presentationId,
>                                                    this);
>       break;
>     }
>   },
> 
>+  notifyTerminate: function(presentationId) {
>+    if (!this._terminatingId) {
>+      this._terminatingId = presentationId;
>+      this._presentationService.onSessionTerminate(this._deviceInfo,
>+                                                  presentationId,
>+                                                  this,
>+                                                  this._direction === "sender");
>+      return;
>+    }
>+
>+    if (this._terminatingId !== presentationId) {
>+      // Requested presentation Id doesn't matched with the one in ACK.
>+      // Disconnect the control channel with error.
>+      DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line
>+      this.disconnect(Cr.NS_ERROR_FAILURE);
>+    }
>+
>+    delete this._terminatingId;
>+  },
>+
>   notifyOffer: function(offer) {
>     this._onOffer(offer);
>   },
> 
>   notifyAnswer: function(answer) {
>     this._onAnswer(answer);
>   },
> 
>diff --git a/dom/presentation/provider/ReceiverStateMachine.jsm b/dom/presentation/provider/ReceiverStateMachine.jsm
>--- a/dom/presentation/provider/ReceiverStateMachine.jsm
>+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
>@@ -53,16 +53,22 @@ var handlers = [
>       case CommandType.LAUNCH:
>         stateMachine._notifyLaunch(command.presentationId,
>                                    command.url);
>         stateMachine._sendCommand({
>           type: CommandType.LAUNCH_ACK,
>           presentationId: command.presentationId
>         });
>         break;
>+      case CommandType.TERMINATE:
>+        stateMachine._notifyTerminate(command.presentationId);
>+        break;
>+      case CommandType.TERMINATE_ACK:
>+        stateMachine._notifyTerminate(command.presentationId);
>+        break;
>       case CommandType.OFFER:
>       case CommandType.ICE_CANDIDATE:
>         stateMachine._notifyChannelDescriptor(command);
>         break;
>       default:
>         debug("unexpected command: " + JSON.stringify(command));
>         // ignore unexpected command
>         break;
>@@ -84,16 +90,34 @@ function ReceiverStateMachine(channel) {
> }
> 
> ReceiverStateMachine.prototype = {
>   launch: function _launch() {
>     // presentation session can only be launched by controlling UA.
>     debug("receiver shouldn't trigger launch");
>   },
> 
>+  terminate: function _terminate(presentationId) {
>+    if (this.state === State.CONNECTED) {
>+      this._sendCommand({
>+        type: CommandType.TERMINATE,
>+        presentationId: presentationId,
>+      });
>+    }
>+  },
>+
>+  terminateAck: function _terminateAck(presentationId) {
>+    if (this.state === State.CONNECTED) {
>+      this._sendCommand({
>+        type: CommandType.TERMINATE_ACK,
>+        presentationId: presentationId,
>+      });
>+    }
>+  },
>+
>   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({
>@@ -166,16 +190,20 @@ ReceiverStateMachine.prototype = {
>   _notifyDisconnected: function _notifyDisconnected(reason) {
>     this._channel.notifyDisconnected(reason);
>   },
> 
>   _notifyLaunch: function _notifyLaunch(presentationId, url) {
>     this._channel.notifyLaunch(presentationId, url);
>   },
> 
>+  _notifyTerminate: function _notifyTerminate(presentationId) {
>+    this._channel.notifyTerminate(presentationId);
>+  },
>+
>   _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;
>diff --git a/dom/presentation/provider/StateMachineHelper.jsm b/dom/presentation/provider/StateMachineHelper.jsm
>--- a/dom/presentation/provider/StateMachineHelper.jsm
>+++ b/dom/presentation/provider/StateMachineHelper.jsm
>@@ -20,16 +20,18 @@ const State = Object.freeze({
> const CommandType = Object.freeze({
>   // control channel life cycle
>   CONNECT: "connect", // { deviceId: <string> }
>   CONNECT_ACK: "connect-ack", // { presentationId: <string> }
>   DISCONNECT: "disconnect", // { reason: <int> }
>   // presentation session life cycle
>   LAUNCH: "launch", // { presentationId: <string>, url: <string> }
>   LAUNCH_ACK: "launch-ack", // { presentationId: <string> }
>+  TERMINATE: "terminate", // { presentationId: <string> }
>+  TERMINATE_ACK: "terminate-ack", // { presentationId: <string> }
>   // session transport establishment
>   OFFER: "offer", // { offer: <json> }
>   ANSWER: "answer", // { answer: <json> }
>   ICE_CANDIDATE: "ice-candidate", // { candidate: <string> }
> });
> 
> this.State = State; // jshint ignore:line
> this.CommandType = CommandType; // jshint ignore:line
>diff --git a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
>--- a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
>+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
>@@ -18,16 +18,17 @@ function TestPresentationDevice() {}
> function TestPresentationControlChannel() {}
> 
> TestPresentationControlChannel.prototype = {
>   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
>   sendOffer: function(offer) {},
>   sendAnswer: function(answer) {},
>   disconnect: function() {},
>   launch: function() {},
>+  terminate: function() {},
>   set listener(listener) {},
>   get listener() {},
> };
> 
> var testProvider = {
>   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
> 
>   forceDiscovery: function() {
>@@ -134,16 +135,37 @@ function sessionRequest() {
>     Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
> 
>     run_next_test();
>   }, 'presentation-session-request', false);
>   manager.QueryInterface(Ci.nsIPresentationDeviceListener)
>          .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel);
> }
> 
>+function terminateRequest() {
>+  let testUrl = 'http://www.example.org/';
>+  let testPresentationId = 'test-presentation-id';
>+  let testControlChannel = new TestPresentationControlChannel();
>+  let testIsFromReceiver = true;
>+  Services.obs.addObserver(function observer(subject, topic, data) {
>+    Services.obs.removeObserver(observer, topic);
>+
>+    let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest);
>+
>+    Assert.equal(request.device.id, testDevice.id, 'expected device');
>+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
>+    Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver');
>+
>+    run_next_test();
>+  }, 'presentation-terminate-request', false);
>+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
>+         .onTerminateRequest(testDevice, testPresentationId,
>+                             testControlChannel, testIsFromReceiver);
>+}
>+
> function removeDevice() {
>   Services.obs.addObserver(function observer(subject, topic, data) {
>     Services.obs.removeObserver(observer, topic);
> 
>     let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
>     Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
>     Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
>     Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
>@@ -171,14 +193,15 @@ function removeProvider() {
>   manager.removeDeviceProvider(testProvider);
> }
> 
> add_test(addProvider);
> add_test(forceDiscovery);
> add_test(addDevice);
> add_test(updateDevice);
> add_test(sessionRequest);
>+add_test(terminateRequest);
> add_test(removeDevice);
> add_test(removeProvider);
> 
> function run_test() {
>   run_next_test();
> }
>diff --git a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
>--- a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
>+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
>@@ -73,16 +73,55 @@ function launch() {
>       Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
>       Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
> 
>       run_next_test();
>     };
>   };
> }
> 
>+function terminateByController() {
>+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
>+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
>+
>+  controllerState.terminate(testPresentationId);
>+  mockReceiverChannel.notifyTerminate = function(presentationId) {
>+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
>+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
>+
>+    mockControllerChannel.notifyTerminate = function(presentationId) {
>+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
>+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
>+
>+      run_next_test();
>+    };
>+
>+    receiverState.terminateAck(presentationId);
>+  };
>+}
>+
>+function terminateByReceiver() {
>+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
>+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
>+
>+  receiverState.terminate(testPresentationId);
>+  mockControllerChannel.notifyTerminate = function(presentationId) {
>+    Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
>+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
>+
>+    mockReceiverChannel.notifyTerminate = function(presentationId) {
>+      Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
>+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
>+      run_next_test();
>+    };
>+
>+    controllerState.terminateAck(presentationId);
>+  };
>+}
>+
> 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);
>@@ -180,16 +219,18 @@ function abnormalDisconnect() {
>       run_next_test();
>     };
>     controllerState.onChannelClosed(Cr.NS_OK, true);
>   };
> }
> 
> add_test(connect);
> add_test(launch);
>+add_test(terminateByController);
>+add_test(terminateByReceiver);
> add_test(exchangeSDP);
> add_test(disconnect);
> add_test(receiverDisconnect);
> add_test(abnormalDisconnect);
> 
> function run_test() { // jshint ignore:line
>   run_next_test();
> }
>diff --git a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
>--- a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
>+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
>@@ -176,16 +176,185 @@ function testPresentationServer() {
>       this.status = 'closed';
>       Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify closed');
>       yayFuncs.presenterControlChannelClose();
>     },
>     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>   };
> }
> 
>+function terminateRequest() {
>+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
>+                                   'controllerControlChannelDisconnected',
>+                                   'presenterControlChannelDisconnected']);
>+  let controllerControlChannel;
>+
>+  pcs.listener = {
>+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiverj) {
>+      controllerControlChannel = controlChannel;
>+      Assert.equal(deviceInfo.id, pcs.id, 'expected device id');
>+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
>+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
>+      Assert.equal(isFromReceiver, false, 'expected request from controller');
>+
>+      controllerControlChannel.listener = {
>+        notifyConnected: function() {
>+          Assert.ok(true, 'control channel notify connected');
>+          yayFuncs.controllerControlChannelConnected();
>+        },
>+        notifyDisconnected: function(aReason) {
>+          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
>+          yayFuncs.controllerControlChannelDisconnected();
>+        },
>+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+      };
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
>+  };
>+
>+  let presenterDeviceInfo = {
>+    id: 'presentatorID',
>+    address: '127.0.0.1',
>+    port: PRESENTER_CONTROL_CHANNEL_PORT,
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
>+  };
>+
>+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
>+
>+  presenterControlChannel.listener = {
>+    notifyConnected: function() {
>+      presenterControlChannel.terminate('testPresentationId', 'http://example.com');
>+      presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
>+    },
>+    notifyDisconnected: function(aReason) {
>+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
>+      yayFuncs.presenterControlChannelDisconnected();
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+  };
>+}
>+
>+function terminateRequest() {
>+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
>+                                   'controllerControlChannelDisconnected',
>+                                   'presenterControlChannelDisconnected',
>+                                   'terminatedByController',
>+                                   'terminatedByReceiver']);
>+  let controllerControlChannel;
>+  let terminatePhase = 'controller';
>+
>+  pcs.listener = {
>+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
>+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
>+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
>+      controlChannel.terminate(presentationId); // Reply terminate ack.
>+
>+      if (terminatePhase === 'controller') {
>+        controllerControlChannel = controlChannel;
>+        Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
>+        Assert.equal(isFromReceiver, false, 'expected request from controller');
>+        yayFuncs.terminatedByController();
>+
>+        controllerControlChannel.listener = {
>+          notifyConnected: function() {
>+            Assert.ok(true, 'control channel notify connected');
>+            yayFuncs.controllerControlChannelConnected();
>+
>+            terminatePhase = 'receiver';
>+            controllerControlChannel.terminate('testPresentationId');
>+          },
>+          notifyDisconnected: function(aReason) {
>+            Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
>+            yayFuncs.controllerControlChannelDisconnected();
>+          },
>+          QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+        };
>+      } else {
>+        Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id');
>+        Assert.equal(isFromReceiver, true, 'expected request from receiver');
>+        yayFuncs.terminatedByReceiver();
>+        presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
>+      }
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
>+  };
>+
>+  let presenterDeviceInfo = {
>+    id: 'presentatorID',
>+    address: '127.0.0.1',
>+    port: PRESENTER_CONTROL_CHANNEL_PORT,
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
>+  };
>+
>+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
>+
>+  presenterControlChannel.listener = {
>+    notifyConnected: function() {
>+      presenterControlChannel.terminate('testPresentationId');
>+    },
>+    notifyDisconnected: function(aReason) {
>+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
>+      yayFuncs.presenterControlChannelDisconnected();
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+  };
>+}
>+
>+function terminateRequestAbnormal() {
>+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
>+                                   'controllerControlChannelDisconnected',
>+                                   'presenterControlChannelDisconnected']);
>+  let controllerControlChannel;
>+
>+  pcs.listener = {
>+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
>+      Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
>+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
>+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
>+      Assert.equal(isFromReceiver, false, 'expected request from controller');
>+      controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack.
>+
>+      controllerControlChannel = controlChannel;
>+
>+      controllerControlChannel.listener = {
>+          notifyConnected: function() {
>+          Assert.ok(true, 'control channel notify connected');
>+          yayFuncs.controllerControlChannelConnected();
>+        },
>+        notifyDisconnected: function(aReason) {
>+          Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error');
>+          yayFuncs.controllerControlChannelDisconnected();
>+        },
>+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+      };
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
>+  };
>+
>+  let presenterDeviceInfo = {
>+    id: 'presentatorID',
>+    address: '127.0.0.1',
>+    port: PRESENTER_CONTROL_CHANNEL_PORT,
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
>+  };
>+
>+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
>+
>+  presenterControlChannel.listener = {
>+    notifyConnected: function() {
>+      presenterControlChannel.terminate('testPresentationId');
>+    },
>+    notifyDisconnected: function(aReason) {
>+      Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error');
>+      yayFuncs.presenterControlChannelDisconnected();
>+    },
>+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
>+  };
>+}
>+
> function setOffline() {
>   pcs.listener = {
>     onPortChange: function(aPort) {
>       Assert.notEqual(aPort, 0, 'TCPPresentationServer port changed and the port should be valid');
>       pcs.close();
>       run_next_test();
>     },
>   };
>@@ -220,16 +389,18 @@ function shutdown()
> 
> // Test manually close control channel with NS_ERROR_FAILURE
> function changeCloseReason() {
>   CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_ERROR_FAILURE;
>   run_next_test();
> }
> 
> add_test(loopOfferAnser);
>+add_test(terminateRequest);
>+add_test(terminateRequestAbnormal);
> add_test(setOffline);
> add_test(changeCloseReason);
> add_test(oneMoreLoop);
> add_test(shutdown);
> 
> function run_test() {
>   Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
>
Attachment #8771231 - Attachment is obsolete: false
Attachment #8771367 - Attachment is obsolete: true
Attachment #8771367 - Flags: review?(juhsu)
Sorry for attaching the patch to wrong bug.
Pushed by ryanvm@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/31897a5f4da8
Part 1: Add terminate command in control protocol. r=junior
https://hg.mozilla.org/integration/mozilla-inbound/rev/41e1c88227ca
Part 2: Implement PresentationConnection.terminate(). r=smaug
Keywords: checkin-needed
https://hg.mozilla.org/mozilla-central/rev/31897a5f4da8
https://hg.mozilla.org/mozilla-central/rev/41e1c88227ca
Status: NEW → RESOLVED
Closed: 3 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla50
NOTE: Please see https://wiki.mozilla.org/Release_Management/B2G_Landing to better understand the B2G approval process and landings.

[Approval Request Comment]
Bug caused by (feature/regressing bug #): presentation api
User impact if declined: receiver web page cannot be terminated
Testing completed: mochitest
Risk to taking this patch (and alternatives if risky): low
String or UUID changes made by this patch: n/a
Attachment #8771916 - Flags: approval-mozilla-b2g48?
Comment on attachment 8771916 [details] [review]
pull request for tv 2.6

Approve for TV 2.6
Attachment #8771916 - Flags: approval-mozilla-b2g48? → approval-mozilla-b2g48+
Comment on attachment 8771916 [details] [review]
pull request for tv 2.6

ni? @xeonchen for uplift.
Flags: needinfo?(xeonchen)
Component: DOM → DOM: Core & HTML
You need to log in before you can comment on or make changes to this bug.