summaryrefslogtreecommitdiffstats
path: root/dom/presentation
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/presentation/AvailabilityCollection.cpp89
-rw-r--r--dom/presentation/AvailabilityCollection.h44
-rw-r--r--dom/presentation/ControllerConnectionCollection.cpp105
-rw-r--r--dom/presentation/ControllerConnectionCollection.h46
-rw-r--r--dom/presentation/DCPresentationChannelDescription.cpp43
-rw-r--r--dom/presentation/DCPresentationChannelDescription.h35
-rw-r--r--dom/presentation/MockedSocketTransport.cpp224
-rw-r--r--dom/presentation/MockedSocketTransport.h36
-rw-r--r--dom/presentation/Presentation.cpp180
-rw-r--r--dom/presentation/Presentation.h65
-rw-r--r--dom/presentation/PresentationAvailability.cpp203
-rw-r--r--dom/presentation/PresentationAvailability.h73
-rw-r--r--dom/presentation/PresentationCallbacks.cpp251
-rw-r--r--dom/presentation/PresentationCallbacks.h83
-rw-r--r--dom/presentation/PresentationConnection.cpp757
-rw-r--r--dom/presentation/PresentationConnection.h116
-rw-r--r--dom/presentation/PresentationConnectionList.cpp124
-rw-r--r--dom/presentation/PresentationConnectionList.h58
-rw-r--r--dom/presentation/PresentationDataChannelSessionTransport.jsm421
-rw-r--r--dom/presentation/PresentationDeviceManager.cpp307
-rw-r--r--dom/presentation/PresentationDeviceManager.h53
-rw-r--r--dom/presentation/PresentationLog.h33
-rw-r--r--dom/presentation/PresentationNetworkHelper.jsm27
-rw-r--r--dom/presentation/PresentationReceiver.cpp169
-rw-r--r--dom/presentation/PresentationReceiver.h69
-rw-r--r--dom/presentation/PresentationRequest.cpp530
-rw-r--r--dom/presentation/PresentationRequest.h80
-rw-r--r--dom/presentation/PresentationService.cpp1117
-rw-r--r--dom/presentation/PresentationService.h64
-rw-r--r--dom/presentation/PresentationServiceBase.h356
-rw-r--r--dom/presentation/PresentationSessionInfo.cpp1536
-rw-r--r--dom/presentation/PresentationSessionInfo.h268
-rw-r--r--dom/presentation/PresentationSessionRequest.cpp66
-rw-r--r--dom/presentation/PresentationSessionRequest.h39
-rw-r--r--dom/presentation/PresentationTCPSessionTransport.cpp561
-rw-r--r--dom/presentation/PresentationTCPSessionTransport.h104
-rw-r--r--dom/presentation/PresentationTerminateRequest.cpp63
-rw-r--r--dom/presentation/PresentationTerminateRequest.h40
-rw-r--r--dom/presentation/PresentationTransportBuilderConstructor.cpp78
-rw-r--r--dom/presentation/PresentationTransportBuilderConstructor.h47
-rw-r--r--dom/presentation/components.conf36
-rw-r--r--dom/presentation/interfaces/moz.build29
-rw-r--r--dom/presentation/interfaces/nsIPresentationControlChannel.idl139
-rw-r--r--dom/presentation/interfaces/nsIPresentationControlService.idl156
-rw-r--r--dom/presentation/interfaces/nsIPresentationDevice.idl43
-rw-r--r--dom/presentation/interfaces/nsIPresentationDeviceManager.idl51
-rw-r--r--dom/presentation/interfaces/nsIPresentationDevicePrompt.idl59
-rw-r--r--dom/presentation/interfaces/nsIPresentationDeviceProvider.idl75
-rw-r--r--dom/presentation/interfaces/nsIPresentationListener.idl54
-rw-r--r--dom/presentation/interfaces/nsIPresentationLocalDevice.idl17
-rw-r--r--dom/presentation/interfaces/nsIPresentationNetworkHelper.idl36
-rw-r--r--dom/presentation/interfaces/nsIPresentationRequestUIGlue.idl29
-rw-r--r--dom/presentation/interfaces/nsIPresentationService.idl276
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionRequest.idl35
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionTransport.idl70
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl80
-rw-r--r--dom/presentation/interfaces/nsIPresentationTerminateRequest.idl33
-rw-r--r--dom/presentation/ipc/PPresentation.ipdl114
-rw-r--r--dom/presentation/ipc/PPresentationBuilder.ipdl34
-rw-r--r--dom/presentation/ipc/PPresentationRequest.ipdl22
-rw-r--r--dom/presentation/ipc/PresentationBuilderChild.cpp172
-rw-r--r--dom/presentation/ipc/PresentationBuilderChild.h47
-rw-r--r--dom/presentation/ipc/PresentationBuilderParent.cpp228
-rw-r--r--dom/presentation/ipc/PresentationBuilderParent.h52
-rw-r--r--dom/presentation/ipc/PresentationChild.cpp173
-rw-r--r--dom/presentation/ipc/PresentationChild.h88
-rw-r--r--dom/presentation/ipc/PresentationContentSessionInfo.cpp98
-rw-r--r--dom/presentation/ipc/PresentationContentSessionInfo.h62
-rw-r--r--dom/presentation/ipc/PresentationIPCService.cpp477
-rw-r--r--dom/presentation/ipc/PresentationIPCService.h71
-rw-r--r--dom/presentation/ipc/PresentationParent.cpp497
-rw-r--r--dom/presentation/ipc/PresentationParent.h133
-rw-r--r--dom/presentation/moz.build88
-rw-r--r--dom/presentation/provider/ControllerStateMachine.jsm236
-rw-r--r--dom/presentation/provider/DeviceProviderHelpers.cpp53
-rw-r--r--dom/presentation/provider/DeviceProviderHelpers.h29
-rw-r--r--dom/presentation/provider/MulticastDNSDeviceProvider.cpp1153
-rw-r--r--dom/presentation/provider/MulticastDNSDeviceProvider.h185
-rw-r--r--dom/presentation/provider/PresentationControlService.jsm1054
-rw-r--r--dom/presentation/provider/ReceiverStateMachine.jsm232
-rw-r--r--dom/presentation/provider/StateMachineHelper.jsm38
-rw-r--r--dom/presentation/provider/components.conf26
-rw-r--r--dom/presentation/provider/moz.build29
-rw-r--r--dom/presentation/provider/nsTCPDeviceInfo.h67
-rw-r--r--dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js150
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript.js553
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js412
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionFrameScript.js291
-rw-r--r--dom/presentation/tests/mochitest/chrome.ini13
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html216
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html95
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html10
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html158
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_non_receiver.html41
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html26
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver.html139
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html60
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html79
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html26
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_reconnect.html100
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html113
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_terminate.html104
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html114
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test1
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^1
-rw-r--r--dom/presentation/tests/mochitest/mochitest.ini81
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js241
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js522
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_availability.html35
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_availability_iframe.html227
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html243
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver.html138
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html209
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_sender.html289
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html143
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html80
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js91
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_reconnect.html378
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html75
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html187
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html173
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html130
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html103
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html76
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js116
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html16
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html16
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html171
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender.html258
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html151
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html160
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html514
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate.js325
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js266
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_oop.html18
-rw-r--r--dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js1465
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_device_manager.js288
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_session_transport.js225
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_state_machine.js376
-rw-r--r--dom/presentation/tests/xpcshell/test_tcp_control_channel.js523
-rw-r--r--dom/presentation/tests/xpcshell/xpcshell.ini9
150 files changed, 26418 insertions, 0 deletions
diff --git a/dom/presentation/AvailabilityCollection.cpp b/dom/presentation/AvailabilityCollection.cpp
new file mode 100644
index 0000000000..33e2e394c0
--- /dev/null
+++ b/dom/presentation/AvailabilityCollection.cpp
@@ -0,0 +1,89 @@
+/* -*- 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 "AvailabilityCollection.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "PresentationAvailability.h"
+
+namespace mozilla {
+namespace dom {
+
+/* static */
+StaticAutoPtr<AvailabilityCollection> AvailabilityCollection::sSingleton;
+static bool gOnceAliveNowDead = false;
+
+/* static */
+AvailabilityCollection* AvailabilityCollection::GetSingleton() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!sSingleton && !gOnceAliveNowDead) {
+ sSingleton = new AvailabilityCollection();
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return sSingleton;
+}
+
+AvailabilityCollection::AvailabilityCollection() {
+ MOZ_COUNT_CTOR(AvailabilityCollection);
+}
+
+AvailabilityCollection::~AvailabilityCollection() {
+ MOZ_COUNT_DTOR(AvailabilityCollection);
+ gOnceAliveNowDead = true;
+}
+
+void AvailabilityCollection::Add(PresentationAvailability* aAvailability) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aAvailability) {
+ return;
+ }
+
+ WeakPtr<PresentationAvailability> availability = aAvailability;
+ if (mAvailabilities.Contains(aAvailability)) {
+ return;
+ }
+
+ mAvailabilities.AppendElement(aAvailability);
+}
+
+void AvailabilityCollection::Remove(PresentationAvailability* aAvailability) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aAvailability) {
+ return;
+ }
+
+ WeakPtr<PresentationAvailability> availability = aAvailability;
+ mAvailabilities.RemoveElement(availability);
+}
+
+already_AddRefed<PresentationAvailability> AvailabilityCollection::Find(
+ const uint64_t aWindowId, const nsTArray<nsString>& aUrls) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Loop backwards to allow removing elements in the loop.
+ for (int i = mAvailabilities.Length() - 1; i >= 0; --i) {
+ WeakPtr<PresentationAvailability> availability = mAvailabilities[i];
+ if (!availability) {
+ // The availability object was destroyed. Remove it from the list.
+ mAvailabilities.RemoveElementAt(i);
+ continue;
+ }
+
+ if (availability->Equals(aWindowId, aUrls)) {
+ RefPtr<PresentationAvailability> matchedAvailability = availability.get();
+ return matchedAvailability.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/AvailabilityCollection.h b/dom/presentation/AvailabilityCollection.h
new file mode 100644
index 0000000000..0e0914850d
--- /dev/null
+++ b/dom/presentation/AvailabilityCollection.h
@@ -0,0 +1,44 @@
+/* -*- 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_AvailabilityCollection_h
+#define mozilla_dom_AvailabilityCollection_h
+
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationAvailability;
+
+class AvailabilityCollection final {
+ public:
+ static AvailabilityCollection* GetSingleton();
+
+ void Add(PresentationAvailability* aAvailability);
+
+ void Remove(PresentationAvailability* aAvailability);
+
+ already_AddRefed<PresentationAvailability> Find(
+ const uint64_t aWindowId, const nsTArray<nsString>& aUrls);
+
+ private:
+ friend class StaticAutoPtr<AvailabilityCollection>;
+
+ AvailabilityCollection();
+ virtual ~AvailabilityCollection();
+
+ static StaticAutoPtr<AvailabilityCollection> sSingleton;
+ nsTArray<WeakPtr<PresentationAvailability>> mAvailabilities;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AvailabilityCollection_h
diff --git a/dom/presentation/ControllerConnectionCollection.cpp b/dom/presentation/ControllerConnectionCollection.cpp
new file mode 100644
index 0000000000..96659cbfba
--- /dev/null
+++ b/dom/presentation/ControllerConnectionCollection.cpp
@@ -0,0 +1,105 @@
+/* -*- 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 "ControllerConnectionCollection.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "nsIPresentationService.h"
+#include "PresentationConnection.h"
+
+namespace mozilla {
+namespace dom {
+
+/* static */
+StaticAutoPtr<ControllerConnectionCollection>
+ ControllerConnectionCollection::sSingleton;
+
+/* static */
+ControllerConnectionCollection* ControllerConnectionCollection::GetSingleton() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!sSingleton) {
+ sSingleton = new ControllerConnectionCollection();
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return sSingleton;
+}
+
+ControllerConnectionCollection::ControllerConnectionCollection() {
+ MOZ_COUNT_CTOR(ControllerConnectionCollection);
+}
+
+ControllerConnectionCollection::~ControllerConnectionCollection() {
+ MOZ_COUNT_DTOR(ControllerConnectionCollection);
+}
+
+void ControllerConnectionCollection::AddConnection(
+ PresentationConnection* aConnection, const uint8_t aRole) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "This is allowed only to be called at controller side.");
+ return;
+ }
+
+ if (!aConnection) {
+ return;
+ }
+
+ WeakPtr<PresentationConnection> connection = aConnection;
+ if (mConnections.Contains(connection)) {
+ return;
+ }
+
+ mConnections.AppendElement(connection);
+}
+
+void ControllerConnectionCollection::RemoveConnection(
+ PresentationConnection* aConnection, const uint8_t aRole) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "This is allowed only to be called at controller side.");
+ return;
+ }
+
+ if (!aConnection) {
+ return;
+ }
+
+ WeakPtr<PresentationConnection> connection = aConnection;
+ mConnections.RemoveElement(connection);
+}
+
+already_AddRefed<PresentationConnection>
+ControllerConnectionCollection::FindConnection(uint64_t aWindowId,
+ const nsAString& aId,
+ const uint8_t aRole) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "This is allowed only to be called at controller side.");
+ return nullptr;
+ }
+
+ // Loop backwards to allow removing elements in the loop.
+ for (int i = mConnections.Length() - 1; i >= 0; --i) {
+ WeakPtr<PresentationConnection> connection = mConnections[i];
+ if (!connection) {
+ // The connection was destroyed. Remove it from the list.
+ mConnections.RemoveElementAt(i);
+ continue;
+ }
+
+ if (connection->Equals(aWindowId, aId)) {
+ RefPtr<PresentationConnection> matchedConnection = connection.get();
+ return matchedConnection.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ControllerConnectionCollection.h b/dom/presentation/ControllerConnectionCollection.h
new file mode 100644
index 0000000000..c48e9ca495
--- /dev/null
+++ b/dom/presentation/ControllerConnectionCollection.h
@@ -0,0 +1,46 @@
+/* -*- 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_ControllerConnectionCollection_h
+#define mozilla_dom_ControllerConnectionCollection_h
+
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationConnection;
+
+class ControllerConnectionCollection final {
+ public:
+ static ControllerConnectionCollection* GetSingleton();
+
+ void AddConnection(PresentationConnection* aConnection, const uint8_t aRole);
+
+ void RemoveConnection(PresentationConnection* aConnection,
+ const uint8_t aRole);
+
+ already_AddRefed<PresentationConnection> FindConnection(uint64_t aWindowId,
+ const nsAString& aId,
+ const uint8_t aRole);
+
+ private:
+ friend class StaticAutoPtr<ControllerConnectionCollection>;
+
+ ControllerConnectionCollection();
+ virtual ~ControllerConnectionCollection();
+
+ static StaticAutoPtr<ControllerConnectionCollection> sSingleton;
+ nsTArray<WeakPtr<PresentationConnection>> mConnections;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ControllerConnectionCollection_h
diff --git a/dom/presentation/DCPresentationChannelDescription.cpp b/dom/presentation/DCPresentationChannelDescription.cpp
new file mode 100644
index 0000000000..471ede837e
--- /dev/null
+++ b/dom/presentation/DCPresentationChannelDescription.cpp
@@ -0,0 +1,43 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(DCPresentationChannelDescription,
+ nsIPresentationChannelDescription)
+
+NS_IMETHODIMP
+DCPresentationChannelDescription::GetType(uint8_t* aRetVal) {
+ if (NS_WARN_IF(!aRetVal)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ *aRetVal = nsIPresentationChannelDescription::TYPE_DATACHANNEL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DCPresentationChannelDescription::GetTcpAddress(nsIArray** aRetVal) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DCPresentationChannelDescription::GetTcpPort(uint16_t* aRetVal) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DCPresentationChannelDescription::GetDataChannelSDP(
+ nsAString& aDataChannelSDP) {
+ aDataChannelSDP = mSDP;
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/DCPresentationChannelDescription.h b/dom/presentation/DCPresentationChannelDescription.h
new file mode 100644
index 0000000000..997f3d99e5
--- /dev/null
+++ b/dom/presentation/DCPresentationChannelDescription.h
@@ -0,0 +1,35 @@
+/* -*- 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_DCPresentationChannelDescription_h
+#define mozilla_dom_DCPresentationChannelDescription_h
+
+#include "nsIPresentationControlChannel.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+
+// PresentationChannelDescription for Data Channel
+class DCPresentationChannelDescription final
+ : public nsIPresentationChannelDescription {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONCHANNELDESCRIPTION
+
+ explicit DCPresentationChannelDescription(const nsAString& aSDP)
+ : mSDP(aSDP) {}
+
+ private:
+ virtual ~DCPresentationChannelDescription() = default;
+
+ nsString mSDP;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_DCPresentationChannelDescription_h
diff --git a/dom/presentation/MockedSocketTransport.cpp b/dom/presentation/MockedSocketTransport.cpp
new file mode 100644
index 0000000000..f5d29f746c
--- /dev/null
+++ b/dom/presentation/MockedSocketTransport.cpp
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "MockedSocketTransport.h"
+
+using namespace mozilla::net;
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(MockedSocketTransport, nsISocketTransport, nsITransport)
+
+// static
+already_AddRefed<MockedSocketTransport> MockedSocketTransport::Create() {
+ RefPtr<MockedSocketTransport> transport = new MockedSocketTransport();
+ return transport.forget();
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetKeepaliveEnabled(bool) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetKeepaliveVals(int32_t, int32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetSecurityCallbacks(nsIInterfaceRequestor**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetSecurityCallbacks(nsIInterfaceRequestor*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::OpenInputStream(uint32_t, uint32_t, uint32_t,
+ nsIInputStream**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::OpenOutputStream(uint32_t, uint32_t, uint32_t,
+ nsIOutputStream**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::Close(nsresult) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::SetEventSink(nsITransportEventSink*, nsIEventTarget*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::Bind(NetAddr*) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::GetFirstRetryError(nsresult*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetEchConfigUsed(bool*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetEchConfig(const nsACString&) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::ResolvedByTRR(bool*) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+nsresult MockedSocketTransport::GetOriginAttributes(
+ mozilla::OriginAttributes*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetKeepaliveEnabled(bool*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetSendBufferSize(uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetSendBufferSize(uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetPort(int32_t*) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::GetPeerAddr(mozilla::net::NetAddr*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetSelfAddr(mozilla::net::NetAddr*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetScriptablePeerAddr(nsINetAddr**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetScriptableSelfAddr(nsINetAddr**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetSecurityInfo(nsISupports**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::IsAlive(bool*) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::GetConnectionFlags(uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetConnectionFlags(uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetIsPrivate(bool) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::GetTlsFlags(uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetTlsFlags(uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetRecvBufferSize(uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetRecvBufferSize(uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetResetIPFamilyPreference(bool*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult MockedSocketTransport::SetOriginAttributes(
+ const mozilla::OriginAttributes&) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetScriptableOriginAttributes(
+ JSContext* aCx, JS::MutableHandle<JS::Value>) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetScriptableOriginAttributes(JSContext* aCx,
+ JS::Handle<JS::Value>) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetHost(nsACString&) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::GetTimeout(uint32_t, uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetTimeout(uint32_t, uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetReuseAddrPort(bool) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::SetLinger(bool, int16_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+MockedSocketTransport::GetQoSBits(uint8_t*) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::SetQoSBits(uint8_t) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+MockedSocketTransport::SetFastOpenCallback(TCPFastOpen*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/MockedSocketTransport.h b/dom/presentation/MockedSocketTransport.h
new file mode 100644
index 0000000000..c83e8f1382
--- /dev/null
+++ b/dom/presentation/MockedSocketTransport.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_PRESENTATION_MOCKEDSOCKETTRANSPORT_H_
+#define DOM_PRESENTATION_MOCKEDSOCKETTRANSPORT_H_
+
+#include "nsISocketTransport.h"
+
+namespace mozilla {
+namespace dom {
+
+// For testing purposes, we need a mocked socket transport that doesn't do
+// anything. It has to be implemented in C++ because nsISocketTransport is
+// builtinclass because it contains nostdcall methods.
+
+class MockedSocketTransport final : public nsISocketTransport {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITRANSPORT
+ NS_DECL_NSISOCKETTRANSPORT
+
+ MockedSocketTransport() = default;
+
+ static already_AddRefed<MockedSocketTransport> Create();
+
+ protected:
+ virtual ~MockedSocketTransport() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_PRESENTATION_MOCKEDSOCKETTRANSPORT_H_
diff --git a/dom/presentation/Presentation.cpp b/dom/presentation/Presentation.cpp
new file mode 100644
index 0000000000..9f27fa56f7
--- /dev/null
+++ b/dom/presentation/Presentation.cpp
@@ -0,0 +1,180 @@
+/* -*- 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 "Presentation.h"
+
+#include <ctype.h>
+
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/PresentationBinding.h"
+#include "mozilla/dom/PresentationRequest.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocShell.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsJSUtils.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsSandboxFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "PresentationReceiver.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Presentation, mWindow, mDefaultRequest,
+ mReceiver)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Presentation)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Presentation)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Presentation)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<Presentation> Presentation::Create(
+ nsPIDOMWindowInner* aWindow) {
+ RefPtr<Presentation> presentation = new Presentation(aWindow);
+ return presentation.forget();
+}
+
+Presentation::Presentation(nsPIDOMWindowInner* aWindow) : mWindow(aWindow) {}
+
+Presentation::~Presentation() = default;
+
+/* virtual */
+JSObject* Presentation::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Presentation_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void Presentation::SetDefaultRequest(PresentationRequest* aRequest) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ nsCOMPtr<Document> doc = mWindow ? mWindow->GetExtantDoc() : nullptr;
+ if (NS_WARN_IF(!doc)) {
+ return;
+ }
+
+ if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
+ return;
+ }
+
+ mDefaultRequest = aRequest;
+}
+
+already_AddRefed<PresentationRequest> Presentation::GetDefaultRequest() const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return nullptr;
+ }
+
+ RefPtr<PresentationRequest> request = mDefaultRequest;
+ return request.forget();
+}
+
+already_AddRefed<PresentationReceiver> Presentation::GetReceiver() {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return nullptr;
+ }
+
+ // return the same receiver if already created
+ if (mReceiver) {
+ RefPtr<PresentationReceiver> receiver = mReceiver;
+ return receiver.forget();
+ }
+
+ if (!HasReceiverSupport() || !IsInPresentedContent()) {
+ return nullptr;
+ }
+
+ mReceiver = PresentationReceiver::Create(mWindow);
+ if (NS_WARN_IF(!mReceiver)) {
+ MOZ_ASSERT(mReceiver);
+ return nullptr;
+ }
+
+ RefPtr<PresentationReceiver> receiver = mReceiver;
+ return receiver.forget();
+}
+
+void Presentation::SetStartSessionUnsettled(bool aIsUnsettled) {
+ mStartSessionUnsettled = aIsUnsettled;
+}
+
+bool Presentation::IsStartSessionUnsettled() const {
+ return mStartSessionUnsettled;
+}
+
+bool Presentation::HasReceiverSupport() const {
+ if (!mWindow) {
+ return false;
+ }
+
+ // Grant access to browser receiving pages and their same-origin iframes. (App
+ // pages should be controlled by "presentation" permission in app manifests.)
+ nsCOMPtr<nsIDocShell> docShell = mWindow->GetDocShell();
+ if (!docShell) {
+ return false;
+ }
+
+ if (!StaticPrefs::dom_presentation_testing_simulate_receiver() &&
+ !docShell->GetIsTopLevelContentDocShell()) {
+ return false;
+ }
+
+ nsAutoString presentationURL;
+ nsContentUtils::GetPresentationURL(docShell, presentationURL);
+
+ if (presentationURL.IsEmpty()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIScriptSecurityManager> securityManager =
+ nsContentUtils::GetSecurityManager();
+ if (!securityManager) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> presentationURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(presentationURI), presentationURL);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ bool isPrivateWin = false;
+ nsCOMPtr<Document> doc = mWindow->GetExtantDoc();
+ if (doc) {
+ isPrivateWin =
+ doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
+ }
+
+ nsCOMPtr<nsIURI> docURI = mWindow->GetDocumentURI();
+ return NS_SUCCEEDED(securityManager->CheckSameOriginURI(
+ presentationURI, docURI, false, isPrivateWin));
+}
+
+bool Presentation::IsInPresentedContent() const {
+ if (!mWindow) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = mWindow->GetDocShell();
+ MOZ_ASSERT(docShell);
+
+ nsAutoString presentationURL;
+ nsContentUtils::GetPresentationURL(docShell, presentationURL);
+
+ return !presentationURL.IsEmpty();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/Presentation.h b/dom/presentation/Presentation.h
new file mode 100644
index 0000000000..c2c0284ce7
--- /dev/null
+++ b/dom/presentation/Presentation.h
@@ -0,0 +1,65 @@
+/* -*- 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_Presentation_h
+#define mozilla_dom_Presentation_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+class PresentationReceiver;
+class PresentationRequest;
+
+class Presentation final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Presentation)
+
+ static already_AddRefed<Presentation> Create(nsPIDOMWindowInner* aWindow);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+
+ // WebIDL (public APIs)
+ void SetDefaultRequest(PresentationRequest* aRequest);
+
+ already_AddRefed<PresentationRequest> GetDefaultRequest() const;
+
+ already_AddRefed<PresentationReceiver> GetReceiver();
+
+ // For bookkeeping unsettled start session request
+ void SetStartSessionUnsettled(bool aIsUnsettled);
+ bool IsStartSessionUnsettled() const;
+
+ private:
+ explicit Presentation(nsPIDOMWindowInner* aWindow);
+
+ virtual ~Presentation();
+
+ bool HasReceiverSupport() const;
+
+ bool IsInPresentedContent() const;
+
+ RefPtr<PresentationRequest> mDefaultRequest;
+ RefPtr<PresentationReceiver> mReceiver;
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ bool mStartSessionUnsettled = false;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Presentation_h
diff --git a/dom/presentation/PresentationAvailability.cpp b/dom/presentation/PresentationAvailability.cpp
new file mode 100644
index 0000000000..ba816f975a
--- /dev/null
+++ b/dom/presentation/PresentationAvailability.cpp
@@ -0,0 +1,203 @@
+/* -*- 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 "PresentationAvailability.h"
+
+#include "mozilla/dom/PresentationAvailabilityBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationService.h"
+#include "nsServiceManagerUtils.h"
+#include "AvailabilityCollection.h"
+#include "PresentationLog.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PresentationAvailability)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PresentationAvailability,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PresentationAvailability,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromises);
+ tmp->Shutdown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ADDREF_INHERITED(PresentationAvailability, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationAvailability, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationAvailability)
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationAvailabilityListener)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+/* static */
+already_AddRefed<PresentationAvailability> PresentationAvailability::Create(
+ nsPIDOMWindowInner* aWindow, const nsTArray<nsString>& aUrls,
+ RefPtr<Promise>& aPromise) {
+ RefPtr<PresentationAvailability> availability =
+ new PresentationAvailability(aWindow, aUrls);
+ return NS_WARN_IF(!availability->Init(aPromise)) ? nullptr
+ : availability.forget();
+}
+
+PresentationAvailability::PresentationAvailability(
+ nsPIDOMWindowInner* aWindow, const nsTArray<nsString>& aUrls)
+ : DOMEventTargetHelper(aWindow), mIsAvailable(false), mUrls(aUrls.Clone()) {
+ for (uint32_t i = 0; i < mUrls.Length(); ++i) {
+ mAvailabilityOfUrl.AppendElement(false);
+ }
+}
+
+PresentationAvailability::~PresentationAvailability() { Shutdown(); }
+
+bool PresentationAvailability::Init(RefPtr<Promise>& aPromise) {
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return false;
+ }
+
+ nsresult rv = service->RegisterAvailabilityListener(mUrls, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If the user agent is unable to monitor available device,
+ // Resolve promise with |value| set to false.
+ mIsAvailable = false;
+ aPromise->MaybeResolve(this);
+ return true;
+ }
+
+ EnqueuePromise(aPromise);
+
+ AvailabilityCollection* collection = AvailabilityCollection::GetSingleton();
+ if (collection) {
+ collection->Add(this);
+ }
+
+ return true;
+}
+
+void PresentationAvailability::Shutdown() {
+ AvailabilityCollection* collection = AvailabilityCollection::GetSingleton();
+ if (collection) {
+ collection->Remove(this);
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return;
+ }
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(service->UnregisterAvailabilityListener(mUrls, this)));
+}
+
+/* virtual */
+void PresentationAvailability::DisconnectFromOwner() {
+ Shutdown();
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+/* virtual */
+JSObject* PresentationAvailability::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PresentationAvailability_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool PresentationAvailability::Equals(const uint64_t aWindowID,
+ const nsTArray<nsString>& aUrls) const {
+ if (GetOwner() && GetOwner()->WindowID() == aWindowID &&
+ mUrls.Length() == aUrls.Length()) {
+ for (const auto& url : aUrls) {
+ if (!mUrls.Contains(url)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool PresentationAvailability::IsCachedValueReady() {
+ // All pending promises will be solved when cached value is ready and
+ // no promise should be enqueued afterward.
+ return mPromises.IsEmpty();
+}
+
+void PresentationAvailability::EnqueuePromise(RefPtr<Promise>& aPromise) {
+ mPromises.AppendElement(aPromise);
+}
+
+bool PresentationAvailability::Value() const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return false;
+ }
+
+ return mIsAvailable;
+}
+
+NS_IMETHODIMP
+PresentationAvailability::NotifyAvailableChange(
+ const nsTArray<nsString>& aAvailabilityUrls, bool aIsAvailable) {
+ bool available = false;
+ for (uint32_t i = 0; i < mUrls.Length(); ++i) {
+ if (aAvailabilityUrls.Contains(mUrls[i])) {
+ mAvailabilityOfUrl[i] = aIsAvailable;
+ }
+ available |= mAvailabilityOfUrl[i];
+ }
+
+ return NS_DispatchToCurrentThread(NewRunnableMethod<bool>(
+ "dom::PresentationAvailability::UpdateAvailabilityAndDispatchEvent", this,
+ &PresentationAvailability::UpdateAvailabilityAndDispatchEvent,
+ available));
+}
+
+void PresentationAvailability::UpdateAvailabilityAndDispatchEvent(
+ bool aIsAvailable) {
+ PRES_DEBUG("%s\n", __func__);
+ bool isChanged = (aIsAvailable != mIsAvailable);
+
+ mIsAvailable = aIsAvailable;
+
+ if (!mPromises.IsEmpty()) {
+ // Use the first availability change notification to resolve promise.
+ do {
+ nsTArray<RefPtr<Promise>> promises = std::move(mPromises);
+
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ continue;
+ }
+
+ for (auto& promise : promises) {
+ promise->MaybeResolve(this);
+ }
+ // more promises may have been added to mPromises, at least in theory
+ } while (!mPromises.IsEmpty());
+
+ return;
+ }
+
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ if (isChanged) {
+ Unused << NS_WARN_IF(NS_FAILED(DispatchTrustedEvent(u"change"_ns)));
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationAvailability.h b/dom/presentation/PresentationAvailability.h
new file mode 100644
index 0000000000..0559841046
--- /dev/null
+++ b/dom/presentation/PresentationAvailability.h
@@ -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/. */
+
+#ifndef mozilla_dom_PresentationAvailability_h
+#define mozilla_dom_PresentationAvailability_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/WeakPtr.h"
+#include "nsIPresentationListener.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+
+class PresentationAvailability final
+ : public DOMEventTargetHelper,
+ public nsIPresentationAvailabilityListener,
+ public SupportsWeakPtr {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PresentationAvailability,
+ DOMEventTargetHelper)
+ NS_DECL_NSIPRESENTATIONAVAILABILITYLISTENER
+
+ static already_AddRefed<PresentationAvailability> Create(
+ nsPIDOMWindowInner* aWindow, const nsTArray<nsString>& aUrls,
+ RefPtr<Promise>& aPromise);
+
+ virtual void DisconnectFromOwner() override;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ bool Equals(const uint64_t aWindowID, const nsTArray<nsString>& aUrls) const;
+
+ bool IsCachedValueReady();
+
+ void EnqueuePromise(RefPtr<Promise>& aPromise);
+
+ // WebIDL (public APIs)
+ bool Value() const;
+
+ IMPL_EVENT_HANDLER(change);
+
+ private:
+ explicit PresentationAvailability(nsPIDOMWindowInner* aWindow,
+ const nsTArray<nsString>& aUrls);
+
+ virtual ~PresentationAvailability();
+
+ bool Init(RefPtr<Promise>& aPromise);
+
+ void Shutdown();
+
+ void UpdateAvailabilityAndDispatchEvent(bool aIsAvailable);
+
+ bool mIsAvailable;
+
+ nsTArray<RefPtr<Promise>> mPromises;
+
+ nsTArray<nsString> mUrls;
+ nsTArray<bool> mAvailabilityOfUrl;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationAvailability_h
diff --git a/dom/presentation/PresentationCallbacks.cpp b/dom/presentation/PresentationCallbacks.cpp
new file mode 100644
index 0000000000..8880ba9b7c
--- /dev/null
+++ b/dom/presentation/PresentationCallbacks.cpp
@@ -0,0 +1,251 @@
+/* -*- 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 "mozilla/dom/Promise.h"
+#include "nsIDocShell.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPresentationService.h"
+#include "nsIWebProgress.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "PresentationCallbacks.h"
+#include "PresentationRequest.h"
+#include "PresentationConnection.h"
+#include "PresentationTransportBuilderConstructor.h"
+
+namespace mozilla {
+namespace dom {
+
+/*
+ * Implementation of PresentationRequesterCallback
+ */
+
+NS_IMPL_ISUPPORTS(PresentationRequesterCallback, nsIPresentationServiceCallback)
+
+PresentationRequesterCallback::PresentationRequesterCallback(
+ PresentationRequest* aRequest, const nsAString& aSessionId,
+ Promise* aPromise)
+ : mRequest(aRequest), mSessionId(aSessionId), mPromise(aPromise) {
+ MOZ_ASSERT(mRequest);
+ MOZ_ASSERT(mPromise);
+ MOZ_ASSERT(!mSessionId.IsEmpty());
+}
+
+PresentationRequesterCallback::~PresentationRequesterCallback() = default;
+
+// nsIPresentationServiceCallback
+NS_IMETHODIMP
+PresentationRequesterCallback::NotifySuccess(const nsAString& aUrl) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aUrl.IsEmpty()) {
+ return NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ RefPtr<PresentationConnection> connection =
+ PresentationConnection::Create(mRequest->GetOwner(), mSessionId, aUrl,
+ nsIPresentationService::ROLE_CONTROLLER);
+ if (NS_WARN_IF(!connection)) {
+ return NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ mRequest->NotifyPromiseSettled();
+ mPromise->MaybeResolve(connection);
+
+ return mRequest->DispatchConnectionAvailableEvent(connection);
+}
+
+NS_IMETHODIMP
+PresentationRequesterCallback::NotifyError(nsresult aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRequest->NotifyPromiseSettled();
+ mPromise->MaybeReject(aError);
+ return NS_OK;
+}
+
+/*
+ * Implementation of PresentationRequesterCallback
+ */
+
+PresentationReconnectCallback::PresentationReconnectCallback(
+ PresentationRequest* aRequest, const nsAString& aSessionId,
+ Promise* aPromise, PresentationConnection* aConnection)
+ : PresentationRequesterCallback(aRequest, aSessionId, aPromise),
+ mConnection(aConnection) {}
+
+PresentationReconnectCallback::~PresentationReconnectCallback() = default;
+
+NS_IMETHODIMP
+PresentationReconnectCallback::NotifySuccess(const nsAString& aUrl) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = NS_OK;
+ // We found a matched connection with the same window ID, URL, and
+ // the session ID. Resolve the promise with this connection and dispatch
+ // the event.
+ if (mConnection) {
+ mConnection->NotifyStateChange(
+ mSessionId, nsIPresentationSessionListener::STATE_CONNECTING, NS_OK);
+ mPromise->MaybeResolve(mConnection);
+ rv = mRequest->DispatchConnectionAvailableEvent(mConnection);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // Use |PresentationRequesterCallback::NotifySuccess| to create a new
+ // connection since we don't find one that can be reused.
+ rv = PresentationRequesterCallback::NotifySuccess(aUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = service->UpdateWindowIdBySessionId(
+ mSessionId, nsIPresentationService::ROLE_CONTROLLER,
+ mRequest->GetOwner()->WindowID());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ nsString sessionId = nsString(mSessionId);
+ return NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "dom::PresentationReconnectCallback::NotifySuccess",
+ [sessionId, service]() -> void {
+ service->BuildTransport(sessionId,
+ nsIPresentationService::ROLE_CONTROLLER);
+ }));
+}
+
+NS_IMETHODIMP
+PresentationReconnectCallback::NotifyError(nsresult aError) {
+ if (mConnection) {
+ mConnection->NotifyStateChange(
+ mSessionId, nsIPresentationSessionListener::STATE_CLOSED, aError);
+ }
+ return PresentationRequesterCallback::NotifyError(aError);
+}
+
+NS_IMPL_ISUPPORTS(PresentationResponderLoadingCallback, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+PresentationResponderLoadingCallback::PresentationResponderLoadingCallback(
+ const nsAString& aSessionId)
+ : mSessionId(aSessionId) {}
+
+PresentationResponderLoadingCallback::~PresentationResponderLoadingCallback() {
+ if (mProgress) {
+ mProgress->RemoveProgressListener(this);
+ mProgress = nullptr;
+ }
+}
+
+nsresult PresentationResponderLoadingCallback::Init(nsIDocShell* aDocShell) {
+ mProgress = do_GetInterface(aDocShell);
+ if (NS_WARN_IF(!mProgress)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ auto busyFlags = aDocShell->GetBusyFlags();
+
+ if ((busyFlags == nsIDocShell::BUSY_FLAGS_NONE) ||
+ (busyFlags & nsIDocShell::BUSY_FLAGS_PAGE_LOADING)) {
+ // The docshell has finished loading or is receiving data
+ // (|STATE_TRANSFERRING| has already been fired), so the page is ready for
+ // presentation use.
+ return NotifyReceiverReady(/* isLoading = */ true);
+ }
+
+ // Start to listen to document state change event |STATE_TRANSFERRING|.
+ return mProgress->AddProgressListener(this,
+ nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+}
+
+nsresult PresentationResponderLoadingCallback::NotifyReceiverReady(
+ bool aIsLoading) {
+ nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(mProgress);
+ if (NS_WARN_IF(!window || !window->GetCurrentInnerWindow())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ uint64_t windowId = window->GetCurrentInnerWindow()->WindowID();
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> constructor =
+ PresentationTransportBuilderConstructor::Create();
+ return service->NotifyReceiverReady(mSessionId, windowId, aIsLoading,
+ constructor);
+}
+
+// nsIWebProgressListener
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnStateChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aStateFlags & (nsIWebProgressListener::STATE_TRANSFERRING |
+ nsIWebProgressListener::STATE_STOP)) {
+ mProgress->RemoveProgressListener(this);
+
+ bool isLoading = aStateFlags & nsIWebProgressListener::STATE_TRANSFERRING;
+ return NotifyReceiverReady(isLoading);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnProgressChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress, int32_t aMaxTotalProgress) {
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnLocationChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aURI,
+ uint32_t aFlags) {
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnStatusChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnSecurityChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aState) {
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationResponderLoadingCallback::OnContentBlockingEvent(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aEvent) {
+ // Do nothing.
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationCallbacks.h b/dom/presentation/PresentationCallbacks.h
new file mode 100644
index 0000000000..9b0d2b02fe
--- /dev/null
+++ b/dom/presentation/PresentationCallbacks.h
@@ -0,0 +1,83 @@
+/* -*- 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_PresentationCallbacks_h
+#define mozilla_dom_PresentationCallbacks_h
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsIPresentationService.h"
+#include "nsIWebProgressListener.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+
+class nsIDocShell;
+class nsIWebProgress;
+
+namespace mozilla {
+namespace dom {
+
+class PresentationConnection;
+class PresentationRequest;
+class Promise;
+
+class PresentationRequesterCallback : public nsIPresentationServiceCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSERVICECALLBACK
+
+ PresentationRequesterCallback(PresentationRequest* aRequest,
+ const nsAString& aSessionId, Promise* aPromise);
+
+ protected:
+ virtual ~PresentationRequesterCallback();
+
+ RefPtr<PresentationRequest> mRequest;
+ nsString mSessionId;
+ RefPtr<Promise> mPromise;
+};
+
+class PresentationReconnectCallback final
+ : public PresentationRequesterCallback {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(PresentationReconnectCallback,
+ PresentationRequesterCallback)
+ NS_DECL_NSIPRESENTATIONSERVICECALLBACK
+
+ PresentationReconnectCallback(PresentationRequest* aRequest,
+ const nsAString& aSessionId, Promise* aPromise,
+ PresentationConnection* aConnection);
+
+ private:
+ virtual ~PresentationReconnectCallback();
+
+ RefPtr<PresentationConnection> mConnection;
+};
+
+class PresentationResponderLoadingCallback final
+ : public nsIWebProgressListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ explicit PresentationResponderLoadingCallback(const nsAString& aSessionId);
+
+ nsresult Init(nsIDocShell* aDocShell);
+
+ private:
+ ~PresentationResponderLoadingCallback();
+
+ nsresult NotifyReceiverReady(bool aIsLoading);
+
+ nsString mSessionId;
+ nsCOMPtr<nsIWebProgress> mProgress;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationCallbacks_h
diff --git a/dom/presentation/PresentationConnection.cpp b/dom/presentation/PresentationConnection.cpp
new file mode 100644
index 0000000000..8d944f1e78
--- /dev/null
+++ b/dom/presentation/PresentationConnection.cpp
@@ -0,0 +1,757 @@
+/* -*- 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 "PresentationConnection.h"
+
+#include "ControllerConnectionCollection.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/MessageEvent.h"
+#include "mozilla/dom/MessageEventBinding.h"
+#include "mozilla/dom/PresentationConnectionCloseEvent.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringStream.h"
+#include "PresentationConnectionList.h"
+#include "PresentationLog.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PresentationConnection)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PresentationConnection,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwningConnectionList)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PresentationConnection,
+ DOMEventTargetHelper)
+ tmp->Shutdown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwningConnectionList)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ADDREF_INHERITED(PresentationConnection, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationConnection, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationConnection)
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationSessionListener)
+ NS_INTERFACE_MAP_ENTRY(nsIRequest)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+PresentationConnection::PresentationConnection(
+ nsPIDOMWindowInner* aWindow, const nsAString& aId, const nsAString& aUrl,
+ const uint8_t aRole, PresentationConnectionList* aList)
+ : DOMEventTargetHelper(aWindow),
+ mId(aId),
+ mUrl(aUrl),
+ mState(PresentationConnectionState::Connecting),
+ mOwningConnectionList(aList),
+ mBinaryType(PresentationConnectionBinaryType::Arraybuffer) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ mRole = aRole;
+}
+
+/* virtual */ PresentationConnection::~PresentationConnection() = default;
+
+/* static */
+already_AddRefed<PresentationConnection> PresentationConnection::Create(
+ nsPIDOMWindowInner* aWindow, const nsAString& aId, const nsAString& aUrl,
+ const uint8_t aRole, PresentationConnectionList* aList) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ RefPtr<PresentationConnection> connection =
+ new PresentationConnection(aWindow, aId, aUrl, aRole, aList);
+ if (NS_WARN_IF(!connection->Init())) {
+ return nullptr;
+ }
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ ControllerConnectionCollection::GetSingleton()->AddConnection(connection,
+ aRole);
+ }
+
+ return connection.forget();
+}
+
+bool PresentationConnection::Init() {
+ if (NS_WARN_IF(mId.IsEmpty())) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return false;
+ }
+
+ nsresult rv = service->RegisterSessionListener(mId, mRole, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = AddIntoLoadGroup();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return true;
+}
+
+void PresentationConnection::Shutdown() {
+ PRES_DEBUG("connection shutdown:id[%s], role[%d]\n",
+ NS_ConvertUTF16toUTF8(mId).get(), mRole);
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return;
+ }
+
+ DebugOnly<nsresult> rv = service->UnregisterSessionListener(mId, mRole);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "UnregisterSessionListener failed");
+
+ DebugOnly<nsresult> rv2 = RemoveFromLoadGroup();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv2), "RemoveFromLoadGroup failed");
+
+ if (mRole == nsIPresentationService::ROLE_CONTROLLER) {
+ ControllerConnectionCollection::GetSingleton()->RemoveConnection(this,
+ mRole);
+ }
+}
+
+/* virtual */
+void PresentationConnection::DisconnectFromOwner() {
+ Unused << NS_WARN_IF(NS_FAILED(ProcessConnectionWentAway()));
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+/* virtual */
+JSObject* PresentationConnection::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PresentationConnection_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PresentationConnection::GetId(nsAString& aId) const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ aId.Truncate();
+ return;
+ }
+
+ aId = mId;
+}
+
+void PresentationConnection::GetUrl(nsAString& aUrl) const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ aUrl.Truncate();
+ return;
+ }
+
+ aUrl = mUrl;
+}
+
+PresentationConnectionState PresentationConnection::State() const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return PresentationConnectionState::Terminated;
+ }
+
+ return mState;
+}
+
+PresentationConnectionBinaryType PresentationConnection::BinaryType() const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return PresentationConnectionBinaryType::Blob;
+ }
+
+ return mBinaryType;
+}
+
+void PresentationConnection::SetBinaryType(
+ PresentationConnectionBinaryType aType) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ mBinaryType = aType;
+}
+
+void PresentationConnection::Send(const nsAString& aData, ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ // Sending is not allowed if the session is not connected.
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ AsyncCloseConnectionWithErrorMsg(
+ u"Unable to send message due to an internal error."_ns);
+ return;
+ }
+
+ nsresult rv = service->SendSessionMessage(mId, mRole, aData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ const uint32_t kMaxMessageLength = 256;
+ nsAutoString data(Substring(aData, 0, kMaxMessageLength));
+
+ AsyncCloseConnectionWithErrorMsg(u"Unable to send message: \""_ns + data +
+ u"\""_ns);
+ }
+}
+
+void PresentationConnection::Send(Blob& aData, ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ AsyncCloseConnectionWithErrorMsg(
+ u"Unable to send message due to an internal error."_ns);
+ return;
+ }
+
+ nsresult rv = service->SendSessionBlob(mId, mRole, &aData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AsyncCloseConnectionWithErrorMsg(
+ u"Unable to send binary message for Blob message."_ns);
+ }
+}
+
+void PresentationConnection::Send(const ArrayBuffer& aData, ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ AsyncCloseConnectionWithErrorMsg(
+ u"Unable to send message due to an internal error."_ns);
+ return;
+ }
+
+ aData.ComputeState();
+
+ static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required");
+
+ uint32_t length = aData.Length();
+ char* data = reinterpret_cast<char*>(aData.Data());
+ nsDependentCSubstring msgString(data, length);
+
+ nsresult rv = service->SendSessionBinaryMsg(mId, mRole, msgString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AsyncCloseConnectionWithErrorMsg(nsLiteralString(
+ u"Unable to send binary message for ArrayBuffer message."));
+ }
+}
+
+void PresentationConnection::Send(const ArrayBufferView& aData,
+ ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ AsyncCloseConnectionWithErrorMsg(
+ u"Unable to send message due to an internal error."_ns);
+ return;
+ }
+
+ aData.ComputeState();
+
+ static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required");
+
+ uint32_t length = aData.Length();
+ char* data = reinterpret_cast<char*>(aData.Data());
+ nsDependentCSubstring msgString(data, length);
+
+ nsresult rv = service->SendSessionBinaryMsg(mId, mRole, msgString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AsyncCloseConnectionWithErrorMsg(nsLiteralString(
+ u"Unable to send binary message for ArrayBufferView message."));
+ }
+}
+
+void PresentationConnection::Close(ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ // It only works when the state is CONNECTED or CONNECTING.
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected &&
+ mState != PresentationConnectionState::Connecting)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ aRv.Throw(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ Unused << NS_WARN_IF(NS_FAILED(service->CloseSession(
+ mId, mRole, nsIPresentationService::CLOSED_REASON_CLOSED)));
+}
+
+void PresentationConnection::Terminate(ErrorResult& aRv) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return;
+ }
+
+ // It only works when the state is CONNECTED.
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ aRv.Throw(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ Unused << NS_WARN_IF(NS_FAILED(service->TerminateSession(mId, mRole)));
+}
+
+bool PresentationConnection::Equals(uint64_t aWindowId, const nsAString& aId) {
+ return GetOwner() && aWindowId == GetOwner()->WindowID() && mId.Equals(aId);
+}
+
+NS_IMETHODIMP
+PresentationConnection::NotifyStateChange(const nsAString& aSessionId,
+ uint16_t aState, nsresult aReason) {
+ PRES_DEBUG("connection state change:id[%s], state[%" PRIx32
+ "], reason[%" PRIx32 "], role[%d]\n",
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aState,
+ static_cast<uint32_t>(aReason), mRole);
+
+ if (!aSessionId.Equals(mId)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // A terminated connection should always remain in terminated.
+ if (mState == PresentationConnectionState::Terminated) {
+ return NS_OK;
+ }
+
+ PresentationConnectionState state;
+ switch (aState) {
+ case nsIPresentationSessionListener::STATE_CONNECTING:
+ state = PresentationConnectionState::Connecting;
+ break;
+ case nsIPresentationSessionListener::STATE_CONNECTED:
+ state = PresentationConnectionState::Connected;
+ break;
+ case nsIPresentationSessionListener::STATE_CLOSED:
+ state = PresentationConnectionState::Closed;
+ break;
+ case nsIPresentationSessionListener::STATE_TERMINATED:
+ state = PresentationConnectionState::Terminated;
+ break;
+ default:
+ NS_WARNING("Unknown presentation session state.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mState == state) {
+ return NS_OK;
+ }
+ mState = state;
+
+ nsresult rv = ProcessStateChanged(aReason);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mOwningConnectionList) {
+ mOwningConnectionList->NotifyStateChange(aSessionId, this);
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationConnection::ProcessStateChanged(nsresult aReason) {
+ switch (mState) {
+ case PresentationConnectionState::Connecting:
+ return NS_OK;
+ case PresentationConnectionState::Connected: {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_OK;
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"connect"_ns, CanBubble::eNo);
+ return asyncDispatcher->PostDOMEvent();
+ }
+ case PresentationConnectionState::Closed: {
+ PresentationConnectionClosedReason reason =
+ PresentationConnectionClosedReason::Closed;
+
+ nsString errorMsg;
+ if (NS_FAILED(aReason)) {
+ reason = PresentationConnectionClosedReason::Error;
+ nsCString name, message;
+
+ // If aReason is not a DOM error, use error name as message.
+ if (NS_FAILED(
+ NS_GetNameAndMessageForDOMNSResult(aReason, name, message))) {
+ GetErrorName(aReason, message);
+ message.InsertLiteral("Internal error: ", 0);
+ }
+ CopyUTF8toUTF16(message, errorMsg);
+ }
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(DispatchConnectionCloseEvent(reason, errorMsg)));
+
+ return RemoveFromLoadGroup();
+ }
+ case PresentationConnectionState::Terminated: {
+ if (!nsContentUtils::ShouldResistFingerprinting()) {
+ // Ensure onterminate event is fired.
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"terminate"_ns, CanBubble::eNo);
+ Unused << 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;
+ }
+
+ return RemoveFromLoadGroup();
+ }
+ default:
+ MOZ_CRASH("Unknown presentation session state.");
+ return NS_ERROR_INVALID_ARG;
+ }
+}
+
+NS_IMETHODIMP
+PresentationConnection::NotifyMessage(const nsAString& aSessionId,
+ const nsACString& aData, bool aIsBinary) {
+ PRES_DEBUG("connection %s:id[%s], data[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(),
+ nsPromiseFlatCString(aData).get(), mRole);
+
+ if (!aSessionId.Equals(mId)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // No message should be expected when the session is not connected.
+ if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(DoReceiveMessage(aData, aIsBinary)))) {
+ AsyncCloseConnectionWithErrorMsg(u"Unable to receive a message."_ns);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationConnection::DoReceiveMessage(const nsACString& aData,
+ bool aIsBinary) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_OK;
+ }
+
+ // Transform the data.
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetOwner())) {
+ return NS_ERROR_FAILURE;
+ }
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> jsData(cx);
+
+ nsresult rv;
+ if (aIsBinary) {
+ if (mBinaryType == PresentationConnectionBinaryType::Blob) {
+ RefPtr<Blob> blob =
+ Blob::CreateStringBlob(GetOwnerGlobal(), aData, u""_ns);
+ if (NS_WARN_IF(!blob)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!ToJSValue(cx, blob, &jsData)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (mBinaryType == PresentationConnectionBinaryType::Arraybuffer) {
+ JS::Rooted<JSObject*> arrayBuf(cx);
+ rv = nsContentUtils::CreateArrayBuffer(cx, aData, arrayBuf.address());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ jsData.setObject(*arrayBuf);
+ } else {
+ MOZ_CRASH("Unknown binary type!");
+ }
+ } else {
+ NS_ConvertUTF8toUTF16 utf16Data(aData);
+ if (NS_WARN_IF(!ToJSValue(cx, utf16Data, &jsData))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return DispatchMessageEvent(jsData);
+}
+
+nsresult PresentationConnection::DispatchConnectionCloseEvent(
+ PresentationConnectionClosedReason aReason, const nsAString& aMessage,
+ bool aDispatchNow) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_OK;
+ }
+
+ if (mState != PresentationConnectionState::Closed) {
+ MOZ_ASSERT(false, "The connection state should be closed.");
+ return NS_ERROR_FAILURE;
+ }
+
+ PresentationConnectionCloseEventInit init;
+ init.mReason = aReason;
+ init.mMessage = aMessage;
+
+ RefPtr<PresentationConnectionCloseEvent> closedEvent =
+ PresentationConnectionCloseEvent::Constructor(this, u"close"_ns, init);
+ closedEvent->SetTrusted(true);
+
+ if (aDispatchNow) {
+ ErrorResult rv;
+ DispatchEvent(*closedEvent, rv);
+ return rv.StealNSResult();
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, closedEvent);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+nsresult PresentationConnection::DispatchMessageEvent(
+ JS::Handle<JS::Value> aData) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Get the origin.
+ nsAutoString origin;
+ nsresult rv = nsContentUtils::GetUTFOrigin(global->PrincipalOrNull(), origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<MessageEvent> messageEvent = new MessageEvent(this, nullptr, nullptr);
+
+ messageEvent->InitMessageEvent(
+ nullptr, u"message"_ns, CanBubble::eNo, Cancelable::eNo, aData, origin,
+ u""_ns, nullptr, Sequence<OwningNonNull<MessagePort>>());
+ messageEvent->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, messageEvent);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+nsresult PresentationConnection::ProcessConnectionWentAway() {
+ if (mState != PresentationConnectionState::Connected &&
+ mState != PresentationConnectionState::Connecting) {
+ // If the state is not connected or connecting, do not need to
+ // close the session.
+ return NS_OK;
+ }
+
+ mState = PresentationConnectionState::Terminated;
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return service->CloseSession(mId, mRole,
+ nsIPresentationService::CLOSED_REASON_WENTAWAY);
+}
+
+NS_IMETHODIMP
+PresentationConnection::GetName(nsACString& aResult) {
+ aResult.AssignLiteral("about:presentation-connection");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationConnection::IsPending(bool* aRetval) {
+ *aRetval = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationConnection::GetStatus(nsresult* aStatus) {
+ *aStatus = NS_OK;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationConnection::Cancel(nsresult aStatus) {
+ nsCOMPtr<nsIRunnable> event = NewRunnableMethod(
+ "dom::PresentationConnection::ProcessConnectionWentAway", this,
+ &PresentationConnection::ProcessConnectionWentAway);
+ return NS_DispatchToCurrentThread(event);
+}
+NS_IMETHODIMP
+PresentationConnection::Suspend(void) { return NS_ERROR_NOT_IMPLEMENTED; }
+NS_IMETHODIMP
+PresentationConnection::Resume(void) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+PresentationConnection::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ *aLoadGroup = nullptr;
+
+ nsCOMPtr<Document> doc = GetOwner() ? GetOwner()->GetExtantDoc() : nullptr;
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aLoadGroup = doc->GetDocumentLoadGroup().take();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationConnection::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+PresentationConnection::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = nsIRequest::LOAD_BACKGROUND;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationConnection::SetLoadFlags(nsLoadFlags aLoadFlags) { return NS_OK; }
+
+NS_IMETHODIMP
+PresentationConnection::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+PresentationConnection::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+nsresult PresentationConnection::AddIntoLoadGroup() {
+ // Avoid adding to loadgroup multiple times
+ if (mWeakLoadGroup) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ nsresult rv = GetLoadGroup(getter_AddRefs(loadGroup));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = loadGroup->AddRequest(this, nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mWeakLoadGroup = do_GetWeakReference(loadGroup);
+ return NS_OK;
+}
+
+nsresult PresentationConnection::RemoveFromLoadGroup() {
+ if (!mWeakLoadGroup) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup = do_QueryReferent(mWeakLoadGroup);
+ if (loadGroup) {
+ mWeakLoadGroup = nullptr;
+ return loadGroup->RemoveRequest(this, nullptr, NS_OK);
+ }
+
+ return NS_OK;
+}
+
+void PresentationConnection::AsyncCloseConnectionWithErrorMsg(
+ const nsAString& aMessage) {
+ if (mState == PresentationConnectionState::Terminated) {
+ return;
+ }
+
+ nsString message = nsString(aMessage);
+ RefPtr<PresentationConnection> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "dom::PresentationConnection::AsyncCloseConnectionWithErrorMsg",
+ [self, message]() -> void {
+ // Set |mState| to |PresentationConnectionState::Closed| here to avoid
+ // calling |ProcessStateChanged|.
+ self->mState = PresentationConnectionState::Closed;
+
+ // Make sure dispatching the event and closing the connection are
+ // invoked at the same time by setting |aDispatchNow| to true.
+ Unused << NS_WARN_IF(NS_FAILED(self->DispatchConnectionCloseEvent(
+ PresentationConnectionClosedReason::Error, message, true)));
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return;
+ }
+
+ Unused << NS_WARN_IF(NS_FAILED(service->CloseSession(
+ self->mId, self->mRole,
+ nsIPresentationService::CLOSED_REASON_ERROR)));
+ });
+
+ Unused << NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(r)));
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationConnection.h b/dom/presentation/PresentationConnection.h
new file mode 100644
index 0000000000..dc994bf295
--- /dev/null
+++ b/dom/presentation/PresentationConnection.h
@@ -0,0 +1,116 @@
+/* -*- 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_PresentationConnection_h
+#define mozilla_dom_PresentationConnection_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/PresentationConnectionBinding.h"
+#include "mozilla/dom/PresentationConnectionCloseEventBinding.h"
+#include "nsIPresentationListener.h"
+#include "nsIRequest.h"
+#include "nsIWeakReferenceUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+class Blob;
+class PresentationConnectionList;
+
+class PresentationConnection final : public DOMEventTargetHelper,
+ public nsIPresentationSessionListener,
+ public nsIRequest,
+ public SupportsWeakPtr {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PresentationConnection,
+ DOMEventTargetHelper)
+ NS_DECL_NSIPRESENTATIONSESSIONLISTENER
+ NS_DECL_NSIREQUEST
+
+ static already_AddRefed<PresentationConnection> Create(
+ nsPIDOMWindowInner* aWindow, const nsAString& aId, const nsAString& aUrl,
+ const uint8_t aRole, PresentationConnectionList* aList = nullptr);
+
+ virtual void DisconnectFromOwner() override;
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL (public APIs)
+ void GetId(nsAString& aId) const;
+
+ void GetUrl(nsAString& aUrl) const;
+
+ PresentationConnectionState State() const;
+
+ PresentationConnectionBinaryType BinaryType() const;
+
+ void SetBinaryType(PresentationConnectionBinaryType aType);
+
+ void Send(const nsAString& aData, ErrorResult& aRv);
+
+ void Send(Blob& aData, ErrorResult& aRv);
+
+ void Send(const ArrayBuffer& aData, ErrorResult& aRv);
+
+ void Send(const ArrayBufferView& aData, ErrorResult& aRv);
+
+ void Close(ErrorResult& aRv);
+
+ void Terminate(ErrorResult& aRv);
+
+ bool Equals(uint64_t aWindowId, const nsAString& aId);
+
+ IMPL_EVENT_HANDLER(connect);
+ IMPL_EVENT_HANDLER(close);
+ IMPL_EVENT_HANDLER(terminate);
+ IMPL_EVENT_HANDLER(message);
+
+ private:
+ PresentationConnection(nsPIDOMWindowInner* aWindow, const nsAString& aId,
+ const nsAString& aUrl, const uint8_t aRole,
+ PresentationConnectionList* aList);
+
+ ~PresentationConnection();
+
+ bool Init();
+
+ void Shutdown();
+
+ nsresult ProcessStateChanged(nsresult aReason);
+
+ nsresult DispatchConnectionCloseEvent(
+ PresentationConnectionClosedReason aReason, const nsAString& aMessage,
+ bool aDispatchNow = false);
+
+ nsresult DispatchMessageEvent(JS::Handle<JS::Value> aData);
+
+ nsresult ProcessConnectionWentAway();
+
+ nsresult AddIntoLoadGroup();
+
+ nsresult RemoveFromLoadGroup();
+
+ void AsyncCloseConnectionWithErrorMsg(const nsAString& aMessage);
+
+ nsresult DoReceiveMessage(const nsACString& aData, bool aIsBinary);
+
+ nsString mId;
+ nsString mUrl;
+ uint8_t mRole;
+ PresentationConnectionState mState;
+ RefPtr<PresentationConnectionList> mOwningConnectionList;
+ nsWeakPtr mWeakLoadGroup;
+ PresentationConnectionBinaryType mBinaryType;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationConnection_h
diff --git a/dom/presentation/PresentationConnectionList.cpp b/dom/presentation/PresentationConnectionList.cpp
new file mode 100644
index 0000000000..7b2cc0b5a1
--- /dev/null
+++ b/dom/presentation/PresentationConnectionList.cpp
@@ -0,0 +1,124 @@
+/* -*- 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 "PresentationConnectionList.h"
+
+#include "nsContentUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/PresentationConnectionAvailableEvent.h"
+#include "mozilla/dom/PresentationConnectionListBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "PresentationConnection.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PresentationConnectionList,
+ DOMEventTargetHelper,
+ mGetConnectionListPromise, mConnections)
+
+NS_IMPL_ADDREF_INHERITED(PresentationConnectionList, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationConnectionList, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationConnectionList)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+PresentationConnectionList::PresentationConnectionList(
+ nsPIDOMWindowInner* aWindow, Promise* aPromise)
+ : DOMEventTargetHelper(aWindow), mGetConnectionListPromise(aPromise) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aPromise);
+}
+
+/* virtual */
+JSObject* PresentationConnectionList::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PresentationConnectionList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PresentationConnectionList::GetConnections(
+ nsTArray<RefPtr<PresentationConnection>>& aConnections) const {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ aConnections.Clear();
+ return;
+ }
+
+ aConnections = mConnections.Clone();
+}
+
+nsresult PresentationConnectionList::DispatchConnectionAvailableEvent(
+ PresentationConnection* aConnection) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_OK;
+ }
+
+ PresentationConnectionAvailableEventInit init;
+ init.mConnection = aConnection;
+
+ RefPtr<PresentationConnectionAvailableEvent> event =
+ PresentationConnectionAvailableEvent::Constructor(
+ this, u"connectionavailable"_ns, init);
+
+ if (NS_WARN_IF(!event)) {
+ return NS_ERROR_FAILURE;
+ }
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+PresentationConnectionList::ConnectionArrayIndex
+PresentationConnectionList::FindConnectionById(const nsAString& aId) {
+ for (ConnectionArrayIndex i = 0; i < mConnections.Length(); i++) {
+ nsAutoString id;
+ mConnections[i]->GetId(id);
+ if (id == aId) {
+ return i;
+ }
+ }
+
+ return ConnectionArray::NoIndex;
+}
+
+void PresentationConnectionList::NotifyStateChange(
+ const nsAString& aSessionId, PresentationConnection* aConnection) {
+ if (!aConnection) {
+ MOZ_ASSERT(false, "PresentationConnection can not be null.");
+ return;
+ }
+
+ bool connectionFound =
+ FindConnectionById(aSessionId) != ConnectionArray::NoIndex;
+
+ PresentationConnectionList_Binding::ClearCachedConnectionsValue(this);
+ switch (aConnection->State()) {
+ case PresentationConnectionState::Connected:
+ if (!connectionFound) {
+ mConnections.AppendElement(aConnection);
+ if (mGetConnectionListPromise) {
+ if (!nsContentUtils::ShouldResistFingerprinting()) {
+ mGetConnectionListPromise->MaybeResolve(this);
+ }
+ mGetConnectionListPromise = nullptr;
+ return;
+ }
+ }
+ DispatchConnectionAvailableEvent(aConnection);
+ break;
+ case PresentationConnectionState::Terminated:
+ if (connectionFound) {
+ mConnections.RemoveElement(aConnection);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationConnectionList.h b/dom/presentation/PresentationConnectionList.h
new file mode 100644
index 0000000000..60412daf25
--- /dev/null
+++ b/dom/presentation/PresentationConnectionList.h
@@ -0,0 +1,58 @@
+/* -*- 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_PresentationConnectionList_h
+#define mozilla_dom_PresentationConnectionList_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationConnection;
+class Promise;
+
+class PresentationConnectionList final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PresentationConnectionList,
+ DOMEventTargetHelper)
+
+ PresentationConnectionList(nsPIDOMWindowInner* aWindow, Promise* aPromise);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetConnections(
+ nsTArray<RefPtr<PresentationConnection>>& aConnections) const;
+
+ void NotifyStateChange(const nsAString& aSessionId,
+ PresentationConnection* aConnection);
+
+ IMPL_EVENT_HANDLER(connectionavailable);
+
+ private:
+ virtual ~PresentationConnectionList() = default;
+
+ nsresult DispatchConnectionAvailableEvent(
+ PresentationConnection* aConnection);
+
+ typedef nsTArray<RefPtr<PresentationConnection>> ConnectionArray;
+ typedef ConnectionArray::index_type ConnectionArrayIndex;
+
+ ConnectionArrayIndex FindConnectionById(const nsAString& aId);
+
+ RefPtr<Promise> mGetConnectionListPromise;
+
+ // This array stores only non-terminsted connections.
+ ConnectionArray mConnections;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationConnectionList_h
diff --git a/dom/presentation/PresentationDataChannelSessionTransport.jsm b/dom/presentation/PresentationDataChannelSessionTransport.jsm
new file mode 100644
index 0000000000..f3e788a1a7
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.jsm
@@ -0,0 +1,421 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Bug 1228209 - plan to remove this eventually
+function log(aMsg) {
+ // dump("-*- PresentationDataChannelSessionTransport.js : " + aMsg + "\n");
+}
+
+const PRESENTATIONTRANSPORT_CID = Components.ID(
+ "{dd2bbf2f-3399-4389-8f5f-d382afb8b2d6}"
+);
+const PRESENTATIONTRANSPORT_CONTRACTID =
+ "mozilla.org/presentation/datachanneltransport;1";
+
+const PRESENTATIONTRANSPORTBUILDER_CID = Components.ID(
+ "{215b2f62-46e2-4004-a3d1-6858e56c20f3}"
+);
+const PRESENTATIONTRANSPORTBUILDER_CONTRACTID =
+ "mozilla.org/presentation/datachanneltransportbuilder;1";
+
+function PresentationDataChannelDescription(aDataChannelSDP) {
+ this._dataChannelSDP = JSON.stringify(aDataChannelSDP);
+}
+
+PresentationDataChannelDescription.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+ get type() {
+ return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL;
+ },
+ get tcpAddress() {
+ return null;
+ },
+ get tcpPort() {
+ return null;
+ },
+ get dataChannelSDP() {
+ return this._dataChannelSDP;
+ },
+};
+
+function PresentationTransportBuilder() {
+ log("PresentationTransportBuilder construct");
+ this._isControlChannelNeeded = true;
+}
+
+PresentationTransportBuilder.prototype = {
+ classID: PRESENTATIONTRANSPORTBUILDER_CID,
+ contractID: PRESENTATIONTRANSPORTBUILDER_CONTRACTID,
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportBuilder",
+ "nsIPresentationDataChannelSessionTransportBuilder",
+ "nsITimerCallback",
+ ]),
+
+ buildDataChannelTransport(aRole, aWindow, aListener) {
+ if (!aRole || !aWindow || !aListener) {
+ log("buildDataChannelTransport with illegal parameters");
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (this._window) {
+ log("buildDataChannelTransport has started.");
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ log("buildDataChannelTransport with role " + aRole);
+ this._role = aRole;
+ this._window = aWindow;
+ this._listener = aListener.QueryInterface(
+ Ci.nsIPresentationSessionTransportBuilderListener
+ );
+
+ // TODO bug 1227053 set iceServers from |nsIPresentationDevice|
+ this._peerConnection = new this._window.RTCPeerConnection();
+
+ // |this._listener == null| will throw since the control channel is
+ // abnormally closed.
+ this._peerConnection.onicecandidate = aEvent =>
+ aEvent.candidate &&
+ this._listener.sendIceCandidate(JSON.stringify(aEvent.candidate));
+
+ this._peerConnection.onnegotiationneeded = () => {
+ log("onnegotiationneeded with role " + this._role);
+ if (!this._peerConnection) {
+ log("ignoring negotiationneeded without PeerConnection");
+ return;
+ }
+ this._peerConnection
+ .createOffer()
+ .then(aOffer => this._peerConnection.setLocalDescription(aOffer))
+ .then(() =>
+ this._listener.sendOffer(
+ new PresentationDataChannelDescription(
+ this._peerConnection.localDescription
+ )
+ )
+ )
+ .catch(e => this._reportError(e));
+ };
+
+ switch (this._role) {
+ case Ci.nsIPresentationService.ROLE_CONTROLLER:
+ this._dataChannel = this._peerConnection.createDataChannel(
+ "presentationAPI"
+ );
+ this._setDataChannel();
+ break;
+
+ case Ci.nsIPresentationService.ROLE_RECEIVER:
+ this._peerConnection.ondatachannel = aEvent => {
+ this._dataChannel = aEvent.channel;
+ // Ensure the binaryType of dataChannel is blob.
+ this._dataChannel.binaryType = "blob";
+ this._setDataChannel();
+ };
+ break;
+ default:
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ // TODO bug 1228235 we should have a way to let device providers customize
+ // the time-out duration.
+ let timeout = Services.prefs.getIntPref(
+ "presentation.receiver.loading.timeout",
+ 10000
+ );
+
+ // The timer is to check if the negotiation finishes on time.
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(this, timeout, this._timer.TYPE_ONE_SHOT);
+ },
+
+ notify() {
+ if (!this._sessionTransport) {
+ this._cleanup(Cr.NS_ERROR_NET_TIMEOUT);
+ }
+ },
+
+ _reportError(aError) {
+ log("report Error " + aError.name + ":" + aError.message);
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ },
+
+ _setDataChannel() {
+ this._dataChannel.onopen = () => {
+ log("data channel is open, notify the listener, role " + this._role);
+
+ // Handoff the ownership of _peerConnection and _dataChannel to
+ // _sessionTransport
+ this._sessionTransport = new PresentationTransport();
+ this._sessionTransport.init(
+ this._peerConnection,
+ this._dataChannel,
+ this._window
+ );
+ this._peerConnection.onicecandidate = null;
+ this._peerConnection.onnegotiationneeded = null;
+ this._peerConnection = this._dataChannel = null;
+
+ this._listener.onSessionTransport(this._sessionTransport);
+ this._sessionTransport.callback.notifyTransportReady();
+
+ this._cleanup(Cr.NS_OK);
+ };
+
+ this._dataChannel.onerror = aError => {
+ log("data channel onerror " + aError.name + ":" + aError.message);
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ };
+ },
+
+ _cleanup(aReason) {
+ if (aReason != Cr.NS_OK) {
+ this._listener.onError(aReason);
+ }
+
+ if (this._dataChannel) {
+ this._dataChannel.close();
+ this._dataChannel = null;
+ }
+
+ if (this._peerConnection) {
+ this._peerConnection.close();
+ this._peerConnection = null;
+ }
+
+ this._role = null;
+ this._window = null;
+
+ this._listener = null;
+ this._sessionTransport = null;
+
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ },
+
+ // nsIPresentationControlChannelListener
+ onOffer(aOffer) {
+ if (
+ this._role !== Ci.nsIPresentationService.ROLE_RECEIVER ||
+ this._sessionTransport
+ ) {
+ log("onOffer status error");
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ }
+
+ log("onOffer: " + aOffer.dataChannelSDP + " with role " + this._role);
+
+ let offer = new this._window.RTCSessionDescription(
+ JSON.parse(aOffer.dataChannelSDP)
+ );
+
+ this._peerConnection
+ .setRemoteDescription(offer)
+ .then(
+ () =>
+ this._peerConnection.signalingState == "stable" ||
+ this._peerConnection.createAnswer()
+ )
+ .then(aAnswer => this._peerConnection.setLocalDescription(aAnswer))
+ .then(() => {
+ this._isControlChannelNeeded = false;
+ this._listener.sendAnswer(
+ new PresentationDataChannelDescription(
+ this._peerConnection.localDescription
+ )
+ );
+ })
+ .catch(e => this._reportError(e));
+ },
+
+ onAnswer(aAnswer) {
+ if (
+ this._role !== Ci.nsIPresentationService.ROLE_CONTROLLER ||
+ this._sessionTransport
+ ) {
+ log("onAnswer status error");
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ }
+
+ log("onAnswer: " + aAnswer.dataChannelSDP + " with role " + this._role);
+
+ let answer = new this._window.RTCSessionDescription(
+ JSON.parse(aAnswer.dataChannelSDP)
+ );
+
+ this._peerConnection
+ .setRemoteDescription(answer)
+ .catch(e => this._reportError(e));
+ this._isControlChannelNeeded = false;
+ },
+
+ onIceCandidate(aCandidate) {
+ log("onIceCandidate: " + aCandidate + " with role " + this._role);
+ if (!this._window || !this._peerConnection) {
+ log("ignoring ICE candidate after connection");
+ return;
+ }
+ let candidate = new this._window.RTCIceCandidate(JSON.parse(aCandidate));
+ this._peerConnection
+ .addIceCandidate(candidate)
+ .catch(e => this._reportError(e));
+ },
+
+ notifyDisconnected(aReason) {
+ log("notifyDisconnected reason: " + aReason);
+
+ if (aReason != Cr.NS_OK) {
+ this._cleanup(aReason);
+ } else if (this._isControlChannelNeeded) {
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ }
+ },
+};
+
+function PresentationTransport() {
+ this._messageQueue = [];
+ this._closeReason = Cr.NS_OK;
+}
+
+PresentationTransport.prototype = {
+ classID: PRESENTATIONTRANSPORT_CID,
+ contractID: PRESENTATIONTRANSPORT_CONTRACTID,
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransport"]),
+
+ init(aPeerConnection, aDataChannel, aWindow) {
+ log("initWithDataChannel");
+ this._enableDataNotification = false;
+ this._dataChannel = aDataChannel;
+ this._peerConnection = aPeerConnection;
+ this._window = aWindow;
+
+ this._dataChannel.onopen = () => {
+ log("data channel reopen. Should never touch here");
+ };
+
+ this._dataChannel.onclose = () => {
+ log("data channel onclose");
+ if (this._callback) {
+ this._callback.notifyTransportClosed(this._closeReason);
+ }
+ this._cleanup();
+ };
+
+ this._dataChannel.onmessage = aEvent => {
+ log("data channel onmessage " + aEvent.data);
+
+ if (!this._enableDataNotification || !this._callback) {
+ log("queue message");
+ this._messageQueue.push(aEvent.data);
+ return;
+ }
+ this._doNotifyData(aEvent.data);
+ };
+
+ this._dataChannel.onerror = aError => {
+ log("data channel onerror " + aError.name + ":" + aError.message);
+ if (this._callback) {
+ this._callback.notifyTransportClosed(Cr.NS_ERROR_FAILURE);
+ }
+ this._cleanup();
+ };
+ },
+
+ // nsIPresentationTransport
+ get selfAddress() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+
+ get callback() {
+ return this._callback;
+ },
+
+ set callback(aCallback) {
+ this._callback = aCallback;
+ },
+
+ send(aData) {
+ log("send " + aData);
+ this._dataChannel.send(aData);
+ },
+
+ sendBinaryMsg(aData) {
+ log("sendBinaryMsg");
+
+ let array = new Uint8Array(aData.length);
+ for (let i = 0; i < aData.length; i++) {
+ array[i] = aData.charCodeAt(i);
+ }
+
+ this._dataChannel.send(array);
+ },
+
+ sendBlob(aBlob) {
+ log("sendBlob");
+
+ this._dataChannel.send(aBlob);
+ },
+
+ enableDataNotification() {
+ log("enableDataNotification");
+ if (this._enableDataNotification) {
+ return;
+ }
+
+ if (!this._callback) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ this._enableDataNotification = true;
+
+ this._messageQueue.forEach(aData => this._doNotifyData(aData));
+ this._messageQueue = [];
+ },
+
+ close(aReason) {
+ this._closeReason = aReason;
+
+ this._dataChannel.close();
+ },
+
+ _cleanup() {
+ this._dataChannel = null;
+
+ if (this._peerConnection) {
+ this._peerConnection.close();
+ this._peerConnection = null;
+ }
+ this._callback = null;
+ this._messageQueue = [];
+ this._window = null;
+ },
+
+ _doNotifyData(aData) {
+ if (!this._callback) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ if (aData instanceof this._window.Blob) {
+ let reader = new this._window.FileReader();
+ reader.addEventListener("load", aEvent => {
+ this._callback.notifyData(aEvent.target.result, true);
+ });
+ reader.readAsBinaryString(aData);
+ } else {
+ this._callback.notifyData(aData, false);
+ }
+ },
+};
+
+var EXPORTED_SYMBOLS = [
+ "PresentationTransportBuilder",
+ "PresentationTransport",
+];
diff --git a/dom/presentation/PresentationDeviceManager.cpp b/dom/presentation/PresentationDeviceManager.cpp
new file mode 100644
index 0000000000..185f541430
--- /dev/null
+++ b/dom/presentation/PresentationDeviceManager.cpp
@@ -0,0 +1,307 @@
+/* -*- 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 "PresentationDeviceManager.h"
+
+#include "mozilla/Services.h"
+#include "MainThreadUtils.h"
+#include "nsArrayUtils.h"
+#include "nsCategoryCache.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIMutableArray.h"
+#include "nsIObserverService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "PresentationSessionRequest.h"
+#include "PresentationTerminateRequest.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationDeviceManager, nsIPresentationDeviceManager,
+ nsIPresentationDeviceListener, nsIObserver,
+ nsISupportsWeakReference)
+
+PresentationDeviceManager::PresentationDeviceManager() = default;
+
+PresentationDeviceManager::~PresentationDeviceManager() {
+ UnloadDeviceProviders();
+ mDevices.Clear();
+}
+
+void PresentationDeviceManager::Init() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ }
+
+ LoadDeviceProviders();
+}
+
+void PresentationDeviceManager::Shutdown() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ UnloadDeviceProviders();
+}
+
+void PresentationDeviceManager::LoadDeviceProviders() {
+ MOZ_ASSERT(mProviders.IsEmpty());
+
+ nsCategoryCache<nsIPresentationDeviceProvider> providerCache(
+ PRESENTATION_DEVICE_PROVIDER_CATEGORY);
+ providerCache.GetEntries(mProviders);
+
+ for (uint32_t i = 0; i < mProviders.Length(); ++i) {
+ mProviders[i]->SetListener(this);
+ }
+}
+
+void PresentationDeviceManager::UnloadDeviceProviders() {
+ for (uint32_t i = 0; i < mProviders.Length(); ++i) {
+ mProviders[i]->SetListener(nullptr);
+ }
+
+ mProviders.Clear();
+}
+
+void PresentationDeviceManager::NotifyDeviceChange(
+ nsIPresentationDevice* aDevice, const char16_t* aType) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aDevice, PRESENTATION_DEVICE_CHANGE_TOPIC, aType);
+ }
+}
+
+// nsIPresentationDeviceManager
+NS_IMETHODIMP
+PresentationDeviceManager::ForceDiscovery() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (uint32_t i = 0; i < mProviders.Length(); ++i) {
+ mProviders[i]->ForceDiscovery();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::AddDeviceProvider(
+ nsIPresentationDeviceProvider* aProvider) {
+ NS_ENSURE_ARG(aProvider);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(mProviders.Contains(aProvider))) {
+ return NS_OK;
+ }
+
+ mProviders.AppendElement(aProvider);
+ aProvider->SetListener(this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::RemoveDeviceProvider(
+ nsIPresentationDeviceProvider* aProvider) {
+ NS_ENSURE_ARG(aProvider);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mProviders.RemoveElement(aProvider))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aProvider->SetListener(nullptr);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::GetDeviceAvailable(bool* aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ *aRetVal = !mDevices.IsEmpty();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::GetAvailableDevices(nsIArray* aPresentationUrls,
+ nsIArray** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Bug 1194049: some providers may discontinue discovery after timeout.
+ // Call |ForceDiscovery()| here to make sure device lists are updated.
+ NS_DispatchToMainThread(
+ NewRunnableMethod("dom::PresentationDeviceManager::ForceDiscovery", this,
+ &PresentationDeviceManager::ForceDiscovery));
+
+ nsTArray<nsString> presentationUrls;
+ if (aPresentationUrls) {
+ uint32_t length;
+ nsresult rv = aPresentationUrls->GetLength(&length);
+ if (NS_SUCCEEDED(rv)) {
+ for (uint32_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsISupportsString> isupportStr =
+ do_QueryElementAt(aPresentationUrls, i);
+
+ nsAutoString presentationUrl;
+ rv = isupportStr->GetData(presentationUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ presentationUrls.AppendElement(presentationUrl);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIMutableArray> devices = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ for (uint32_t i = 0; i < mDevices.Length(); ++i) {
+ if (presentationUrls.IsEmpty()) {
+ devices->AppendElement(mDevices[i]);
+ continue;
+ }
+
+ for (uint32_t j = 0; j < presentationUrls.Length(); ++j) {
+ bool isSupported;
+ if (NS_SUCCEEDED(mDevices[i]->IsRequestedUrlSupported(presentationUrls[j],
+ &isSupported)) &&
+ isSupported) {
+ devices->AppendElement(mDevices[i]);
+ break;
+ }
+ }
+ }
+
+ devices.forget(aRetVal);
+
+ return NS_OK;
+}
+
+// nsIPresentationDeviceListener
+NS_IMETHODIMP
+PresentationDeviceManager::AddDevice(nsIPresentationDevice* aDevice) {
+ NS_ENSURE_ARG(aDevice);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(mDevices.Contains(aDevice))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mDevices.AppendElement(aDevice);
+
+ NotifyDeviceChange(aDevice, u"add");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::RemoveDevice(nsIPresentationDevice* aDevice) {
+ NS_ENSURE_ARG(aDevice);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int32_t index = mDevices.IndexOf(aDevice);
+ if (NS_WARN_IF(index < 0)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mDevices.RemoveElementAt(index);
+
+ NotifyDeviceChange(aDevice, u"remove");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::UpdateDevice(nsIPresentationDevice* aDevice) {
+ NS_ENSURE_ARG(aDevice);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mDevices.Contains(aDevice))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NotifyDeviceChange(aDevice, u"update");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::OnSessionRequest(
+ nsIPresentationDevice* aDevice, const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel) {
+ NS_ENSURE_ARG(aDevice);
+ NS_ENSURE_ARG(aControlChannel);
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
+
+ RefPtr<PresentationSessionRequest> request = 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;
+}
+
+NS_IMETHODIMP
+PresentationDeviceManager::OnReconnectRequest(
+ nsIPresentationDevice* aDevice, const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel) {
+ NS_ENSURE_ARG(aDevice);
+ NS_ENSURE_ARG(aControlChannel);
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
+
+ RefPtr<PresentationSessionRequest> request = new PresentationSessionRequest(
+ aDevice, aUrl, aPresentationId, aControlChannel);
+ obs->NotifyObservers(request, PRESENTATION_RECONNECT_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();
+ } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ Shutdown();
+ }
+
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationDeviceManager.h b/dom/presentation/PresentationDeviceManager.h
new file mode 100644
index 0000000000..88a9f42345
--- /dev/null
+++ b/dom/presentation/PresentationDeviceManager.h
@@ -0,0 +1,53 @@
+/* -*- 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_PresentationDeviceManager_h__
+#define mozilla_dom_PresentationDeviceManager_h__
+
+#include "nsIObserver.h"
+#include "nsIPresentationDevice.h"
+#include "nsIPresentationDeviceManager.h"
+#include "nsIPresentationDeviceProvider.h"
+#include "nsCOMArray.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationDeviceManager final : public nsIPresentationDeviceManager,
+ public nsIPresentationDeviceListener,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICEMANAGER
+ NS_DECL_NSIPRESENTATIONDEVICELISTENER
+ NS_DECL_NSIOBSERVER
+
+ PresentationDeviceManager();
+
+ private:
+ virtual ~PresentationDeviceManager();
+
+ void Init();
+
+ void Shutdown();
+
+ void LoadDeviceProviders();
+
+ void UnloadDeviceProviders();
+
+ void NotifyDeviceChange(nsIPresentationDevice* aDevice,
+ const char16_t* aType);
+
+ nsCOMArray<nsIPresentationDeviceProvider> mProviders;
+ nsCOMArray<nsIPresentationDevice> mDevices;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_PresentationDeviceManager_h__ */
diff --git a/dom/presentation/PresentationLog.h b/dom/presentation/PresentationLog.h
new file mode 100644
index 0000000000..36e1f19948
--- /dev/null
+++ b/dom/presentation/PresentationLog.h
@@ -0,0 +1,33 @@
+/* -*- 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_PresentationLog_h
+#define mozilla_dom_PresentationLog_h
+
+/*
+ * MOZ_LOG=Presentation:5
+ * For detail, see PresentationService.cpp
+ */
+namespace mozilla {
+
+class LazyLogModule;
+
+namespace dom {
+extern mozilla::LazyLogModule gPresentationLog;
+}
+} // namespace mozilla
+
+#undef PRES_ERROR
+#define PRES_ERROR(...) \
+ MOZ_LOG(mozilla::dom::gPresentationLog, mozilla::LogLevel::Error, \
+ (__VA_ARGS__))
+
+#undef PRES_DEBUG
+#define PRES_DEBUG(...) \
+ MOZ_LOG(mozilla::dom::gPresentationLog, mozilla::LogLevel::Debug, \
+ (__VA_ARGS__))
+
+#endif // mozilla_dom_PresentationLog_h
diff --git a/dom/presentation/PresentationNetworkHelper.jsm b/dom/presentation/PresentationNetworkHelper.jsm
new file mode 100644
index 0000000000..fd21c19142
--- /dev/null
+++ b/dom/presentation/PresentationNetworkHelper.jsm
@@ -0,0 +1,27 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { EventDispatcher } = ChromeUtils.import(
+ "resource://gre/modules/Messaging.jsm"
+);
+
+function PresentationNetworkHelper() {}
+
+PresentationNetworkHelper.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationNetworkHelper"]),
+
+ getWifiIPAddress(aListener) {
+ EventDispatcher.instance
+ .sendRequestForResult({ type: "Wifi:GetIPAddress" })
+ .then(
+ result => aListener.onGetWifiIPAddress(result),
+ err => aListener.onError(err)
+ );
+ },
+};
+
+var EXPORTED_SYMBOLS = ["PresentationNetworkHelper"];
diff --git a/dom/presentation/PresentationReceiver.cpp b/dom/presentation/PresentationReceiver.cpp
new file mode 100644
index 0000000000..6eb9c87b8a
--- /dev/null
+++ b/dom/presentation/PresentationReceiver.cpp
@@ -0,0 +1,169 @@
+/* -*- 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 "PresentationReceiver.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/dom/PresentationReceiverBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsIDocShell.h"
+#include "nsIPresentationService.h"
+#include "nsPIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "PresentationConnection.h"
+#include "PresentationConnectionList.h"
+#include "PresentationLog.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PresentationReceiver, mOwner,
+ mGetConnectionListPromise,
+ mConnectionList)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PresentationReceiver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PresentationReceiver)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationReceiver)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationRespondingListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<PresentationReceiver> PresentationReceiver::Create(
+ nsPIDOMWindowInner* aWindow) {
+ RefPtr<PresentationReceiver> receiver = new PresentationReceiver(aWindow);
+ return NS_WARN_IF(!receiver->Init()) ? nullptr : receiver.forget();
+}
+
+PresentationReceiver::PresentationReceiver(nsPIDOMWindowInner* aWindow)
+ : mOwner(aWindow) {
+ MOZ_ASSERT(aWindow);
+}
+
+PresentationReceiver::~PresentationReceiver() { Shutdown(); }
+
+bool PresentationReceiver::Init() {
+ if (NS_WARN_IF(!mOwner)) {
+ return false;
+ }
+ mWindowId = mOwner->WindowID();
+
+ nsCOMPtr<nsIDocShell> docShell = mOwner->GetDocShell();
+ MOZ_ASSERT(docShell);
+
+ nsContentUtils::GetPresentationURL(docShell, mUrl);
+ return !mUrl.IsEmpty();
+}
+
+void PresentationReceiver::Shutdown() {
+ PRES_DEBUG("receiver shutdown:windowId[%" PRId64 "]\n", mWindowId);
+
+ // Unregister listener for incoming sessions.
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return;
+ }
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(service->UnregisterRespondingListener(mWindowId)));
+}
+
+/* virtual */
+JSObject* PresentationReceiver::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PresentationReceiver_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMETHODIMP
+PresentationReceiver::NotifySessionConnect(uint64_t aWindowId,
+ const nsAString& aSessionId) {
+ PRES_DEBUG("receiver session connect:id[%s], windowId[%" PRIx64 "]\n",
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aWindowId);
+
+ if (NS_WARN_IF(!mOwner)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(aWindowId != mWindowId)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!mConnectionList)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<PresentationConnection> connection = PresentationConnection::Create(
+ mOwner, aSessionId, mUrl, nsIPresentationService::ROLE_RECEIVER,
+ mConnectionList);
+ if (NS_WARN_IF(!connection)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<Promise> PresentationReceiver::GetConnectionList(
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mOwner);
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ if (!mGetConnectionListPromise) {
+ mGetConnectionListPromise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<PresentationReceiver> self = this;
+ nsresult rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "dom::PresentationReceiver::GetConnectionList",
+ [self]() -> void { self->CreateConnectionList(); }));
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+ }
+
+ RefPtr<Promise> promise = mGetConnectionListPromise;
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ return promise.forget();
+}
+
+void PresentationReceiver::CreateConnectionList() {
+ MOZ_ASSERT(mGetConnectionListPromise);
+
+ if (mConnectionList) {
+ return;
+ }
+
+ mConnectionList =
+ new PresentationConnectionList(mOwner, mGetConnectionListPromise);
+
+ // Register listener for incoming sessions.
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ mGetConnectionListPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ nsresult rv = service->RegisterRespondingListener(mWindowId, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mGetConnectionListPromise->MaybeReject(rv);
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationReceiver.h b/dom/presentation/PresentationReceiver.h
new file mode 100644
index 0000000000..b71cf0b894
--- /dev/null
+++ b/dom/presentation/PresentationReceiver.h
@@ -0,0 +1,69 @@
+/* -*- 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_PresentationReceiver_h
+#define mozilla_dom_PresentationReceiver_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationListener.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class PresentationConnection;
+class PresentationConnectionList;
+class Promise;
+
+class PresentationReceiver final : public nsIPresentationRespondingListener,
+ public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PresentationReceiver)
+ NS_DECL_NSIPRESENTATIONRESPONDINGLISTENER
+
+ static already_AddRefed<PresentationReceiver> Create(
+ nsPIDOMWindowInner* aWindow);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsPIDOMWindowInner* GetParentObject() const { return mOwner; }
+
+ // WebIDL (public APIs)
+ already_AddRefed<Promise> GetConnectionList(ErrorResult& aRv);
+
+ private:
+ explicit PresentationReceiver(nsPIDOMWindowInner* aWindow);
+
+ virtual ~PresentationReceiver();
+
+ MOZ_IS_CLASS_INIT bool Init();
+
+ void Shutdown();
+
+ void CreateConnectionList();
+
+ // Store the inner window ID for |UnregisterRespondingListener| call in
+ // |Shutdown| since the inner window may not exist at that moment.
+ uint64_t mWindowId;
+
+ nsCOMPtr<nsPIDOMWindowInner> mOwner;
+ nsString mUrl;
+ RefPtr<Promise> mGetConnectionListPromise;
+ RefPtr<PresentationConnectionList> mConnectionList;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationReceiver_h
diff --git a/dom/presentation/PresentationRequest.cpp b/dom/presentation/PresentationRequest.cpp
new file mode 100644
index 0000000000..6a7595a3bd
--- /dev/null
+++ b/dom/presentation/PresentationRequest.cpp
@@ -0,0 +1,530 @@
+/* -*- 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 "PresentationRequest.h"
+
+#include <utility>
+
+#include "AvailabilityCollection.h"
+#include "ControllerConnectionCollection.h"
+#include "Presentation.h"
+#include "PresentationAvailability.h"
+#include "PresentationCallbacks.h"
+#include "PresentationLog.h"
+#include "PresentationTransportBuilderConstructor.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/PresentationConnectionAvailableEvent.h"
+#include "mozilla/dom/PresentationRequestBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentSecurityManager.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGlobalWindow.h"
+#include "nsIPresentationService.h"
+#include "nsIURI.h"
+#include "nsIUUIDGenerator.h"
+#include "nsNetUtil.h"
+#include "nsSandboxFlags.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ADDREF_INHERITED(PresentationRequest, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationRequest, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationRequest)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+static nsresult GetAbsoluteURL(const nsAString& aUrl, nsIURI* aBaseUri,
+ Document* aDocument, nsAString& aAbsoluteUrl) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv;
+ if (aDocument) {
+ rv = NS_NewURI(getter_AddRefs(uri), aUrl,
+ aDocument->GetDocumentCharacterSet(), aBaseUri);
+ } else {
+ rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, aBaseUri);
+ }
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+
+ CopyUTF8toUTF16(spec, aAbsoluteUrl);
+
+ return NS_OK;
+}
+
+/* static */
+already_AddRefed<PresentationRequest> PresentationRequest::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aUrl, ErrorResult& aRv) {
+ Sequence<nsString> urls;
+ if (!urls.AppendElement(aUrl, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+ return Constructor(aGlobal, urls, aRv);
+}
+
+/* static */
+already_AddRefed<PresentationRequest> PresentationRequest::Constructor(
+ const GlobalObject& aGlobal, const Sequence<nsString>& aUrls,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ if (aUrls.IsEmpty()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+
+ // Resolve relative URL to absolute URL
+ nsCOMPtr<nsIURI> baseUri = window->GetDocBaseURI();
+ nsTArray<nsString> urls;
+ for (const auto& url : aUrls) {
+ nsAutoString absoluteUrl;
+ nsresult rv =
+ GetAbsoluteURL(url, baseUri, window->GetExtantDoc(), absoluteUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+ return nullptr;
+ }
+
+ urls.AppendElement(absoluteUrl);
+ }
+
+ RefPtr<PresentationRequest> request =
+ new PresentationRequest(window, std::move(urls));
+ return NS_WARN_IF(!request->Init()) ? nullptr : request.forget();
+}
+
+PresentationRequest::PresentationRequest(nsPIDOMWindowInner* aWindow,
+ nsTArray<nsString>&& aUrls)
+ : DOMEventTargetHelper(aWindow), mUrls(std::move(aUrls)) {}
+
+PresentationRequest::~PresentationRequest() = default;
+
+bool PresentationRequest::Init() { return true; }
+
+/* virtual */
+JSObject* PresentationRequest::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PresentationRequest_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<Promise> PresentationRequest::Start(ErrorResult& aRv) {
+ return StartWithDevice(VoidString(), aRv);
+}
+
+already_AddRefed<Promise> PresentationRequest::StartWithDevice(
+ const nsAString& aDeviceId, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ // Get the origin.
+ nsAutoString origin;
+ nsresult rv = nsContentUtils::GetUTFOrigin(global->PrincipalOrNull(), origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = GetOwner()->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (IsProhibitMixedSecurityContexts(doc) && !IsAllURLAuthenticated()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ RefPtr<Navigator> navigator =
+ nsGlobalWindowInner::Cast(GetOwner())->Navigator();
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<Presentation> presentation = navigator->GetPresentation(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (presentation->IsStartSessionUnsettled()) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ // Generate a session ID.
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1");
+ if (NS_WARN_IF(!uuidgen)) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ nsID uuid;
+ uuidgen->GenerateUUIDInPlace(&uuid);
+ char buffer[NSID_LENGTH];
+ uuid.ToProvidedString(buffer);
+ nsAutoString id;
+ CopyASCIItoUTF16(Span(buffer, NSID_LENGTH - 1), id);
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ presentation->SetStartSessionUnsettled(true);
+
+ // Get xul:browser element in parent process or nsWindowRoot object in child
+ // process. If it's in child process, the corresponding xul:browser element
+ // will be obtained at PresentationRequestParent::DoRequest in its parent
+ // process.
+ nsCOMPtr<EventTarget> handler = GetOwner()->GetChromeEventHandler();
+ nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+ nsCOMPtr<nsIPresentationServiceCallback> callback =
+ new PresentationRequesterCallback(this, id, promise);
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> constructor =
+ PresentationTransportBuilderConstructor::Create();
+ rv = service->StartSession(mUrls, id, origin, aDeviceId,
+ GetOwner()->WindowID(), handler, principal,
+ callback, constructor);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ NotifyPromiseSettled();
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> PresentationRequest::Reconnect(
+ const nsAString& aPresentationId, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = GetOwner()->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (IsProhibitMixedSecurityContexts(doc) && !IsAllURLAuthenticated()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ nsString presentationId = nsString(aPresentationId);
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<nsString, RefPtr<Promise>>(
+ "dom::PresentationRequest::FindOrCreatePresentationConnection", this,
+ &PresentationRequest::FindOrCreatePresentationConnection, presentationId,
+ promise);
+
+ if (NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(r)))) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ return promise.forget();
+}
+
+void PresentationRequest::FindOrCreatePresentationConnection(
+ const nsAString& aPresentationId, Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+
+ if (NS_WARN_IF(!GetOwner())) {
+ aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<PresentationConnection> connection =
+ ControllerConnectionCollection::GetSingleton()->FindConnection(
+ GetOwner()->WindowID(), aPresentationId,
+ nsIPresentationService::ROLE_CONTROLLER);
+
+ if (connection) {
+ nsAutoString url;
+ connection->GetUrl(url);
+ if (mUrls.Contains(url)) {
+ switch (connection->State()) {
+ case PresentationConnectionState::Closed:
+ // We found the matched connection.
+ break;
+ case PresentationConnectionState::Connecting:
+ case PresentationConnectionState::Connected:
+ aPromise->MaybeResolve(connection);
+ return;
+ case PresentationConnectionState::Terminated:
+ // A terminated connection cannot be reused.
+ connection = nullptr;
+ break;
+ default:
+ MOZ_CRASH("Unknown presentation session state.");
+ return;
+ }
+ } else {
+ connection = nullptr;
+ }
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPresentationServiceCallback> callback =
+ new PresentationReconnectCallback(this, aPresentationId, aPromise,
+ connection);
+
+ nsresult rv = service->ReconnectSession(
+ mUrls, aPresentationId, nsIPresentationService::ROLE_CONTROLLER,
+ callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ }
+}
+
+already_AddRefed<Promise> PresentationRequest::GetAvailability(
+ ErrorResult& aRv) {
+ PRES_DEBUG("%s\n", __func__);
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = GetOwner()->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (IsProhibitMixedSecurityContexts(doc) && !IsAllURLAuthenticated()) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ FindOrCreatePresentationAvailability(promise);
+
+ return promise.forget();
+}
+
+void PresentationRequest::FindOrCreatePresentationAvailability(
+ RefPtr<Promise>& aPromise) {
+ MOZ_ASSERT(aPromise);
+
+ if (NS_WARN_IF(!GetOwner())) {
+ aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ AvailabilityCollection* collection = AvailabilityCollection::GetSingleton();
+ if (NS_WARN_IF(!collection)) {
+ aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<PresentationAvailability> availability =
+ collection->Find(GetOwner()->WindowID(), mUrls);
+
+ if (!availability) {
+ availability =
+ PresentationAvailability::Create(GetOwner(), mUrls, aPromise);
+ } else {
+ PRES_DEBUG(">resolve with same object\n");
+
+ // Fetching cached available devices is asynchronous in our implementation,
+ // we need to ensure the promise is resolved in order.
+ if (availability->IsCachedValueReady()) {
+ aPromise->MaybeResolve(availability);
+ return;
+ }
+
+ availability->EnqueuePromise(aPromise);
+ }
+
+ if (!availability) {
+ aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return;
+ }
+}
+
+nsresult PresentationRequest::DispatchConnectionAvailableEvent(
+ PresentationConnection* aConnection) {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_OK;
+ }
+
+ PresentationConnectionAvailableEventInit init;
+ init.mConnection = aConnection;
+
+ RefPtr<PresentationConnectionAvailableEvent> event =
+ PresentationConnectionAvailableEvent::Constructor(
+ this, u"connectionavailable"_ns, init);
+ if (NS_WARN_IF(!event)) {
+ return NS_ERROR_FAILURE;
+ }
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+void PresentationRequest::NotifyPromiseSettled() {
+ PRES_DEBUG("%s\n", __func__);
+
+ if (!GetOwner()) {
+ return;
+ }
+
+ RefPtr<Navigator> navigator =
+ nsGlobalWindowInner::Cast(GetOwner())->Navigator();
+ if (!navigator) {
+ return;
+ }
+
+ ErrorResult rv;
+ RefPtr<Presentation> presentation = navigator->GetPresentation(rv);
+
+ if (presentation) {
+ presentation->SetStartSessionUnsettled(false);
+ }
+}
+
+bool PresentationRequest::IsProhibitMixedSecurityContexts(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+
+ if (nsContentUtils::IsChromeDoc(aDocument)) {
+ return true;
+ }
+
+ nsCOMPtr<Document> doc = aDocument;
+ while (doc && !nsContentUtils::IsChromeDoc(doc)) {
+ if (nsContentUtils::HttpsStateIsModern(doc)) {
+ return true;
+ }
+
+ doc = doc->GetInProcessParentDocument();
+ }
+
+ return false;
+}
+
+bool PresentationRequest::IsPrioriAuthenticatedURL(const nsAString& aUrl) {
+ nsCOMPtr<nsIURI> uri;
+ if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aUrl))) {
+ return false;
+ }
+
+ nsAutoCString scheme;
+ nsresult rv = uri->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ if (scheme.EqualsLiteral("data")) {
+ return true;
+ }
+
+ nsAutoCString uriSpec;
+ rv = uri->GetSpec(uriSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ if (uriSpec.EqualsLiteral("about:blank") ||
+ uriSpec.EqualsLiteral("about:srcdoc")) {
+ return true;
+ }
+
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+ if (NS_WARN_IF(!principal)) {
+ return false;
+ }
+
+ return principal->GetIsOriginPotentiallyTrustworthy();
+}
+
+bool PresentationRequest::IsAllURLAuthenticated() {
+ for (const auto& url : mUrls) {
+ if (!IsPrioriAuthenticatedURL(url)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationRequest.h b/dom/presentation/PresentationRequest.h
new file mode 100644
index 0000000000..26c6031bcd
--- /dev/null
+++ b/dom/presentation/PresentationRequest.h
@@ -0,0 +1,80 @@
+/* -*- 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_PresentationRequest_h
+#define mozilla_dom_PresentationRequest_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/DOMEventTargetHelper.h"
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+class PresentationAvailability;
+class PresentationConnection;
+
+class PresentationRequest final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ static already_AddRefed<PresentationRequest> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aUrl, ErrorResult& aRv);
+
+ static already_AddRefed<PresentationRequest> Constructor(
+ const GlobalObject& aGlobal, const Sequence<nsString>& aUrls,
+ ErrorResult& aRv);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL (public APIs)
+ already_AddRefed<Promise> Start(ErrorResult& aRv);
+
+ already_AddRefed<Promise> StartWithDevice(const nsAString& aDeviceId,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> Reconnect(const nsAString& aPresentationId,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetAvailability(ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(connectionavailable);
+
+ nsresult DispatchConnectionAvailableEvent(
+ PresentationConnection* aConnection);
+
+ void NotifyPromiseSettled();
+
+ private:
+ PresentationRequest(nsPIDOMWindowInner* aWindow, nsTArray<nsString>&& aUrls);
+
+ ~PresentationRequest();
+
+ bool Init();
+
+ void FindOrCreatePresentationConnection(const nsAString& aPresentationId,
+ Promise* aPromise);
+
+ void FindOrCreatePresentationAvailability(RefPtr<Promise>& aPromise);
+
+ // Implement
+ // https://w3c.github.io/webappsec-mixed-content/#categorize-settings-object
+ bool IsProhibitMixedSecurityContexts(Document* aDocument);
+
+ // Implement
+ // https://w3c.github.io/webappsec-mixed-content/#a-priori-authenticated-url
+ bool IsPrioriAuthenticatedURL(const nsAString& aUrl);
+
+ bool IsAllURLAuthenticated();
+
+ nsTArray<nsString> mUrls;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationRequest_h
diff --git a/dom/presentation/PresentationService.cpp b/dom/presentation/PresentationService.cpp
new file mode 100644
index 0000000000..2845ad5931
--- /dev/null
+++ b/dom/presentation/PresentationService.cpp
@@ -0,0 +1,1117 @@
+/* -*- 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 "PresentationService.h"
+
+#include "ipc/PresentationIPCService.h"
+#include "mozilla/Services.h"
+#include "nsArrayUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsIMutableArray.h"
+#include "nsIObserverService.h"
+#include "nsIPresentationDeviceManager.h"
+#include "nsIPresentationDevicePrompt.h"
+#include "nsIPresentationListener.h"
+#include "nsIPresentationRequestUIGlue.h"
+#include "nsIPresentationSessionRequest.h"
+#include "nsIPresentationTerminateRequest.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOMCID.h"
+#include "nsXULAppAPI.h"
+#include "PresentationLog.h"
+
+namespace mozilla {
+namespace dom {
+
+static bool IsSameDevice(nsIPresentationDevice* aDevice,
+ nsIPresentationDevice* aDeviceAnother) {
+ if (!aDevice || !aDeviceAnother) {
+ return false;
+ }
+
+ nsAutoCString deviceId;
+ aDevice->GetId(deviceId);
+ nsAutoCString anotherId;
+ aDeviceAnother->GetId(anotherId);
+ if (!deviceId.Equals(anotherId)) {
+ return false;
+ }
+
+ nsAutoCString deviceType;
+ aDevice->GetType(deviceType);
+ nsAutoCString anotherType;
+ aDeviceAnother->GetType(anotherType);
+ if (!deviceType.Equals(anotherType)) {
+ return false;
+ }
+
+ return true;
+}
+
+static nsresult ConvertURLArrayHelper(const nsTArray<nsString>& aUrls,
+ nsIArray** aResult) {
+ if (!aResult) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ *aResult = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIMutableArray> urls = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ for (const auto& url : aUrls) {
+ nsCOMPtr<nsISupportsString> isupportsString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = isupportsString->SetData(url);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = urls->AppendElement(isupportsString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ urls.forget(aResult);
+ return NS_OK;
+}
+
+/*
+ * Implementation of PresentationDeviceRequest
+ */
+
+class PresentationDeviceRequest final : public nsIPresentationDeviceRequest {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICEREQUEST
+
+ PresentationDeviceRequest(
+ const nsTArray<nsString>& aUrls, const nsAString& aId,
+ const nsAString& aOrigin, uint64_t aWindowId, EventTarget* aEventTarget,
+ nsIPrincipal* aPrincipal, nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor);
+
+ private:
+ virtual ~PresentationDeviceRequest() = default;
+ nsresult CreateSessionInfo(nsIPresentationDevice* aDevice,
+ const nsAString& aSelectedRequestUrl);
+
+ nsTArray<nsString> mRequestUrls;
+ nsString mId;
+ nsString mOrigin;
+ uint64_t mWindowId;
+ nsWeakPtr mChromeEventHandler;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCOMPtr<nsIPresentationServiceCallback> mCallback;
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> mBuilderConstructor;
+};
+
+LazyLogModule gPresentationLog("Presentation");
+
+NS_IMPL_ISUPPORTS(PresentationDeviceRequest, nsIPresentationDeviceRequest)
+
+PresentationDeviceRequest::PresentationDeviceRequest(
+ const nsTArray<nsString>& aUrls, const nsAString& aId,
+ const nsAString& aOrigin, uint64_t aWindowId, EventTarget* aEventTarget,
+ nsIPrincipal* aPrincipal, nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor)
+ : mRequestUrls(aUrls.Clone()),
+ mId(aId),
+ mOrigin(aOrigin),
+ mWindowId(aWindowId),
+ mChromeEventHandler(do_GetWeakReference(aEventTarget)),
+ mPrincipal(aPrincipal),
+ mCallback(aCallback),
+ mBuilderConstructor(aBuilderConstructor) {
+ MOZ_ASSERT(!mRequestUrls.IsEmpty());
+ MOZ_ASSERT(!mId.IsEmpty());
+ MOZ_ASSERT(!mOrigin.IsEmpty());
+ MOZ_ASSERT(mCallback);
+ MOZ_ASSERT(mBuilderConstructor);
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::GetOrigin(nsAString& aOrigin) {
+ aOrigin = mOrigin;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::GetRequestURLs(nsIArray** aUrls) {
+ return ConvertURLArrayHelper(mRequestUrls, aUrls);
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::GetChromeEventHandler(
+ EventTarget** aChromeEventHandler) {
+ RefPtr<EventTarget> handler(do_QueryReferent(mChromeEventHandler));
+ handler.forget(aChromeEventHandler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::GetPrincipal(nsIPrincipal** aPrincipal) {
+ nsCOMPtr<nsIPrincipal> principal(mPrincipal);
+ principal.forget(aPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::Select(nsIPresentationDevice* aDevice) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!aDevice)) {
+ MOZ_ASSERT(false, "|aDevice| should noe be null.");
+ mCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Select the most suitable URL for starting the presentation.
+ nsAutoString selectedRequestUrl;
+ for (const auto& url : mRequestUrls) {
+ bool isSupported;
+ if (NS_SUCCEEDED(aDevice->IsRequestedUrlSupported(url, &isSupported)) &&
+ isSupported) {
+ selectedRequestUrl.Assign(url);
+ break;
+ }
+ }
+
+ if (selectedRequestUrl.IsEmpty()) {
+ return mCallback->NotifyError(NS_ERROR_DOM_NOT_FOUND_ERR);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(CreateSessionInfo(aDevice, selectedRequestUrl)))) {
+ return mCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ return mCallback->NotifySuccess(selectedRequestUrl);
+}
+
+nsresult PresentationDeviceRequest::CreateSessionInfo(
+ nsIPresentationDevice* aDevice, const nsAString& aSelectedRequestUrl) {
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Create the controlling session info
+ RefPtr<PresentationSessionInfo> info =
+ static_cast<PresentationService*>(service.get())
+ ->CreateControllingSessionInfo(aSelectedRequestUrl, mId, mWindowId);
+ 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(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);
+ }
+
+ info->SetTransportBuilderConstructor(mBuilderConstructor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationDeviceRequest::Cancel(nsresult aReason) {
+ return mCallback->NotifyError(aReason);
+}
+
+/*
+ * Implementation of PresentationService
+ */
+
+NS_IMPL_ISUPPORTS(PresentationService, nsIPresentationService, nsIObserver)
+
+PresentationService::PresentationService() = default;
+
+PresentationService::~PresentationService() { HandleShutdown(); }
+
+bool PresentationService::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return false;
+ }
+
+ nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ rv = obs->AddObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ rv = obs->AddObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ rv = obs->AddObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ rv = obs->AddObserver(this, PRESENTATION_RECONNECT_REQUEST_TOPIC, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return !NS_WARN_IF(NS_FAILED(rv));
+}
+
+NS_IMETHODIMP
+PresentationService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ HandleShutdown();
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, PRESENTATION_DEVICE_CHANGE_TOPIC)) {
+ // Ignore the "update" case here, since we only care about the arrival and
+ // removal of the device.
+ if (!NS_strcmp(aData, u"add")) {
+ nsCOMPtr<nsIPresentationDevice> device = do_QueryInterface(aSubject);
+ if (NS_WARN_IF(!device)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ HandleDeviceAdded(device);
+ return NS_OK;
+ }
+
+ if (!NS_strcmp(aData, u"remove")) {
+ return HandleDeviceRemoved();
+ }
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, PRESENTATION_SESSION_REQUEST_TOPIC)) {
+ nsCOMPtr<nsIPresentationSessionRequest> request(
+ do_QueryInterface(aSubject));
+ if (NS_WARN_IF(!request)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return HandleSessionRequest(request);
+ }
+ if (!strcmp(aTopic, PRESENTATION_TERMINATE_REQUEST_TOPIC)) {
+ nsCOMPtr<nsIPresentationTerminateRequest> request(
+ do_QueryInterface(aSubject));
+ if (NS_WARN_IF(!request)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return HandleTerminateRequest(request);
+ }
+ if (!strcmp(aTopic, PRESENTATION_RECONNECT_REQUEST_TOPIC)) {
+ nsCOMPtr<nsIPresentationSessionRequest> request(
+ do_QueryInterface(aSubject));
+ if (NS_WARN_IF(!request)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return HandleReconnectRequest(request);
+ }
+ if (!strcmp(aTopic, "profile-after-change")) {
+ // It's expected since we add and entry to |kLayoutCategories| in
+ // |nsLayoutModule.cpp| to launch this service earlier.
+ return NS_OK;
+ }
+ MOZ_ASSERT(false, "Unexpected topic for PresentationService");
+ return NS_ERROR_UNEXPECTED;
+}
+
+void PresentationService::HandleShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Shutdown();
+
+ mAvailabilityManager.Clear();
+ mSessionInfoAtController.Clear();
+ mSessionInfoAtReceiver.Clear();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ obs->RemoveObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC);
+ obs->RemoveObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC);
+ obs->RemoveObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC);
+ obs->RemoveObserver(this, PRESENTATION_RECONNECT_REQUEST_TOPIC);
+ }
+}
+
+void PresentationService::HandleDeviceAdded(nsIPresentationDevice* aDevice) {
+ PRES_DEBUG("%s\n", __func__);
+ MOZ_ASSERT(aDevice);
+
+ // Query for only unavailable URLs while device added.
+ nsTArray<nsString> unavailableUrls;
+ mAvailabilityManager.GetAvailbilityUrlByAvailability(unavailableUrls, false);
+
+ nsTArray<nsString> supportedAvailabilityUrl;
+ for (const auto& url : unavailableUrls) {
+ bool isSupported;
+ if (NS_SUCCEEDED(aDevice->IsRequestedUrlSupported(url, &isSupported)) &&
+ isSupported) {
+ supportedAvailabilityUrl.AppendElement(url);
+ }
+ }
+
+ if (!supportedAvailabilityUrl.IsEmpty()) {
+ mAvailabilityManager.DoNotifyAvailableChange(supportedAvailabilityUrl,
+ true);
+ }
+}
+
+nsresult PresentationService::HandleDeviceRemoved() {
+ PRES_DEBUG("%s\n", __func__);
+
+ // Query for only available URLs while device removed.
+ nsTArray<nsString> availabilityUrls;
+ mAvailabilityManager.GetAvailbilityUrlByAvailability(availabilityUrls, true);
+
+ return UpdateAvailabilityUrlChange(availabilityUrls);
+}
+
+nsresult PresentationService::UpdateAvailabilityUrlChange(
+ const nsTArray<nsString>& aAvailabilityUrls) {
+ nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
+ do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
+ if (NS_WARN_IF(!deviceManager)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIArray> devices;
+ nsresult rv =
+ deviceManager->GetAvailableDevices(nullptr, getter_AddRefs(devices));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint32_t numOfDevices;
+ devices->GetLength(&numOfDevices);
+
+ nsTArray<nsString> supportedAvailabilityUrl;
+ for (const auto& url : aAvailabilityUrls) {
+ for (uint32_t i = 0; i < numOfDevices; ++i) {
+ nsCOMPtr<nsIPresentationDevice> device = do_QueryElementAt(devices, i);
+ if (device) {
+ bool isSupported;
+ if (NS_SUCCEEDED(device->IsRequestedUrlSupported(url, &isSupported)) &&
+ isSupported) {
+ supportedAvailabilityUrl.AppendElement(url);
+ break;
+ }
+ }
+ }
+ }
+
+ if (supportedAvailabilityUrl.IsEmpty()) {
+ mAvailabilityManager.DoNotifyAvailableChange(aAvailabilityUrls, false);
+ } else {
+ mAvailabilityManager.DoNotifyAvailableChange(supportedAvailabilityUrl,
+ true);
+ }
+ return NS_OK;
+}
+
+nsresult PresentationService::HandleSessionRequest(
+ nsIPresentationSessionRequest* aRequest) {
+ nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+ 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->Disconnect(rv);
+ return rv;
+ }
+
+ nsAutoString sessionId;
+ rv = aRequest->GetPresentationId(sessionId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ nsCOMPtr<nsIPresentationDevice> device;
+ rv = aRequest->GetDevice(getter_AddRefs(device));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ // Create or reuse session info.
+ RefPtr<PresentationSessionInfo> info =
+ GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
+
+ // This is the case for reconnecting a session.
+ // Update the control channel and device of the session info.
+ // Call |NotifyResponderReady| to indicate the receiver page is already there.
+ if (info) {
+ PRES_DEBUG("handle reconnection:id[%s]\n",
+ NS_ConvertUTF16toUTF8(sessionId).get());
+
+ info->SetControlChannel(ctrlChannel);
+ info->SetDevice(device);
+ return static_cast<PresentationPresentingInfo*>(info.get())->DoReconnect();
+ }
+
+ // This is the case for a new session.
+ PRES_DEBUG("handle new session:url[%s], id[%s]\n",
+ NS_ConvertUTF16toUTF8(url).get(),
+ NS_ConvertUTF16toUTF8(sessionId).get());
+
+ info = new PresentationPresentingInfo(url, sessionId, device);
+ rv = info->Init(ctrlChannel);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ mSessionInfoAtReceiver.Put(sessionId, RefPtr{info});
+
+ // Notify the receiver to launch.
+ nsCOMPtr<nsIPresentationRequestUIGlue> glue =
+ do_CreateInstance(PRESENTATION_REQUEST_UI_GLUE_CONTRACTID);
+ if (NS_WARN_IF(!glue)) {
+ ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+ return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ RefPtr<Promise> promise;
+ rv = glue->SendRequest(url, sessionId, device, getter_AddRefs(promise));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(promise);
+
+ return NS_OK;
+}
+
+nsresult PresentationService::HandleTerminateRequest(
+ nsIPresentationTerminateRequest* aRequest) {
+ nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+ nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel));
+ if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) {
+ return rv;
+ }
+
+ nsAutoString sessionId;
+ rv = aRequest->GetPresentationId(sessionId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ nsCOMPtr<nsIPresentationDevice> device;
+ rv = aRequest->GetDevice(getter_AddRefs(device));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ bool isFromReceiver;
+ rv = aRequest->GetIsFromReceiver(&isFromReceiver);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ RefPtr<PresentationSessionInfo> info;
+ if (!isFromReceiver) {
+ info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
+ } else {
+ info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_CONTROLLER);
+ }
+ if (NS_WARN_IF(!info)) {
+ // Cannot terminate non-existed session.
+ ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+ return NS_ERROR_DOM_ABORT_ERR;
+ }
+
+ // Check if terminate request comes from known device.
+ RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
+ if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {
+ ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+ return NS_ERROR_DOM_ABORT_ERR;
+ }
+
+ PRES_DEBUG("%s:handle termination:id[%s], receiver[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(sessionId).get(), isFromReceiver);
+
+ info->OnTerminate(ctrlChannel);
+ return NS_OK;
+}
+
+nsresult PresentationService::HandleReconnectRequest(
+ nsIPresentationSessionRequest* aRequest) {
+ nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+ nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel));
+ if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) {
+ return rv;
+ }
+
+ nsAutoString sessionId;
+ rv = aRequest->GetPresentationId(sessionId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ uint64_t windowId;
+ rv = GetWindowIdBySessionIdInternal(
+ sessionId, nsIPresentationService::ROLE_RECEIVER, &windowId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ RefPtr<PresentationSessionInfo> info =
+ GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
+ if (NS_WARN_IF(!info)) {
+ // Cannot reconnect non-existed session
+ ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+ return NS_ERROR_DOM_ABORT_ERR;
+ }
+
+ nsAutoString url;
+ rv = aRequest->GetUrl(url);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ // Make sure the url is the same as the previous one.
+ if (NS_WARN_IF(!info->GetUrl().Equals(url))) {
+ ctrlChannel->Disconnect(rv);
+ return rv;
+ }
+
+ return HandleSessionRequest(aRequest);
+}
+
+NS_IMETHODIMP
+PresentationService::StartSession(
+ const nsTArray<nsString>& aUrls, const nsAString& aSessionId,
+ const nsAString& aOrigin, const nsAString& aDeviceId, uint64_t aWindowId,
+ EventTarget* aEventTarget, nsIPrincipal* aPrincipal,
+ nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor) {
+ PRES_DEBUG("%s:id[%s]\n", __func__, NS_ConvertUTF16toUTF8(aSessionId).get());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(!aUrls.IsEmpty());
+
+ nsCOMPtr<nsIPresentationDeviceRequest> request =
+ new PresentationDeviceRequest(aUrls, aSessionId, aOrigin, aWindowId,
+ aEventTarget, aPrincipal, aCallback,
+ aBuilderConstructor);
+
+ if (aDeviceId.IsVoid()) {
+ // Pop up a prompt and ask user to select a device.
+ nsCOMPtr<nsIPresentationDevicePrompt> prompt =
+ do_GetService(PRESENTATION_DEVICE_PROMPT_CONTRACTID);
+ if (NS_WARN_IF(!prompt)) {
+ return aCallback->NotifyError(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+ }
+
+ nsresult rv = prompt->PromptDeviceSelection(request);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return aCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ return NS_OK;
+ }
+
+ // Find the designated device from available device list.
+ nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
+ do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
+ if (NS_WARN_IF(!deviceManager)) {
+ return aCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ nsCOMPtr<nsIArray> presentationUrls;
+ if (NS_WARN_IF(NS_FAILED(
+ ConvertURLArrayHelper(aUrls, getter_AddRefs(presentationUrls))))) {
+ return aCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ nsCOMPtr<nsIArray> devices;
+ nsresult rv = deviceManager->GetAvailableDevices(presentationUrls,
+ getter_AddRefs(devices));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return aCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ nsCOMPtr<nsISimpleEnumerator> enumerator;
+ rv = devices->Enumerate(getter_AddRefs(enumerator));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return aCallback->NotifyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ NS_ConvertUTF16toUTF8 utf8DeviceId(aDeviceId);
+ bool hasMore;
+ while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> isupports;
+ rv = enumerator->GetNext(getter_AddRefs(isupports));
+
+ nsCOMPtr<nsIPresentationDevice> device(do_QueryInterface(isupports));
+ MOZ_ASSERT(device);
+
+ nsAutoCString id;
+ if (NS_SUCCEEDED(device->GetId(id)) && id.Equals(utf8DeviceId)) {
+ request->Select(device);
+ return NS_OK;
+ }
+ }
+
+ // Reject if designated device is not available.
+ return aCallback->NotifyError(NS_ERROR_DOM_NOT_FOUND_ERR);
+}
+
+already_AddRefed<PresentationSessionInfo>
+PresentationService::CreateControllingSessionInfo(const nsAString& aUrl,
+ const nsAString& aSessionId,
+ uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSessionId.IsEmpty()) {
+ return nullptr;
+ }
+
+ RefPtr<PresentationSessionInfo> info =
+ new PresentationControllingInfo(aUrl, aSessionId);
+
+ mSessionInfoAtController.Put(aSessionId, RefPtr{info});
+ AddRespondingSessionId(aWindowId, aSessionId,
+ nsIPresentationService::ROLE_CONTROLLER);
+ return info.forget();
+}
+
+NS_IMETHODIMP
+PresentationService::SendSessionMessage(const nsAString& aSessionId,
+ uint8_t aRole, const nsAString& aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aData.IsEmpty());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->Send(aData);
+}
+
+NS_IMETHODIMP
+PresentationService::SendSessionBinaryMsg(const nsAString& aSessionId,
+ uint8_t aRole,
+ const nsACString& aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aData.IsEmpty());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->SendBinaryMsg(aData);
+}
+
+NS_IMETHODIMP
+PresentationService::SendSessionBlob(const nsAString& aSessionId, uint8_t aRole,
+ Blob* aBlob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ MOZ_ASSERT(aBlob);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->SendBlob(aBlob);
+}
+
+NS_IMETHODIMP
+PresentationService::CloseSession(const nsAString& aSessionId, uint8_t aRole,
+ uint8_t aClosedReason) {
+ PRES_DEBUG("%s:id[%s], reason[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aClosedReason, aRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aClosedReason == nsIPresentationService::CLOSED_REASON_WENTAWAY) {
+ // Remove nsIPresentationSessionListener since we don't want to dispatch
+ // PresentationConnectionCloseEvent if the page is went away.
+ info->SetListener(nullptr);
+ }
+
+ return info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED);
+}
+
+NS_IMETHODIMP
+PresentationService::TerminateSession(const nsAString& aSessionId,
+ uint8_t aRole) {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->Close(NS_OK, nsIPresentationSessionListener::STATE_TERMINATED);
+}
+
+NS_IMETHODIMP
+PresentationService::ReconnectSession(
+ const nsTArray<nsString>& aUrls, const nsAString& aSessionId, uint8_t aRole,
+ nsIPresentationServiceCallback* aCallback) {
+ PRES_DEBUG("%s:id[%s]\n", __func__, NS_ConvertUTF16toUTF8(aSessionId).get());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!aUrls.IsEmpty());
+
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "Only controller can call ReconnectSession.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aCallback)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return aCallback->NotifyError(NS_ERROR_DOM_NOT_FOUND_ERR);
+ }
+
+ if (NS_WARN_IF(!aUrls.Contains(info->GetUrl()))) {
+ return aCallback->NotifyError(NS_ERROR_DOM_NOT_FOUND_ERR);
+ }
+
+ return static_cast<PresentationControllingInfo*>(info.get())
+ ->Reconnect(aCallback);
+}
+
+NS_IMETHODIMP
+PresentationService::BuildTransport(const nsAString& aSessionId,
+ uint8_t aRole) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "Only controller can call BuildTransport.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return static_cast<PresentationControllingInfo*>(info.get())
+ ->BuildTransport();
+}
+
+NS_IMETHODIMP
+PresentationService::RegisterAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aAvailabilityUrls.IsEmpty());
+ MOZ_ASSERT(aListener);
+
+ mAvailabilityManager.AddAvailabilityListener(aAvailabilityUrls, aListener);
+ return UpdateAvailabilityUrlChange(aAvailabilityUrls);
+}
+
+NS_IMETHODIMP
+PresentationService::UnregisterAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mAvailabilityManager.RemoveAvailabilityListener(aAvailabilityUrls, aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::RegisterSessionListener(
+ const nsAString& aSessionId, uint8_t aRole,
+ nsIPresentationSessionListener* aListener) {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ // Notify the listener of TERMINATED since no correspondent session info is
+ // available possibly due to establishment failure. This would be useful at
+ // the receiver side, since a presentation session is created at beginning
+ // and here is the place to realize the underlying establishment fails.
+ nsresult rv = aListener->NotifyStateChange(
+ aSessionId, nsIPresentationSessionListener::STATE_TERMINATED,
+ NS_ERROR_NOT_AVAILABLE);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->SetListener(aListener);
+}
+
+NS_IMETHODIMP
+PresentationService::UnregisterSessionListener(const nsAString& aSessionId,
+ uint8_t aRole) {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (info) {
+ // When content side decide not handling this session anymore, simply
+ // close the connection. Session info is kept for reconnection.
+ Unused << NS_WARN_IF(NS_FAILED(
+ info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED)));
+ return info->SetListener(nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::RegisterRespondingListener(
+ uint64_t aWindowId, nsIPresentationRespondingListener* aListener) {
+ PRES_DEBUG("%s:windowId[%" PRIu64 "]\n", __func__, aWindowId);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+
+ nsCOMPtr<nsIPresentationRespondingListener> listener;
+ if (mRespondingListeners.Get(aWindowId, getter_AddRefs(listener))) {
+ return (listener == aListener) ? NS_OK : NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ nsTArray<nsString> sessionIdArray;
+ nsresult rv =
+ mReceiverSessionIdManager.GetSessionIds(aWindowId, sessionIdArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ for (const auto& id : sessionIdArray) {
+ aListener->NotifySessionConnect(aWindowId, id);
+ }
+
+ mRespondingListeners.Put(aWindowId, RefPtr{aListener});
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::UnregisterRespondingListener(uint64_t aWindowId) {
+ PRES_DEBUG("%s:windowId[%" PRIu64 "]\n", __func__, aWindowId);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRespondingListeners.Remove(aWindowId);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::NotifyReceiverReady(
+ const nsAString& aSessionId, uint64_t aWindowId, bool aIsLoading,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor) {
+ PRES_DEBUG("%s:id[%s], windowId[%" PRIu64 "], loading[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aWindowId, aIsLoading);
+
+ RefPtr<PresentationSessionInfo> info =
+ GetSessionInfo(aSessionId, nsIPresentationService::ROLE_RECEIVER);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ AddRespondingSessionId(aWindowId, aSessionId,
+ nsIPresentationService::ROLE_RECEIVER);
+
+ if (!aIsLoading) {
+ return static_cast<PresentationPresentingInfo*>(info.get())
+ ->NotifyResponderFailure();
+ }
+
+ nsCOMPtr<nsIPresentationRespondingListener> listener;
+ if (mRespondingListeners.Get(aWindowId, getter_AddRefs(listener))) {
+ nsresult rv = listener->NotifySessionConnect(aWindowId, aSessionId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ info->SetTransportBuilderConstructor(aBuilderConstructor);
+ return static_cast<PresentationPresentingInfo*>(info.get())
+ ->NotifyResponderReady();
+}
+
+nsresult PresentationService::NotifyTransportClosed(const nsAString& aSessionId,
+ uint8_t aRole,
+ nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(),
+ static_cast<uint32_t>(aReason), aRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->NotifyTransportClosed(aReason);
+}
+
+NS_IMETHODIMP
+PresentationService::UntrackSessionInfo(const nsAString& aSessionId,
+ uint8_t aRole) {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aRole);
+
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ // Remove the session info.
+ if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
+ mSessionInfoAtController.Remove(aSessionId);
+ } else {
+ // Terminate receiver page.
+ uint64_t windowId;
+ nsresult rv = GetWindowIdBySessionIdInternal(aSessionId, aRole, &windowId);
+ if (NS_SUCCEEDED(rv)) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "dom::PresentationService::UntrackSessionInfo", [windowId]() -> void {
+ PRES_DEBUG("Attempt to close window[%" PRIu64 "]\n", windowId);
+
+ if (auto* window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowId)) {
+ window->Close();
+ }
+ }));
+ }
+
+ mSessionInfoAtReceiver.Remove(aSessionId);
+ }
+
+ // Remove the in-process responding info if there's still any.
+ RemoveRespondingSessionId(aSessionId, aRole);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::GetWindowIdBySessionId(const nsAString& aSessionId,
+ uint8_t aRole,
+ uint64_t* aWindowId) {
+ return GetWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+}
+
+NS_IMETHODIMP
+PresentationService::UpdateWindowIdBySessionId(const nsAString& aSessionId,
+ uint8_t aRole,
+ const uint64_t aWindowId) {
+ UpdateWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+ return NS_OK;
+}
+
+bool PresentationService::IsSessionAccessible(const nsAString& aSessionId,
+ const uint8_t aRole,
+ base::ProcessId aProcessId) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return false;
+ }
+ return info->IsAccessible(aProcessId);
+}
+
+} // namespace dom
+} // namespace mozilla
+
+already_AddRefed<nsIPresentationService> NS_CreatePresentationService() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPresentationService> service;
+ if (XRE_GetProcessType() == GeckoProcessType_Content) {
+ service = new mozilla::dom::PresentationIPCService();
+ } else {
+ service = new mozilla::dom::PresentationService();
+ if (NS_WARN_IF(
+ !static_cast<mozilla::dom::PresentationService*>(service.get())
+ ->Init())) {
+ return nullptr;
+ }
+ }
+
+ return service.forget();
+}
diff --git a/dom/presentation/PresentationService.h b/dom/presentation/PresentationService.h
new file mode 100644
index 0000000000..2aadbcb49e
--- /dev/null
+++ b/dom/presentation/PresentationService.h
@@ -0,0 +1,64 @@
+/* -*- 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_PresentationService_h
+#define mozilla_dom_PresentationService_h
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "PresentationServiceBase.h"
+#include "PresentationSessionInfo.h"
+
+class nsIPresentationSessionRequest;
+class nsIPresentationTerminateRequest;
+class nsIURI;
+class nsIPresentationSessionTransportBuilder;
+
+namespace mozilla {
+namespace dom {
+
+class PresentationDeviceRequest;
+class PresentationRespondingInfo;
+
+class PresentationService final
+ : public nsIPresentationService,
+ public nsIObserver,
+ public PresentationServiceBase<PresentationSessionInfo> {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIPRESENTATIONSERVICE
+
+ PresentationService();
+ bool Init();
+
+ bool IsSessionAccessible(const nsAString& aSessionId, const uint8_t aRole,
+ base::ProcessId aProcessId);
+
+ private:
+ friend class PresentationDeviceRequest;
+
+ virtual ~PresentationService();
+ void HandleShutdown();
+ void HandleDeviceAdded(nsIPresentationDevice* aDevice);
+ nsresult HandleDeviceRemoved();
+ nsresult HandleSessionRequest(nsIPresentationSessionRequest* aRequest);
+ nsresult HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest);
+ nsresult HandleReconnectRequest(nsIPresentationSessionRequest* aRequest);
+
+ // This is meant to be called by PresentationDeviceRequest.
+ already_AddRefed<PresentationSessionInfo> CreateControllingSessionInfo(
+ const nsAString& aUrl, const nsAString& aSessionId, uint64_t aWindowId);
+
+ // Emumerate all devices to get the availability of each input Urls.
+ nsresult UpdateAvailabilityUrlChange(
+ const nsTArray<nsString>& aAvailabilityUrls);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationService_h
diff --git a/dom/presentation/PresentationServiceBase.h b/dom/presentation/PresentationServiceBase.h
new file mode 100644
index 0000000000..a997d5db89
--- /dev/null
+++ b/dom/presentation/PresentationServiceBase.h
@@ -0,0 +1,356 @@
+/* -*- 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_PresentationServiceBase_h
+#define mozilla_dom_PresentationServiceBase_h
+
+#include "mozilla/Unused.h"
+#include "nsClassHashtable.h"
+#include "nsCOMArray.h"
+#include "nsIPresentationListener.h"
+#include "nsIPresentationService.h"
+#include "nsRefPtrHashtable.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsDataHashtable.h"
+#include "nsThread.h"
+
+namespace mozilla {
+namespace dom {
+
+template <class T>
+class PresentationServiceBase {
+ public:
+ PresentationServiceBase() = default;
+
+ already_AddRefed<T> GetSessionInfo(const nsAString& aSessionId,
+ const uint8_t aRole) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<T> info;
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ return mSessionInfoAtController.Get(aSessionId, getter_AddRefs(info))
+ ? info.forget()
+ : nullptr;
+ } else {
+ return mSessionInfoAtReceiver.Get(aSessionId, getter_AddRefs(info))
+ ? info.forget()
+ : nullptr;
+ }
+ }
+
+ protected:
+ class SessionIdManager final {
+ public:
+ explicit SessionIdManager() { MOZ_COUNT_CTOR(SessionIdManager); }
+
+ MOZ_COUNTED_DTOR(SessionIdManager)
+
+ nsresult GetWindowId(const nsAString& aSessionId, uint64_t* aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mRespondingWindowIds.Get(aSessionId, aWindowId)) {
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult GetSessionIds(uint64_t aWindowId,
+ nsTArray<nsString>& aSessionIds) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsTArray<nsString>* sessionIdArray;
+ if (!mRespondingSessionIds.Get(aWindowId, &sessionIdArray)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aSessionIds.Assign(*sessionIdArray);
+ return NS_OK;
+ }
+
+ void AddSessionId(uint64_t aWindowId, const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(aWindowId == 0)) {
+ return;
+ }
+
+ nsTArray<nsString>* sessionIdArray;
+ if (!mRespondingSessionIds.Get(aWindowId, &sessionIdArray)) {
+ sessionIdArray = new nsTArray<nsString>();
+ mRespondingSessionIds.Put(aWindowId, sessionIdArray);
+ }
+
+ sessionIdArray->AppendElement(nsString(aSessionId));
+ mRespondingWindowIds.Put(aSessionId, aWindowId);
+ }
+
+ void RemoveSessionId(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ uint64_t windowId = 0;
+ if (mRespondingWindowIds.Get(aSessionId, &windowId)) {
+ mRespondingWindowIds.Remove(aSessionId);
+ nsTArray<nsString>* sessionIdArray;
+ if (mRespondingSessionIds.Get(windowId, &sessionIdArray)) {
+ sessionIdArray->RemoveElement(nsString(aSessionId));
+ if (sessionIdArray->IsEmpty()) {
+ mRespondingSessionIds.Remove(windowId);
+ }
+ }
+ }
+ }
+
+ void UpdateWindowId(const nsAString& aSessionId, const uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RemoveSessionId(aSessionId);
+ AddSessionId(aWindowId, aSessionId);
+ }
+
+ void Clear() {
+ mRespondingSessionIds.Clear();
+ mRespondingWindowIds.Clear();
+ }
+
+ private:
+ nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mRespondingSessionIds;
+ nsDataHashtable<nsStringHashKey, uint64_t> mRespondingWindowIds;
+ };
+
+ class AvailabilityManager final {
+ public:
+ explicit AvailabilityManager() { MOZ_COUNT_CTOR(AvailabilityManager); }
+
+ MOZ_COUNTED_DTOR(AvailabilityManager)
+
+ void AddAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ nsTArray<nsString> dummy;
+ AddAvailabilityListener(aAvailabilityUrls, aListener, dummy);
+ }
+
+ void AddAvailabilityListener(const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener,
+ nsTArray<nsString>& aAddedUrls) {
+ if (!aListener) {
+ MOZ_ASSERT(false, "aListener should not be null.");
+ return;
+ }
+
+ if (aAvailabilityUrls.IsEmpty()) {
+ MOZ_ASSERT(false, "aAvailabilityUrls should not be empty.");
+ return;
+ }
+
+ aAddedUrls.Clear();
+ nsTArray<nsString> knownAvailableUrls;
+ for (const auto& url : aAvailabilityUrls) {
+ AvailabilityEntry* entry;
+ if (!mAvailabilityUrlTable.Get(url, &entry)) {
+ entry = new AvailabilityEntry();
+ mAvailabilityUrlTable.Put(url, entry);
+ aAddedUrls.AppendElement(url);
+ }
+ if (!entry->mListeners.Contains(aListener)) {
+ entry->mListeners.AppendElement(aListener);
+ }
+ if (entry->mAvailable) {
+ knownAvailableUrls.AppendElement(url);
+ }
+ }
+
+ if (!knownAvailableUrls.IsEmpty()) {
+ Unused << NS_WARN_IF(NS_FAILED(
+ aListener->NotifyAvailableChange(knownAvailableUrls, true)));
+ } else {
+ // If we can't find any known available url and there is no newly
+ // added url, we still need to notify the listener of the result.
+ // So, the promise returned by |getAvailability| can be resolved.
+ if (aAddedUrls.IsEmpty()) {
+ Unused << NS_WARN_IF(NS_FAILED(
+ aListener->NotifyAvailableChange(aAvailabilityUrls, false)));
+ }
+ }
+ }
+
+ void RemoveAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ nsTArray<nsString> dummy;
+ RemoveAvailabilityListener(aAvailabilityUrls, aListener, dummy);
+ }
+
+ void RemoveAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener,
+ nsTArray<nsString>& aRemovedUrls) {
+ if (!aListener) {
+ MOZ_ASSERT(false, "aListener should not be null.");
+ return;
+ }
+
+ if (aAvailabilityUrls.IsEmpty()) {
+ MOZ_ASSERT(false, "aAvailabilityUrls should not be empty.");
+ return;
+ }
+
+ aRemovedUrls.Clear();
+ for (const auto& url : aAvailabilityUrls) {
+ AvailabilityEntry* entry;
+ if (mAvailabilityUrlTable.Get(url, &entry)) {
+ entry->mListeners.RemoveElement(aListener);
+ if (entry->mListeners.IsEmpty()) {
+ mAvailabilityUrlTable.Remove(url);
+ aRemovedUrls.AppendElement(url);
+ }
+ }
+ }
+ }
+
+ void DoNotifyAvailableChange(const nsTArray<nsString>& aAvailabilityUrls,
+ bool aAvailable) {
+ typedef nsClassHashtable<nsISupportsHashKey, nsTArray<nsString>>
+ ListenerToUrlsMap;
+ ListenerToUrlsMap availabilityListenerTable;
+ // Create a mapping from nsIPresentationAvailabilityListener to
+ // availabilityUrls.
+ for (auto it = mAvailabilityUrlTable.ConstIter(); !it.Done(); it.Next()) {
+ if (aAvailabilityUrls.Contains(it.Key())) {
+ AvailabilityEntry* entry = it.UserData();
+ entry->mAvailable = aAvailable;
+
+ for (uint32_t i = 0; i < entry->mListeners.Length(); ++i) {
+ nsIPresentationAvailabilityListener* listener =
+ entry->mListeners.ObjectAt(i);
+ nsTArray<nsString>* urlArray;
+ if (!availabilityListenerTable.Get(listener, &urlArray)) {
+ urlArray = new nsTArray<nsString>();
+ availabilityListenerTable.Put(listener, urlArray);
+ }
+ urlArray->AppendElement(it.Key());
+ }
+ }
+ }
+
+ for (auto it = availabilityListenerTable.Iter(); !it.Done(); it.Next()) {
+ auto listener =
+ static_cast<nsIPresentationAvailabilityListener*>(it.Key());
+
+ Unused << NS_WARN_IF(NS_FAILED(
+ listener->NotifyAvailableChange(*it.UserData(), aAvailable)));
+ }
+ }
+
+ void GetAvailbilityUrlByAvailability(nsTArray<nsString>& aOutArray,
+ bool aAvailable) {
+ aOutArray.Clear();
+
+ for (auto it = mAvailabilityUrlTable.ConstIter(); !it.Done(); it.Next()) {
+ if (it.UserData()->mAvailable == aAvailable) {
+ aOutArray.AppendElement(it.Key());
+ }
+ }
+ }
+
+ void Clear() { mAvailabilityUrlTable.Clear(); }
+
+ private:
+ struct AvailabilityEntry {
+ explicit AvailabilityEntry() : mAvailable(false) {}
+
+ bool mAvailable;
+ nsCOMArray<nsIPresentationAvailabilityListener> mListeners;
+ };
+
+ nsClassHashtable<nsStringHashKey, AvailabilityEntry> mAvailabilityUrlTable;
+ };
+
+ virtual ~PresentationServiceBase() = default;
+
+ void Shutdown() {
+ mRespondingListeners.Clear();
+ mControllerSessionIdManager.Clear();
+ mReceiverSessionIdManager.Clear();
+ }
+
+ nsresult GetWindowIdBySessionIdInternal(const nsAString& aSessionId,
+ uint8_t aRole, uint64_t* aWindowId) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ if (NS_WARN_IF(!aWindowId)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ return mControllerSessionIdManager.GetWindowId(aSessionId, aWindowId);
+ }
+
+ return mReceiverSessionIdManager.GetWindowId(aSessionId, aWindowId);
+ }
+
+ void AddRespondingSessionId(uint64_t aWindowId, const nsAString& aSessionId,
+ uint8_t aRole) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ mControllerSessionIdManager.AddSessionId(aWindowId, aSessionId);
+ } else {
+ mReceiverSessionIdManager.AddSessionId(aWindowId, aSessionId);
+ }
+ }
+
+ void RemoveRespondingSessionId(const nsAString& aSessionId, uint8_t aRole) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ mControllerSessionIdManager.RemoveSessionId(aSessionId);
+ } else {
+ mReceiverSessionIdManager.RemoveSessionId(aSessionId);
+ }
+ }
+
+ void UpdateWindowIdBySessionIdInternal(const nsAString& aSessionId,
+ uint8_t aRole,
+ const uint64_t aWindowId) {
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ mControllerSessionIdManager.UpdateWindowId(aSessionId, aWindowId);
+ } else {
+ mReceiverSessionIdManager.UpdateWindowId(aSessionId, aWindowId);
+ }
+ }
+
+ // Store the responding listener based on the window ID of the (in-process or
+ // OOP) receiver page.
+ nsRefPtrHashtable<nsUint64HashKey, nsIPresentationRespondingListener>
+ mRespondingListeners;
+
+ // Store the mapping between the window ID of the in-process and OOP page and
+ // the ID of the responding session. It's used for both controller and
+ // receiver page to retrieve the correspondent session ID. Besides, also keep
+ // the mapping between the responding session ID and the window ID to help
+ // look up the window ID.
+ SessionIdManager mControllerSessionIdManager;
+ SessionIdManager mReceiverSessionIdManager;
+
+ nsRefPtrHashtable<nsStringHashKey, T> mSessionInfoAtController;
+ nsRefPtrHashtable<nsStringHashKey, T> mSessionInfoAtReceiver;
+
+ AvailabilityManager mAvailabilityManager;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationServiceBase_h
diff --git a/dom/presentation/PresentationSessionInfo.cpp b/dom/presentation/PresentationSessionInfo.cpp
new file mode 100644
index 0000000000..2db21b31e1
--- /dev/null
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -0,0 +1,1536 @@
+/* -*- 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 "PresentationSessionInfo.h"
+
+#include <utility>
+
+#include "PresentationLog.h"
+#include "PresentationService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/HTMLIFrameElementBinding.h"
+#include "nsContentUtils.h"
+#include "nsFrameLoader.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsGlobalWindow.h"
+#include "nsIDocShell.h"
+#include "nsIMutableArray.h"
+#include "nsINetAddr.h"
+#include "nsISocketTransport.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetCID.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "nsIPresentationNetworkHelper.h"
+#endif // MOZ_WIDGET_ANDROID
+
+/*
+ * Implementation of PresentationChannelDescription
+ */
+
+namespace mozilla {
+namespace dom {
+
+#ifdef MOZ_WIDGET_ANDROID
+
+namespace {
+
+class PresentationNetworkHelper final
+ : public nsIPresentationNetworkHelperListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONNETWORKHELPERLISTENER
+
+ using Function = nsresult (PresentationControllingInfo::*)(const nsACString&);
+
+ explicit PresentationNetworkHelper(PresentationControllingInfo* aInfo,
+ const Function& aFunc);
+
+ nsresult GetWifiIPAddress();
+
+ private:
+ ~PresentationNetworkHelper() = default;
+
+ RefPtr<PresentationControllingInfo> mInfo;
+ Function mFunc;
+};
+
+NS_IMPL_ISUPPORTS(PresentationNetworkHelper,
+ nsIPresentationNetworkHelperListener)
+
+PresentationNetworkHelper::PresentationNetworkHelper(
+ PresentationControllingInfo* aInfo, const Function& aFunc)
+ : mInfo(aInfo), mFunc(aFunc) {
+ MOZ_ASSERT(aInfo);
+ MOZ_ASSERT(aFunc);
+}
+
+nsresult PresentationNetworkHelper::GetWifiIPAddress() {
+ nsresult rv;
+
+ nsCOMPtr<nsIPresentationNetworkHelper> networkHelper =
+ do_GetService(PRESENTATION_NETWORK_HELPER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return networkHelper->GetWifiIPAddress(this);
+}
+
+NS_IMETHODIMP
+PresentationNetworkHelper::OnError(const nsACString& aReason) {
+ PRES_ERROR("PresentationNetworkHelper::OnError: %s",
+ nsPromiseFlatCString(aReason).get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationNetworkHelper::OnGetWifiIPAddress(const nsACString& aIPAddress) {
+ MOZ_ASSERT(mInfo);
+ MOZ_ASSERT(mFunc);
+
+ NS_DispatchToMainThread(NewRunnableMethod<nsCString>(
+ "dom::PresentationNetworkHelper::OnGetWifiIPAddress", mInfo, mFunc,
+ aIPAddress));
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+#endif // MOZ_WIDGET_ANDROID
+
+class TCPPresentationChannelDescription final
+ : public nsIPresentationChannelDescription {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONCHANNELDESCRIPTION
+
+ TCPPresentationChannelDescription(const nsACString& aAddress, uint16_t aPort)
+ : mAddress(aAddress), mPort(aPort) {}
+
+ private:
+ ~TCPPresentationChannelDescription() = default;
+
+ nsCString mAddress;
+ uint16_t mPort;
+};
+
+NS_IMPL_ISUPPORTS(TCPPresentationChannelDescription,
+ nsIPresentationChannelDescription)
+
+NS_IMETHODIMP
+TCPPresentationChannelDescription::GetType(uint8_t* aRetVal) {
+ if (NS_WARN_IF(!aRetVal)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ *aRetVal = nsIPresentationChannelDescription::TYPE_TCP;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPPresentationChannelDescription::GetTcpAddress(nsIArray** aRetVal) {
+ if (NS_WARN_IF(!aRetVal)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ if (NS_WARN_IF(!array)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // TODO bug 1228504 Take all IP addresses in PresentationChannelDescription
+ // into account. And at the first stage Presentation API is only exposed on
+ // Firefox OS where the first IP appears enough for most scenarios.
+ nsCOMPtr<nsISupportsCString> address =
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID);
+ if (NS_WARN_IF(!address)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ address->SetData(mAddress);
+
+ array->AppendElement(address);
+ array.forget(aRetVal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPPresentationChannelDescription::GetTcpPort(uint16_t* aRetVal) {
+ if (NS_WARN_IF(!aRetVal)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ *aRetVal = mPort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPPresentationChannelDescription::GetDataChannelSDP(
+ nsAString& aDataChannelSDP) {
+ aDataChannelSDP.Truncate();
+ return NS_OK;
+}
+
+/*
+ * Implementation of PresentationSessionInfo
+ */
+
+NS_IMPL_ISUPPORTS(PresentationSessionInfo,
+ nsIPresentationSessionTransportCallback,
+ nsIPresentationControlChannelListener,
+ nsIPresentationSessionTransportBuilderListener);
+
+/* virtual */
+nsresult PresentationSessionInfo::Init(
+ nsIPresentationControlChannel* aControlChannel) {
+ SetControlChannel(aControlChannel);
+ return NS_OK;
+}
+
+/* virtual */
+void PresentationSessionInfo::Shutdown(nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(),
+ static_cast<uint32_t>(aReason), mRole);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(aReason), "bad reason");
+
+ // Close the control channel if any.
+ if (mControlChannel) {
+ Unused << 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.
+ Unused << NS_WARN_IF(NS_FAILED(mTransport->Close(aReason)));
+ }
+
+ mIsResponderReady = false;
+ mIsOnTerminating = false;
+
+ ResetBuilder();
+}
+
+nsresult PresentationSessionInfo::SetListener(
+ nsIPresentationSessionListener* aListener) {
+ mListener = aListener;
+
+ if (mListener) {
+ // Enable data notification for the transport channel if it's available.
+ if (mTransport) {
+ nsresult rv = mTransport->EnableDataNotification();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // The transport might become ready, or might become un-ready again, before
+ // the listener has registered. So notify the listener of the state change.
+ return mListener->NotifyStateChange(mSessionId, mState, mReason);
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationSessionInfo::Send(const nsAString& aData) {
+ if (NS_WARN_IF(!IsSessionReady())) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->Send(aData);
+}
+
+nsresult PresentationSessionInfo::SendBinaryMsg(const nsACString& aData) {
+ if (NS_WARN_IF(!IsSessionReady())) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->SendBinaryMsg(aData);
+}
+
+nsresult PresentationSessionInfo::SendBlob(Blob* aBlob) {
+ if (NS_WARN_IF(!IsSessionReady())) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->SendBlob(aBlob);
+}
+
+nsresult PresentationSessionInfo::Close(nsresult aReason, uint32_t aState) {
+ // Do nothing if session is already terminated.
+ if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+ return NS_OK;
+ }
+
+ SetStateWithReason(aState, aReason);
+
+ switch (aState) {
+ case nsIPresentationSessionListener::STATE_CLOSED: {
+ Shutdown(aReason);
+ break;
+ }
+ case nsIPresentationSessionListener::STATE_TERMINATED: {
+ if (!mControlChannel) {
+ nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+ nsresult rv =
+ mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
+ if (NS_FAILED(rv)) {
+ Shutdown(rv);
+ return rv;
+ }
+
+ SetControlChannel(ctrlChannel);
+ return rv;
+ }
+
+ ContinueTermination();
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+void PresentationSessionInfo::OnTerminate(
+ nsIPresentationControlChannel* aControlChannel) {
+ mIsOnTerminating = true; // Mark for terminating transport channel
+ SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED, NS_OK);
+ SetControlChannel(aControlChannel);
+}
+
+nsresult PresentationSessionInfo::ReplySuccess() {
+ SetStateWithReason(nsIPresentationSessionListener::STATE_CONNECTED, NS_OK);
+ return NS_OK;
+}
+
+nsresult PresentationSessionInfo::ReplyError(nsresult aError) {
+ Shutdown(aError);
+
+ // Remove itself since it never succeeds.
+ return UntrackFromService();
+}
+
+/* virtual */
+nsresult PresentationSessionInfo::UntrackFromService() {
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ static_cast<PresentationService*>(service.get())
+ ->UntrackSessionInfo(mSessionId, mRole);
+
+ return NS_OK;
+}
+
+nsPIDOMWindowInner* PresentationSessionInfo::GetWindow() {
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return nullptr;
+ }
+ uint64_t windowId = 0;
+ if (NS_WARN_IF(NS_FAILED(
+ service->GetWindowIdBySessionId(mSessionId, mRole, &windowId)))) {
+ return nullptr;
+ }
+
+ return nsGlobalWindowInner::GetInnerWindowWithId(windowId);
+}
+
+/* virtual */
+bool PresentationSessionInfo::IsAccessible(base::ProcessId aProcessId) {
+ // No restriction by default.
+ return true;
+}
+
+void PresentationSessionInfo::ContinueTermination() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mControlChannel);
+
+ if (NS_WARN_IF(NS_FAILED(mControlChannel->Terminate(mSessionId))) ||
+ mIsOnTerminating) {
+ Shutdown(NS_OK);
+ }
+}
+
+// nsIPresentationSessionTransportCallback
+NS_IMETHODIMP
+PresentationSessionInfo::NotifyTransportReady() {
+ PRES_DEBUG("%s:id[%s], role[%d], state[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole, mState);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != nsIPresentationSessionListener::STATE_CONNECTING &&
+ mState != nsIPresentationSessionListener::STATE_CONNECTED) {
+ return NS_OK;
+ }
+
+ mIsTransportReady = true;
+
+ // Established RTCDataChannel implies responder is ready.
+ if (mTransportType == nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ mIsResponderReady = true;
+ }
+
+ // At sender side, session might not be ready at this point (waiting for
+ // receiver's answer). Yet at receiver side, session must be ready at this
+ // point since the data transport channel is created after the receiver page
+ // is ready for presentation use.
+ if (IsSessionReady()) {
+ return ReplySuccess();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::NotifyTransportClosed(nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(),
+ static_cast<uint32_t>(aReason), mRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Nullify |mTransport| here so it won't try to re-close |mTransport| in
+ // potential subsequent |Shutdown| calls.
+ mTransport = nullptr;
+
+ if (NS_WARN_IF(!IsSessionReady() &&
+ mState == nsIPresentationSessionListener::STATE_CONNECTING)) {
+ // It happens before the session is ready. Reply the callback.
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ // Unset |mIsTransportReady| here so it won't affect |IsSessionReady()| above.
+ mIsTransportReady = false;
+
+ if (mState == nsIPresentationSessionListener::STATE_CONNECTED) {
+ // The transport channel is closed unexpectedly (not caused by a |Close|
+ // call).
+ SetStateWithReason(nsIPresentationSessionListener::STATE_CLOSED, aReason);
+ }
+
+ Shutdown(aReason);
+
+ if (mState == nsIPresentationSessionListener::STATE_TERMINATED) {
+ // Directly untrack the session info from the service.
+ return UntrackFromService();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::NotifyData(const nsACString& aData, bool aIsBinary) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!IsSessionReady())) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (NS_WARN_IF(!mListener)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mListener->NotifyMessage(mSessionId, aData, aIsBinary);
+}
+
+// nsIPresentationSessionTransportBuilderListener
+NS_IMETHODIMP
+PresentationSessionInfo::OnSessionTransport(
+ nsIPresentationSessionTransport* aTransport) {
+ PRES_DEBUG("%s:id[%s], role[%d], state[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole, mState);
+
+ ResetBuilder();
+
+ if (mState != nsIPresentationSessionListener::STATE_CONNECTING) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!aTransport)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mTransport = aTransport;
+
+ nsresult rv = mTransport->SetCallback(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mListener) {
+ mTransport->EnableDataNotification();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::OnError(nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(),
+ static_cast<uint32_t>(aReason), mRole);
+
+ ResetBuilder();
+ return ReplyError(aReason);
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::SendOffer(nsIPresentationChannelDescription* aOffer) {
+ return mControlChannel->SendOffer(aOffer);
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::SendAnswer(
+ nsIPresentationChannelDescription* aAnswer) {
+ return mControlChannel->SendAnswer(aAnswer);
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::SendIceCandidate(const nsAString& candidate) {
+ return mControlChannel->SendIceCandidate(candidate);
+}
+
+NS_IMETHODIMP
+PresentationSessionInfo::Close(nsresult 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.)
+ * 1. |Init| is called to open a socket |mServerSocket| for data transport
+ * channel.
+ * 2. |NotifyConnected| of |nsIPresentationControlChannelListener| is called to
+ * indicate the control channel is ready to use. Then send the offer to the
+ * receiver via the control channel.
+ * 3.1 |OnSocketAccepted| of |nsIServerSocketListener| is called to indicate the
+ * data transport channel is connected. Then initialize |mTransport|.
+ * 3.2 |NotifyTransportReady| of |nsIPresentationSessionTransportCallback| is
+ * called.
+ * 4. |OnAnswer| of |nsIPresentationControlChannelListener| is called to
+ * indicate the receiver is ready. Close the control channel since it's no
+ * longer needed.
+ * 5. Once both step 3 and 4 are done, the presentation session is ready to use.
+ * So notify the listener of CONNECTED state.
+ */
+
+NS_IMPL_ISUPPORTS_INHERITED(PresentationControllingInfo,
+ PresentationSessionInfo, nsIServerSocketListener)
+
+nsresult PresentationControllingInfo::Init(
+ nsIPresentationControlChannel* aControlChannel) {
+ PresentationSessionInfo::Init(aControlChannel);
+
+ // Initialize |mServerSocket| for bootstrapping the data transport channel and
+ // use |this| as the listener.
+ mServerSocket = do_CreateInstance(NS_SERVERSOCKET_CONTRACTID);
+ if (NS_WARN_IF(!mServerSocket)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ nsresult rv = mServerSocket->Init(-1, false, -1);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mServerSocket->AsyncListen(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t port;
+ rv = mServerSocket->GetPort(&port);
+ if (!NS_WARN_IF(NS_FAILED(rv))) {
+ PRES_DEBUG("%s:ServerSocket created.port[%d]\n", __func__, port);
+ }
+
+ return NS_OK;
+}
+
+void PresentationControllingInfo::Shutdown(nsresult aReason) {
+ PresentationSessionInfo::Shutdown(aReason);
+
+ // Close the server socket if any.
+ if (mServerSocket) {
+ Unused << NS_WARN_IF(NS_FAILED(mServerSocket->Close()));
+ mServerSocket = nullptr;
+ }
+}
+
+nsresult PresentationControllingInfo::GetAddress() {
+ if (nsContentUtils::ShouldResistFingerprinting()) {
+ return NS_ERROR_FAILURE;
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ RefPtr<PresentationNetworkHelper> networkHelper =
+ new PresentationNetworkHelper(this,
+ &PresentationControllingInfo::OnGetAddress);
+ nsresult rv = networkHelper->GetWifiIPAddress();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+#else
+ nsCOMPtr<nsINetworkInfoService> networkInfo =
+ do_GetService(NETWORKINFOSERVICE_CONTRACT_ID);
+ MOZ_ASSERT(networkInfo);
+
+ nsresult rv = networkInfo->ListNetworkAddresses(this);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+#endif
+
+ return NS_OK;
+}
+
+nsresult PresentationControllingInfo::OnGetAddress(const nsACString& aAddress) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mServerSocket)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(!mControlChannel)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Prepare and send the offer.
+ int32_t port;
+ nsresult rv = mServerSocket->GetPort(&port);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<TCPPresentationChannelDescription> description =
+ new TCPPresentationChannelDescription(aAddress,
+ static_cast<uint16_t>(port));
+ return mControlChannel->SendOffer(description);
+}
+
+// nsIPresentationControlChannelListener
+NS_IMETHODIMP
+PresentationControllingInfo::OnIceCandidate(const nsAString& aCandidate) {
+ if (mTransportType != nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return builder->OnIceCandidate(aCandidate);
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::OnOffer(
+ nsIPresentationChannelDescription* aDescription) {
+ MOZ_ASSERT(false, "Sender side should not receive offer.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::OnAnswer(
+ nsIPresentationChannelDescription* aDescription) {
+ if (mTransportType == nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return builder->OnAnswer(aDescription);
+ }
+
+ mIsResponderReady = true;
+
+ // Close the control channel since it's no longer needed.
+ 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();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyConnected() {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ switch (mState) {
+ case nsIPresentationSessionListener::STATE_CONNECTING: {
+ if (mIsReconnecting) {
+ return ContinueReconnect();
+ }
+
+ nsresult rv = mControlChannel->Launch(GetSessionId(), GetUrl());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ Unused << NS_WARN_IF(NS_FAILED(BuildTransport()));
+ break;
+ }
+ case nsIPresentationSessionListener::STATE_TERMINATED: {
+ ContinueTermination();
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyReconnected() {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(mState != nsIPresentationSessionListener::STATE_CONNECTING)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NotifyReconnectResult(NS_OK);
+}
+
+nsresult PresentationControllingInfo::BuildTransport() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != nsIPresentationSessionListener::STATE_CONNECTING) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!mBuilderConstructor)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!Preferences::GetBool(
+ "dom.presentation.session_transport.data_channel.enable")) {
+ // Build TCP session transport
+ return GetAddress();
+ }
+ /**
+ * Generally transport is maintained by the chrome process. However, data
+ * channel should be live with the DOM , which implies RTCDataChannel in an
+ * OOP page should be establish in the content process.
+ *
+ * |mBuilderConstructor| is responsible for creating a builder, which is for
+ * building a data channel transport.
+ *
+ * In the OOP case, |mBuilderConstructor| would create a builder which is
+ * an object of |PresentationBuilderParent|. So, |BuildDataChannelTransport|
+ * triggers an IPC call to make content process establish a RTCDataChannel
+ * transport.
+ */
+
+ mTransportType = nsIPresentationChannelDescription::TYPE_DATACHANNEL;
+ if (NS_WARN_IF(NS_FAILED(mBuilderConstructor->CreateTransportBuilder(
+ mTransportType, getter_AddRefs(mBuilder))))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder>
+ dataChannelBuilder(do_QueryInterface(mBuilder));
+ if (NS_WARN_IF(!dataChannelBuilder)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // OOP window would be set from content process
+ nsPIDOMWindowInner* window = GetWindow();
+
+ nsresult rv = dataChannelBuilder->BuildDataChannelTransport(
+ nsIPresentationService::ROLE_CONTROLLER, window, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyDisconnected(nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(),
+ static_cast<uint32_t>(aReason), mRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTransportType == nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+ if (builder) {
+ Unused << NS_WARN_IF(NS_FAILED(builder->NotifyDisconnected(aReason)));
+ }
+ }
+
+ // Unset control channel here so it won't try to re-close it in potential
+ // subsequent |Shutdown| calls.
+ SetControlChannel(nullptr);
+
+ if (NS_WARN_IF(NS_FAILED(aReason) || !mIsResponderReady)) {
+ // The presentation session instance may already exist.
+ // Change the state to CLOSED if it is not terminated.
+ if (nsIPresentationSessionListener::STATE_TERMINATED != mState) {
+ SetStateWithReason(nsIPresentationSessionListener::STATE_CLOSED, aReason);
+ }
+
+ // If |aReason| is NS_OK, it implies that the user closes the connection
+ // before becomming connected. No need to call |ReplyError| in this case.
+ if (NS_FAILED(aReason)) {
+ if (mIsReconnecting) {
+ NotifyReconnectResult(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ // Reply error for an abnormal close.
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ Shutdown(aReason);
+ }
+
+ // This is the case for reconnecting a connection which is in
+ // connecting state and |mTransport| is not ready.
+ if (mDoReconnectAfterClose && !mTransport) {
+ mDoReconnectAfterClose = false;
+ return Reconnect(mReconnectCallback);
+ }
+
+ return NS_OK;
+}
+
+// nsIServerSocketListener
+NS_IMETHODIMP
+PresentationControllingInfo::OnSocketAccepted(nsIServerSocket* aServerSocket,
+ nsISocketTransport* aTransport) {
+ int32_t port;
+ nsresult rv = aTransport->GetPort(&port);
+ if (!NS_WARN_IF(NS_FAILED(rv))) {
+ PRES_DEBUG("%s:receive from port[%d]\n", __func__, port);
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mBuilderConstructor)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ // Initialize session transport builder and use |this| as the callback.
+ nsCOMPtr<nsIPresentationTCPSessionTransportBuilder> builder;
+ if (NS_SUCCEEDED(mBuilderConstructor->CreateTransportBuilder(
+ nsIPresentationChannelDescription::TYPE_TCP,
+ getter_AddRefs(mBuilder)))) {
+ builder = do_QueryInterface(mBuilder);
+ }
+
+ if (NS_WARN_IF(!builder)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ mTransportType = nsIPresentationChannelDescription::TYPE_TCP;
+ return builder->BuildTCPSenderTransport(aTransport, this);
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::OnStopListening(nsIServerSocket* aServerSocket,
+ nsresult aStatus) {
+ PRES_DEBUG("controller %s:status[%" PRIx32 "]\n", __func__,
+ static_cast<uint32_t>(aStatus));
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aStatus ==
+ NS_BINDING_ABORTED) { // The server socket was manually closed.
+ return NS_OK;
+ }
+
+ Shutdown(aStatus);
+
+ if (NS_WARN_IF(!IsSessionReady())) {
+ // It happens before the session is ready. Reply the callback.
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ // It happens after the session is ready. Change the state to CLOSED.
+ SetStateWithReason(nsIPresentationSessionListener::STATE_CLOSED, aStatus);
+
+ return NS_OK;
+}
+
+/**
+ * The steps to reconnect a session are summarized below:
+ * 1. Change |mState| to CONNECTING.
+ * 2. Check whether |mControlChannel| is existed or not. Usually we have to
+ * create a new control cahnnel.
+ * 3.1 |mControlChannel| is null, which means we have to create a new one.
+ * |EstablishControlChannel| is called to create a new control channel.
+ * At this point, |mControlChannel| is not able to use yet. Set
+ * |mIsReconnecting| to true and wait until |NotifyConnected|.
+ * 3.2 |mControlChannel| is not null and is avaliable.
+ * We can just call |ContinueReconnect| to send reconnect command.
+ * 4. |NotifyReconnected| of |nsIPresentationControlChannelListener| is called
+ * to indicate the receiver is ready for reconnecting.
+ * 5. Once both step 3 and 4 are done, the rest is to build a new data
+ * transport channel by following the same steps as starting a
+ * new session.
+ */
+
+nsresult PresentationControllingInfo::Reconnect(
+ nsIPresentationServiceCallback* aCallback) {
+ PRES_DEBUG("%s:id[%s], role[%d], state[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole, mState);
+
+ if (!aCallback) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mReconnectCallback = aCallback;
+
+ if (NS_WARN_IF(mState == nsIPresentationSessionListener::STATE_TERMINATED)) {
+ return NotifyReconnectResult(NS_ERROR_DOM_INVALID_STATE_ERR);
+ }
+
+ // If |mState| is not CLOSED, we have to close the connection before
+ // reconnecting. The process to reconnect will be continued after
+ // |NotifyDisconnected| or |NotifyTransportClosed| is invoked.
+ if (mState == nsIPresentationSessionListener::STATE_CONNECTING ||
+ mState == nsIPresentationSessionListener::STATE_CONNECTED) {
+ mDoReconnectAfterClose = true;
+ return Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED);
+ }
+
+ // Make sure |mState| is closed at this point.
+ MOZ_ASSERT(mState == nsIPresentationSessionListener::STATE_CLOSED);
+
+ mState = nsIPresentationSessionListener::STATE_CONNECTING;
+ mIsReconnecting = true;
+
+ nsresult rv = NS_OK;
+ if (!mControlChannel) {
+ nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+ rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NotifyReconnectResult(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ rv = Init(ctrlChannel);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NotifyReconnectResult(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ } else {
+ return ContinueReconnect();
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationControllingInfo::ContinueReconnect() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mControlChannel);
+
+ mIsReconnecting = false;
+ if (NS_WARN_IF(NS_FAILED(mControlChannel->Reconnect(mSessionId, GetUrl())))) {
+ return NotifyReconnectResult(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ return NS_OK;
+}
+
+// nsIListNetworkAddressesListener
+NS_IMETHODIMP
+PresentationControllingInfo::OnListedNetworkAddresses(
+ const nsTArray<nsCString>& aAddressArray) {
+ if (aAddressArray.IsEmpty()) {
+ return OnListNetworkAddressesFailed();
+ }
+
+ // TODO bug 1228504 Take all IP addresses in PresentationChannelDescription
+ // into account.
+
+ // On Firefox desktop, the IP address is retrieved from a callback function.
+ // To make consistent code sequence, following function call is dispatched
+ // into main thread instead of calling it directly.
+ NS_DispatchToMainThread(NewRunnableMethod<nsCString>(
+ "dom::PresentationControllingInfo::OnGetAddress", this,
+ &PresentationControllingInfo::OnGetAddress, aAddressArray[0]));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::OnListNetworkAddressesFailed() {
+ PRES_ERROR("PresentationControllingInfo:OnListNetworkAddressesFailed");
+
+ // In 1-UA case, transport channel can still be established
+ // on loopback interface even if no network address available.
+ NS_DispatchToMainThread(NewRunnableMethod<nsCString>(
+ "dom::PresentationControllingInfo::OnGetAddress", this,
+ &PresentationControllingInfo::OnGetAddress, "127.0.0.1"));
+
+ return NS_OK;
+}
+
+nsresult PresentationControllingInfo::NotifyReconnectResult(nsresult aStatus) {
+ if (!mReconnectCallback) {
+ MOZ_ASSERT(false, "mReconnectCallback can not be null here.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mIsReconnecting = false;
+ nsCOMPtr<nsIPresentationServiceCallback> callback =
+ std::move(mReconnectCallback);
+ if (NS_FAILED(aStatus)) {
+ return callback->NotifyError(aStatus);
+ }
+
+ return callback->NotifySuccess(GetUrl());
+}
+
+// nsIPresentationSessionTransportCallback
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyTransportReady() {
+ return PresentationSessionInfo::NotifyTransportReady();
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyTransportClosed(nsresult aReason) {
+ if (!mDoReconnectAfterClose) {
+ return PresentationSessionInfo::NotifyTransportClosed(aReason);
+ ;
+ }
+
+ MOZ_ASSERT(mState == nsIPresentationSessionListener::STATE_CLOSED);
+
+ mTransport = nullptr;
+ mIsTransportReady = false;
+ mDoReconnectAfterClose = false;
+ return Reconnect(mReconnectCallback);
+}
+
+NS_IMETHODIMP
+PresentationControllingInfo::NotifyData(const nsACString& aData,
+ bool aIsBinary) {
+ return PresentationSessionInfo::NotifyData(aData, aIsBinary);
+}
+
+/**
+ * Implementation of PresentationPresentingInfo
+ *
+ * During presentation session establishment, the receiver expects the following
+ * after trying to launch the app by notifying "presentation-launch-receiver":
+ * (The order between step 2 and 3 is not guaranteed.)
+ * 1. |Observe| of |nsIObserver| is called with
+ * "presentation-receiver-launched".
+ * Then start listen to document |STATE_TRANSFERRING| event.
+ * 2. |NotifyResponderReady| is called to indicate the receiver page is ready
+ * for presentation use.
+ * 3. |OnOffer| of |nsIPresentationControlChannelListener| is called.
+ * 4. Once both step 2 and 3 are done, establish the data transport channel and
+ * send the answer. (The control channel will be closed by the sender once it
+ * receives the answer.)
+ * 5. |NotifyTransportReady| of |nsIPresentationSessionTransportCallback| is
+ * called. The presentation session is ready to use, so notify the listener
+ * of CONNECTED state.
+ */
+
+NS_IMPL_ISUPPORTS_INHERITED(PresentationPresentingInfo, PresentationSessionInfo,
+ nsITimerCallback, nsINamed)
+
+nsresult PresentationPresentingInfo::Init(
+ nsIPresentationControlChannel* aControlChannel) {
+ PresentationSessionInfo::Init(aControlChannel);
+
+ // Add a timer to prevent waiting indefinitely in case the receiver page fails
+ // to become ready.
+ nsresult rv;
+ int32_t timeout =
+ Preferences::GetInt("presentation.receiver.loading.timeout", 10000);
+ rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void PresentationPresentingInfo::Shutdown(nsresult aReason) {
+ PresentationSessionInfo::Shutdown(aReason);
+
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+
+ mLoadingCallback = nullptr;
+ mRequesterDescription = nullptr;
+ mPendingCandidates.Clear();
+ mPromise = nullptr;
+ mHasFlushPendingEvents = false;
+}
+
+// nsIPresentationSessionTransportBuilderListener
+NS_IMETHODIMP
+PresentationPresentingInfo::OnSessionTransport(
+ nsIPresentationSessionTransport* aTransport) {
+ nsresult rv = PresentationSessionInfo::OnSessionTransport(aTransport);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // The session transport is managed by content process
+ if (NS_WARN_IF(!aTransport)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // send answer for TCP session transport
+ if (mTransportType == nsIPresentationChannelDescription::TYPE_TCP) {
+ // Prepare and send the answer.
+ // In the current implementation of |PresentationSessionTransport|,
+ // |GetSelfAddress| cannot return the real info when it's initialized via
+ // |buildTCPReceiverTransport|. Yet this deficiency only affects the channel
+ // description for the answer, which is not actually checked at requester
+ // side.
+ nsCOMPtr<nsINetAddr> selfAddr;
+ rv = mTransport->GetSelfAddress(getter_AddRefs(selfAddr));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "GetSelfAddress failed");
+
+ nsCString address;
+ uint16_t port = 0;
+ if (NS_SUCCEEDED(rv)) {
+ selfAddr->GetAddress(address);
+ selfAddr->GetPort(&port);
+ }
+ nsCOMPtr<nsIPresentationChannelDescription> description =
+ new TCPPresentationChannelDescription(address, port);
+
+ return mControlChannel->SendAnswer(description);
+ }
+
+ return NS_OK;
+}
+
+// Delegate the pending offer and ICE candidates to builder.
+NS_IMETHODIMP
+PresentationPresentingInfo::FlushPendingEvents(
+ nsIPresentationDataChannelSessionTransportBuilder* builder) {
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mHasFlushPendingEvents = true;
+
+ if (mRequesterDescription) {
+ builder->OnOffer(mRequesterDescription);
+ }
+ mRequesterDescription = nullptr;
+
+ for (size_t i = 0; i < mPendingCandidates.Length(); ++i) {
+ builder->OnIceCandidate(mPendingCandidates[i]);
+ }
+ mPendingCandidates.Clear();
+ return NS_OK;
+}
+
+nsresult PresentationPresentingInfo::InitTransportAndSendAnswer() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mState == nsIPresentationSessionListener::STATE_CONNECTING);
+
+ uint8_t type = 0;
+ nsresult rv = mRequesterDescription->GetType(&type);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!mBuilderConstructor)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(mBuilderConstructor->CreateTransportBuilder(
+ type, getter_AddRefs(mBuilder))))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (type == nsIPresentationChannelDescription::TYPE_TCP) {
+ // Establish a data transport channel |mTransport| to the sender and use
+ // |this| as the callback.
+ nsCOMPtr<nsIPresentationTCPSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mTransportType = nsIPresentationChannelDescription::TYPE_TCP;
+ return builder->BuildTCPReceiverTransport(mRequesterDescription, this);
+ }
+
+ if (type == nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ if (!Preferences::GetBool(
+ "dom.presentation.session_transport.data_channel.enable")) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ /**
+ * Generally transport is maintained by the chrome process. However, data
+ * channel should be live with the DOM , which implies RTCDataChannel in an
+ * OOP page should be establish in the content process.
+ *
+ * |mBuilderConstructor| is responsible for creating a builder, which is for
+ * building a data channel transport.
+ *
+ * In the OOP case, |mBuilderConstructor| would create a builder which is
+ * an object of |PresentationBuilderParent|. So, |BuildDataChannelTransport|
+ * triggers an IPC call to make content process establish a RTCDataChannel
+ * transport.
+ */
+
+ mTransportType = nsIPresentationChannelDescription::TYPE_DATACHANNEL;
+
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder>
+ dataChannelBuilder = do_QueryInterface(mBuilder);
+ if (NS_WARN_IF(!dataChannelBuilder)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsPIDOMWindowInner* window = GetWindow();
+
+ rv = dataChannelBuilder->BuildDataChannelTransport(
+ nsIPresentationService::ROLE_RECEIVER, window, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = FlushPendingEvents(dataChannelBuilder);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(false, "Unknown nsIPresentationChannelDescription type!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult PresentationPresentingInfo::UntrackFromService() {
+ // Remove the OOP responding info (if it has never been used).
+ if (mContentParent) {
+ Unused << NS_WARN_IF(
+ !static_cast<ContentParent*>(mContentParent.get())
+ ->SendNotifyPresentationReceiverCleanUp(mSessionId));
+ }
+
+ // Receiver device might need clean up after session termination.
+ if (mDevice) {
+ mDevice->Disconnect();
+ }
+ mDevice = nullptr;
+
+ // Remove the session info (and the in-process responding info if there's
+ // any).
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ static_cast<PresentationService*>(service.get())
+ ->UntrackSessionInfo(mSessionId, mRole);
+
+ return NS_OK;
+}
+
+bool PresentationPresentingInfo::IsAccessible(base::ProcessId aProcessId) {
+ // Only the specific content process should access the responder info.
+ return (mContentParent)
+ ? aProcessId ==
+ static_cast<ContentParent*>(mContentParent.get())->OtherPid()
+ : false;
+}
+
+nsresult PresentationPresentingInfo::NotifyResponderReady() {
+ PRES_DEBUG("%s:id[%s], role[%d], state[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole, mState);
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ mIsResponderReady = true;
+
+ // Initialize |mTransport| and send the answer to the sender if sender's
+ // description is already offered.
+ if (mRequesterDescription) {
+ nsresult rv = InitTransportAndSendAnswer();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationPresentingInfo::NotifyResponderFailure() {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole);
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+}
+
+nsresult PresentationPresentingInfo::DoReconnect() {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole);
+
+ MOZ_ASSERT(mState == nsIPresentationSessionListener::STATE_CLOSED);
+
+ SetStateWithReason(nsIPresentationSessionListener::STATE_CONNECTING, NS_OK);
+
+ return NotifyResponderReady();
+}
+
+// nsIPresentationControlChannelListener
+NS_IMETHODIMP
+PresentationPresentingInfo::OnOffer(
+ nsIPresentationChannelDescription* aDescription) {
+ if (NS_WARN_IF(mHasFlushPendingEvents)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ if (NS_WARN_IF(!aDescription)) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ mRequesterDescription = aDescription;
+
+ // Initialize |mTransport| and send the answer to the sender if the receiver
+ // page is ready for presentation use.
+ if (mIsResponderReady) {
+ nsresult rv = InitTransportAndSendAnswer();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationPresentingInfo::OnAnswer(
+ nsIPresentationChannelDescription* aDescription) {
+ MOZ_ASSERT(false, "Receiver side should not receive answer.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+PresentationPresentingInfo::OnIceCandidate(const nsAString& aCandidate) {
+ if (!mBuilder && !mHasFlushPendingEvents) {
+ mPendingCandidates.AppendElement(nsString(aCandidate));
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!mBuilder && mHasFlushPendingEvents)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+
+ return builder->OnIceCandidate(aCandidate);
+}
+
+NS_IMETHODIMP
+PresentationPresentingInfo::NotifyConnected() {
+ PRES_DEBUG("%s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), mRole);
+
+ if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+ ContinueTermination();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationPresentingInfo::NotifyReconnected() {
+ MOZ_ASSERT(false, "NotifyReconnected should not be called at receiver side.");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationPresentingInfo::NotifyDisconnected(nsresult aReason) {
+ PRES_DEBUG("%s:id[%s], reason[%" PRIx32 "], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(),
+ static_cast<uint32_t>(aReason), mRole);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTransportType == nsIPresentationChannelDescription::TYPE_DATACHANNEL) {
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> builder =
+ do_QueryInterface(mBuilder);
+ if (builder) {
+ Unused << NS_WARN_IF(NS_FAILED(builder->NotifyDisconnected(aReason)));
+ }
+ }
+
+ // Unset control channel here so it won't try to re-close it in potential
+ // subsequent |Shutdown| calls.
+ SetControlChannel(nullptr);
+
+ if (NS_WARN_IF(NS_FAILED(aReason))) {
+ // The presentation session instance may already exist.
+ // Change the state to TERMINATED since it never succeeds.
+ SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED,
+ aReason);
+
+ // Reply error for an abnormal close.
+ return ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ }
+
+ return NS_OK;
+}
+
+// nsITimerCallback
+NS_IMETHODIMP
+PresentationPresentingInfo::Notify(nsITimer* aTimer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_WARNING("The receiver page fails to become ready before timeout.");
+
+ mTimer = nullptr;
+ return ReplyError(NS_ERROR_DOM_TIMEOUT_ERR);
+}
+
+// nsITimerCallback
+NS_IMETHODIMP
+PresentationPresentingInfo::GetName(nsACString& aName) {
+ aName.AssignLiteral("PresentationPresentingInfo");
+ return NS_OK;
+}
+
+// PromiseNativeHandler
+void PresentationPresentingInfo::ResolvedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aValue.isObject())) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ if (NS_WARN_IF(!obj)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ // Start to listen to document state change event |STATE_TRANSFERRING|.
+ // Use Element to support both HTMLIFrameElement and nsXULElement.
+ Element* frame = nullptr;
+ nsresult rv = UNWRAP_OBJECT(Element, &obj, frame);
+ if (NS_WARN_IF(!frame)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<nsFrameLoaderOwner> owner = do_QueryObject(frame);
+ if (NS_WARN_IF(!owner)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<nsFrameLoader> frameLoader = owner->GetFrameLoader();
+ if (NS_WARN_IF(!frameLoader)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<BrowserParent> browserParent = BrowserParent::GetFrom(frameLoader);
+ if (browserParent) {
+ // OOP frame
+ // Notify the content process that a receiver page has launched, so it can
+ // start monitoring the loading progress.
+ mContentParent = browserParent->Manager();
+ Unused << NS_WARN_IF(!static_cast<ContentParent*>(mContentParent.get())
+ ->SendNotifyPresentationReceiverLaunched(
+ browserParent, mSessionId));
+ } else {
+ // In-process frame
+ IgnoredErrorResult error;
+ nsCOMPtr<nsIDocShell> docShell = frameLoader->GetDocShell(error);
+ if (NS_WARN_IF(error.Failed())) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ // Keep an eye on the loading progress of the receiver page.
+ mLoadingCallback = new PresentationResponderLoadingCallback(mSessionId);
+ rv = mLoadingCallback->Init(docShell);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+ }
+}
+
+void PresentationPresentingInfo::RejectedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_WARNING("Launching the receiver page has been rejected.");
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationSessionInfo.h b/dom/presentation/PresentationSessionInfo.h
new file mode 100644
index 0000000000..c1d530c251
--- /dev/null
+++ b/dom/presentation/PresentationSessionInfo.h
@@ -0,0 +1,268 @@
+/* -*- 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_PresentationSessionInfo_h
+#define mozilla_dom_PresentationSessionInfo_h
+
+#include "base/process.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsINamed.h"
+#include "nsINetworkInfoService.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationDevice.h"
+#include "nsIPresentationListener.h"
+#include "nsIPresentationService.h"
+#include "nsIPresentationSessionTransport.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+#include "nsIServerSocket.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "PresentationCallbacks.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationSessionInfo
+ : public nsIPresentationSessionTransportCallback,
+ public nsIPresentationControlChannelListener,
+ public nsIPresentationSessionTransportBuilderListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTCALLBACK
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTBUILDERLISTENER
+
+ PresentationSessionInfo(const nsAString& aUrl, const nsAString& aSessionId,
+ const uint8_t aRole)
+ : mUrl(aUrl),
+ mSessionId(aSessionId),
+ mIsResponderReady(false),
+ mIsTransportReady(false),
+ mState(nsIPresentationSessionListener::STATE_CONNECTING),
+ mReason(NS_OK) {
+ MOZ_ASSERT(!mUrl.IsEmpty());
+ MOZ_ASSERT(!mSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ mRole = aRole;
+ }
+
+ virtual nsresult Init(nsIPresentationControlChannel* aControlChannel);
+
+ const nsAString& GetUrl() const { return mUrl; }
+
+ const nsAString& GetSessionId() const { return mSessionId; }
+
+ uint8_t GetRole() const { return mRole; }
+
+ nsresult SetListener(nsIPresentationSessionListener* aListener);
+
+ void SetDevice(nsIPresentationDevice* aDevice) { mDevice = aDevice; }
+
+ already_AddRefed<nsIPresentationDevice> GetDevice() const {
+ nsCOMPtr<nsIPresentationDevice> device = mDevice;
+ return device.forget();
+ }
+
+ void SetControlChannel(nsIPresentationControlChannel* aControlChannel) {
+ if (mControlChannel) {
+ mControlChannel->SetListener(nullptr);
+ }
+
+ mControlChannel = aControlChannel;
+ if (mControlChannel) {
+ mControlChannel->SetListener(this);
+ }
+ }
+
+ nsresult Send(const nsAString& aData);
+
+ nsresult SendBinaryMsg(const nsACString& aData);
+
+ nsresult SendBlob(Blob* aBlob);
+
+ nsresult Close(nsresult aReason, uint32_t aState);
+
+ void OnTerminate(nsIPresentationControlChannel* aControlChannel);
+
+ nsresult ReplyError(nsresult aReason);
+
+ virtual bool IsAccessible(base::ProcessId aProcessId);
+
+ void SetTransportBuilderConstructor(
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor) {
+ mBuilderConstructor = aBuilderConstructor;
+ }
+
+ protected:
+ virtual ~PresentationSessionInfo() { Shutdown(NS_OK); }
+
+ virtual void Shutdown(nsresult aReason);
+
+ nsresult ReplySuccess();
+
+ bool IsSessionReady() { return mIsResponderReady && mIsTransportReady; }
+
+ virtual nsresult UntrackFromService();
+
+ void SetStateWithReason(uint32_t aState, nsresult aReason) {
+ if (mState == aState) {
+ return;
+ }
+
+ mState = aState;
+ mReason = aReason;
+
+ // Notify session state change.
+ if (mListener) {
+ DebugOnly<nsresult> rv =
+ mListener->NotifyStateChange(mSessionId, mState, aReason);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NotifyStateChanged");
+ }
+ }
+
+ void ContinueTermination();
+
+ void ResetBuilder() { mBuilder = nullptr; }
+
+ // Should be nsIPresentationChannelDescription::TYPE_TCP/TYPE_DATACHANNEL
+ uint8_t mTransportType = 0;
+
+ nsPIDOMWindowInner* GetWindow();
+
+ nsString mUrl;
+ nsString mSessionId;
+ // mRole should be nsIPresentationService::ROLE_CONTROLLER
+ // or nsIPresentationService::ROLE_RECEIVER.
+ uint8_t mRole;
+ bool mIsResponderReady;
+ bool mIsTransportReady;
+ bool mIsOnTerminating = false;
+ uint32_t mState; // CONNECTED, CLOSED, TERMINATED
+ nsresult mReason;
+ nsCOMPtr<nsIPresentationSessionListener> mListener;
+ nsCOMPtr<nsIPresentationDevice> mDevice;
+ nsCOMPtr<nsIPresentationSessionTransport> mTransport;
+ nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
+ nsCOMPtr<nsIPresentationSessionTransportBuilder> mBuilder;
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> mBuilderConstructor;
+};
+
+// Session info with controlling browsing context (sender side) behaviors.
+class PresentationControllingInfo final
+ : public PresentationSessionInfo,
+ public nsIServerSocketListener,
+ public nsIListNetworkAddressesListener {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPRESENTATIONCONTROLCHANNELLISTENER
+ NS_DECL_NSISERVERSOCKETLISTENER
+ NS_DECL_NSILISTNETWORKADDRESSESLISTENER
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTCALLBACK
+
+ PresentationControllingInfo(const nsAString& aUrl,
+ const nsAString& aSessionId)
+ : PresentationSessionInfo(aUrl, aSessionId,
+ nsIPresentationService::ROLE_CONTROLLER) {}
+
+ nsresult Init(nsIPresentationControlChannel* aControlChannel) override;
+
+ nsresult Reconnect(nsIPresentationServiceCallback* aCallback);
+
+ nsresult BuildTransport();
+
+ private:
+ ~PresentationControllingInfo() { Shutdown(NS_OK); }
+
+ void Shutdown(nsresult aReason) override;
+
+ nsresult GetAddress();
+
+ nsresult OnGetAddress(const nsACString& aAddress);
+
+ nsresult ContinueReconnect();
+
+ nsresult NotifyReconnectResult(nsresult aStatus);
+
+ nsCOMPtr<nsIServerSocket> mServerSocket;
+ nsCOMPtr<nsIPresentationServiceCallback> mReconnectCallback;
+ bool mIsReconnecting = false;
+ bool mDoReconnectAfterClose = false;
+};
+
+// Session info with presenting browsing context (receiver side) behaviors.
+class PresentationPresentingInfo final : public PresentationSessionInfo,
+ public PromiseNativeHandler,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPRESENTATIONCONTROLCHANNELLISTENER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ PresentationPresentingInfo(const nsAString& aUrl, const nsAString& aSessionId,
+ nsIPresentationDevice* aDevice)
+ : PresentationSessionInfo(aUrl, aSessionId,
+ nsIPresentationService::ROLE_RECEIVER) {
+ MOZ_ASSERT(aDevice);
+ SetDevice(aDevice);
+ }
+
+ nsresult Init(nsIPresentationControlChannel* aControlChannel) override;
+
+ nsresult NotifyResponderReady();
+ nsresult NotifyResponderFailure();
+
+ NS_IMETHODIMP OnSessionTransport(
+ nsIPresentationSessionTransport* transport) override;
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override;
+
+ void SetPromise(Promise* aPromise) {
+ mPromise = aPromise;
+ mPromise->AppendNativeHandler(this);
+ }
+
+ bool IsAccessible(base::ProcessId aProcessId) override;
+
+ nsresult DoReconnect();
+
+ private:
+ ~PresentationPresentingInfo() { Shutdown(NS_OK); }
+
+ void Shutdown(nsresult aReason) override;
+
+ nsresult InitTransportAndSendAnswer();
+
+ nsresult UntrackFromService() override;
+
+ NS_IMETHODIMP
+ FlushPendingEvents(
+ nsIPresentationDataChannelSessionTransportBuilder* builder);
+
+ bool mHasFlushPendingEvents = false;
+ RefPtr<PresentationResponderLoadingCallback> mLoadingCallback;
+ nsCOMPtr<nsITimer> mTimer;
+ nsCOMPtr<nsIPresentationChannelDescription> mRequesterDescription;
+ nsTArray<nsString> mPendingCandidates;
+ RefPtr<Promise> mPromise;
+
+ // The content parent communicating with the content process which the OOP
+ // receiver page belongs to.
+ RefPtr<ContentParent> mContentParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationSessionInfo_h
diff --git a/dom/presentation/PresentationSessionRequest.cpp b/dom/presentation/PresentationSessionRequest.cpp
new file mode 100644
index 0000000000..fafc66140a
--- /dev/null
+++ b/dom/presentation/PresentationSessionRequest.cpp
@@ -0,0 +1,66 @@
+/* -*- 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 "PresentationSessionRequest.h"
+
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationDevice.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationSessionRequest, nsIPresentationSessionRequest)
+
+PresentationSessionRequest::PresentationSessionRequest(
+ nsIPresentationDevice* aDevice, const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel)
+ : mUrl(aUrl),
+ mPresentationId(aPresentationId),
+ mDevice(aDevice),
+ mControlChannel(aControlChannel) {}
+
+PresentationSessionRequest::~PresentationSessionRequest() = default;
+
+// nsIPresentationSessionRequest
+
+NS_IMETHODIMP
+PresentationSessionRequest::GetDevice(nsIPresentationDevice** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsCOMPtr<nsIPresentationDevice> device = mDevice;
+ device.forget(aRetVal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionRequest::GetUrl(nsAString& aRetVal) {
+ aRetVal = mUrl;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionRequest::GetPresentationId(nsAString& aRetVal) {
+ aRetVal = mPresentationId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionRequest::GetControlChannel(
+ nsIPresentationControlChannel** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel;
+ controlChannel.forget(aRetVal);
+
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationSessionRequest.h b/dom/presentation/PresentationSessionRequest.h
new file mode 100644
index 0000000000..f90a7f160d
--- /dev/null
+++ b/dom/presentation/PresentationSessionRequest.h
@@ -0,0 +1,39 @@
+/* -*- 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_PresentationSessionRequest_h__
+#define mozilla_dom_PresentationSessionRequest_h__
+
+#include "nsIPresentationSessionRequest.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationSessionRequest final : public nsIPresentationSessionRequest {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONREQUEST
+
+ PresentationSessionRequest(nsIPresentationDevice* aDevice,
+ const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel);
+
+ private:
+ virtual ~PresentationSessionRequest();
+
+ nsString mUrl;
+ nsString mPresentationId;
+ nsCOMPtr<nsIPresentationDevice> mDevice;
+ nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_PresentationSessionRequest_h__ */
diff --git a/dom/presentation/PresentationTCPSessionTransport.cpp b/dom/presentation/PresentationTCPSessionTransport.cpp
new file mode 100644
index 0000000000..3dbe8fceab
--- /dev/null
+++ b/dom/presentation/PresentationTCPSessionTransport.cpp
@@ -0,0 +1,561 @@
+/* -*- 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 "mozilla/Logging.h"
+#include "nsArrayUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIAsyncStreamCopier.h"
+#include "nsIInputStreamPump.h"
+#include "nsIMultiplexInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationService.h"
+#include "nsIScriptableInputStream.h"
+#include "nsISocketTransport.h"
+#include "nsISocketTransportService.h"
+#include "nsIStringStream.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsThreadUtils.h"
+#include "PresentationLog.h"
+#include "PresentationTCPSessionTransport.h"
+
+#define BUFFER_SIZE 65536
+
+namespace mozilla {
+namespace dom {
+
+class CopierCallbacks final : public nsIRequestObserver {
+ public:
+ explicit CopierCallbacks(PresentationTCPSessionTransport* aTransport)
+ : mOwner(aTransport) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ private:
+ ~CopierCallbacks() = default;
+
+ RefPtr<PresentationTCPSessionTransport> mOwner;
+};
+
+NS_IMPL_ISUPPORTS(CopierCallbacks, nsIRequestObserver)
+
+NS_IMETHODIMP
+CopierCallbacks::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+CopierCallbacks::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ mOwner->NotifyCopyComplete(aStatus);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION(PresentationTCPSessionTransport, mTransport,
+ mSocketInputStream, mSocketOutputStream,
+ mInputStreamPump, mInputStreamScriptable, mCallback)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PresentationTCPSessionTransport)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PresentationTCPSessionTransport)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PresentationTCPSessionTransport)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPresentationSessionTransport)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationSessionTransport)
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationSessionTransportBuilder)
+ NS_INTERFACE_MAP_ENTRY(nsIPresentationTCPSessionTransportBuilder)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsITransportEventSink)
+NS_INTERFACE_MAP_END
+
+PresentationTCPSessionTransport::PresentationTCPSessionTransport()
+ : mReadyState(ReadyState::CLOSED),
+ mAsyncCopierActive(false),
+ mCloseStatus(NS_OK),
+ mDataNotificationEnabled(false) {}
+
+PresentationTCPSessionTransport::~PresentationTCPSessionTransport() = default;
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::BuildTCPSenderTransport(
+ nsISocketTransport* aTransport,
+ nsIPresentationSessionTransportBuilderListener* aListener) {
+ if (NS_WARN_IF(!aTransport)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mTransport = aTransport;
+
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mListener = aListener;
+
+ nsresult rv = CreateStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mRole = nsIPresentationService::ROLE_CONTROLLER;
+
+ nsCOMPtr<nsIPresentationSessionTransport> sessionTransport =
+ do_QueryObject(this);
+ nsCOMPtr<nsIRunnable> onSessionTransportRunnable =
+ NewRunnableMethod<nsIPresentationSessionTransport*>(
+ "nsIPresentationSessionTransportBuilderListener::OnSessionTransport",
+ mListener,
+ &nsIPresentationSessionTransportBuilderListener::OnSessionTransport,
+ sessionTransport);
+
+ NS_DispatchToCurrentThread(onSessionTransportRunnable.forget());
+
+ nsCOMPtr<nsIRunnable> setReadyStateRunnable = NewRunnableMethod<ReadyState>(
+ "dom::PresentationTCPSessionTransport::SetReadyState", this,
+ &PresentationTCPSessionTransport::SetReadyState, ReadyState::OPEN);
+ return NS_DispatchToCurrentThread(setReadyStateRunnable.forget());
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::BuildTCPReceiverTransport(
+ nsIPresentationChannelDescription* aDescription,
+ nsIPresentationSessionTransportBuilderListener* aListener) {
+ if (NS_WARN_IF(!aDescription)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mListener = aListener;
+
+ uint16_t serverPort;
+ nsresult rv = aDescription->GetTcpPort(&serverPort);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIArray> serverHosts;
+ rv = aDescription->GetTcpAddress(getter_AddRefs(serverHosts));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // TODO bug 1228504 Take all IP addresses in PresentationChannelDescription
+ // into account. And at the first stage Presentation API is only exposed on
+ // Firefox OS where the first IP appears enough for most scenarios.
+ nsCOMPtr<nsISupportsCString> supportStr = do_QueryElementAt(serverHosts, 0);
+ if (NS_WARN_IF(!supportStr)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString serverHost;
+ supportStr->GetData(serverHost);
+ if (serverHost.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ PRES_DEBUG("%s:ServerHost[%s],ServerPort[%d]\n", __func__, serverHost.get(),
+ serverPort);
+
+ SetReadyState(ReadyState::CONNECTING);
+
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!sts)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ rv = sts->CreateTransport(nsTArray<nsCString>(), serverHost, serverPort,
+ nullptr, getter_AddRefs(mTransport));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadEventTarget();
+ mTransport->SetEventSink(this, mainTarget);
+
+ rv = CreateStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mRole = nsIPresentationService::ROLE_RECEIVER;
+
+ nsCOMPtr<nsIPresentationSessionTransport> sessionTransport =
+ do_QueryObject(this);
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod<nsIPresentationSessionTransport*>(
+ "nsIPresentationSessionTransportBuilderListener::OnSessionTransport",
+ mListener,
+ &nsIPresentationSessionTransportBuilderListener::OnSessionTransport,
+ sessionTransport);
+ return NS_DispatchToCurrentThread(runnable.forget());
+}
+
+nsresult PresentationTCPSessionTransport::CreateStream() {
+ nsresult rv =
+ mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(mSocketInputStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = mTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0,
+ getter_AddRefs(mSocketOutputStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // If the other side is not listening, we will get an |onInputStreamReady|
+ // callback where |available| raises to indicate the connection was refused.
+ nsCOMPtr<nsIAsyncInputStream> asyncStream =
+ do_QueryInterface(mSocketInputStream);
+ if (NS_WARN_IF(!asyncStream)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadEventTarget();
+ rv = asyncStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0,
+ mainTarget);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInputStreamScriptable =
+ do_CreateInstance("@mozilla.org/scriptableinputstream;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = mInputStreamScriptable->Init(mSocketInputStream);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult PresentationTCPSessionTransport::CreateInputStreamPump() {
+ if (NS_WARN_IF(mInputStreamPump)) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ mInputStreamPump = do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mInputStreamPump->Init(mSocketInputStream, 0, 0, false, nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mInputStreamPump->AsyncRead(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::EnableDataNotification() {
+ if (NS_WARN_IF(!mCallback)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ if (mDataNotificationEnabled) {
+ return NS_OK;
+ }
+
+ mDataNotificationEnabled = true;
+
+ if (IsReadyToNotifyData()) {
+ return CreateInputStreamPump();
+ }
+
+ return NS_OK;
+}
+
+// nsIPresentationSessionTransportBuilderListener
+NS_IMETHODIMP
+PresentationTCPSessionTransport::GetCallback(
+ nsIPresentationSessionTransportCallback** aCallback) {
+ nsCOMPtr<nsIPresentationSessionTransportCallback> callback = mCallback;
+ callback.forget(aCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::SetCallback(
+ nsIPresentationSessionTransportCallback* aCallback) {
+ mCallback = aCallback;
+
+ if (!!mCallback && ReadyState::OPEN == mReadyState) {
+ // Notify the transport channel is ready.
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifyTransportReady()));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::GetSelfAddress(nsINetAddr** aSelfAddress) {
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ return mTransport->GetScriptableSelfAddr(aSelfAddress);
+}
+
+nsresult PresentationTCPSessionTransport::EnsureCopying() {
+ if (mAsyncCopierActive) {
+ return NS_OK;
+ }
+
+ mAsyncCopierActive = true;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMultiplexInputStream> multiplexStream =
+ do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> stream = do_QueryInterface(multiplexStream);
+
+ while (!mPendingData.IsEmpty()) {
+ nsCOMPtr<nsIInputStream> stream = mPendingData[0];
+ multiplexStream->AppendStream(stream);
+ mPendingData.RemoveElementAt(0);
+ }
+
+ nsCOMPtr<nsIAsyncStreamCopier> copier =
+ do_CreateInstance("@mozilla.org/network/async-stream-copier;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService("@mozilla.org/network/socket-transport-service;1");
+
+ nsCOMPtr<nsIEventTarget> target = do_QueryInterface(sts);
+ rv = copier->Init(stream, mSocketOutputStream, target,
+ true, /* source buffered */
+ false, /* sink buffered */
+ BUFFER_SIZE, false, /* close source */
+ false); /* close sink */
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<CopierCallbacks> callbacks = new CopierCallbacks(this);
+ rv = copier->AsyncCopy(callbacks, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void PresentationTCPSessionTransport::NotifyCopyComplete(nsresult aStatus) {
+ mAsyncCopierActive = false;
+
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ if (mReadyState != ReadyState::CLOSED) {
+ mCloseStatus = aStatus;
+ SetReadyState(ReadyState::CLOSED);
+ }
+ return;
+ }
+
+ if (!mPendingData.IsEmpty()) {
+ EnsureCopying();
+ return;
+ }
+
+ if (mReadyState == ReadyState::CLOSING) {
+ mSocketOutputStream->Close();
+ mCloseStatus = NS_OK;
+ SetReadyState(ReadyState::CLOSED);
+ }
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::Send(const nsAString& aData) {
+ if (NS_WARN_IF(mReadyState != ReadyState::OPEN)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIStringInputStream> stream =
+ do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ NS_ConvertUTF16toUTF8 msgString(aData);
+ rv = stream->SetData(msgString.BeginReading(), msgString.Length());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ mPendingData.AppendElement(stream);
+
+ EnsureCopying();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::SendBinaryMsg(const nsACString& aData) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::SendBlob(Blob* aBlob) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::Close(nsresult aReason) {
+ PRES_DEBUG("%s:reason[%" PRIx32 "]\n", __func__,
+ static_cast<uint32_t>(aReason));
+
+ if (mReadyState == ReadyState::CLOSED || mReadyState == ReadyState::CLOSING) {
+ return NS_OK;
+ }
+
+ mCloseStatus = aReason;
+ SetReadyState(ReadyState::CLOSING);
+
+ if (!mAsyncCopierActive) {
+ mPendingData.Clear();
+ mSocketOutputStream->Close();
+ }
+
+ mSocketInputStream->Close();
+ mDataNotificationEnabled = false;
+
+ mListener = nullptr;
+
+ return NS_OK;
+}
+
+void PresentationTCPSessionTransport::SetReadyState(ReadyState aReadyState) {
+ mReadyState = aReadyState;
+
+ if (mReadyState == ReadyState::OPEN) {
+ if (IsReadyToNotifyData()) {
+ CreateInputStreamPump();
+ }
+
+ if (NS_WARN_IF(!mCallback)) {
+ return;
+ }
+
+ // Notify the transport channel is ready.
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifyTransportReady()));
+ } else if (mReadyState == ReadyState::CLOSED && mCallback) {
+ if (NS_WARN_IF(!mCallback)) {
+ return;
+ }
+
+ // Notify the transport channel has been shut down.
+ Unused << NS_WARN_IF(
+ NS_FAILED(mCallback->NotifyTransportClosed(mCloseStatus)));
+ mCallback = nullptr;
+ }
+}
+
+// nsITransportEventSink
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnTransportStatus(nsITransport* aTransport,
+ nsresult aStatus,
+ int64_t aProgress,
+ int64_t aProgressMax) {
+ PRES_DEBUG("%s:aStatus[%" PRIx32 "]\n", __func__,
+ static_cast<uint32_t>(aStatus));
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aStatus != NS_NET_STATUS_CONNECTED_TO) {
+ return NS_OK;
+ }
+
+ SetReadyState(ReadyState::OPEN);
+
+ return NS_OK;
+}
+
+// nsIInputStreamCallback
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnInputStreamReady(
+ nsIAsyncInputStream* aStream) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Only used for detecting if the connection was refused.
+ uint64_t dummy;
+ nsresult rv = aStream->Available(&dummy);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ if (mReadyState != ReadyState::CLOSED) {
+ mCloseStatus = NS_ERROR_CONNECTION_REFUSED;
+ SetReadyState(ReadyState::CLOSED);
+ }
+ }
+
+ return NS_OK;
+}
+
+// nsIRequestObserver
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnStartRequest(nsIRequest* aRequest) {
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ PRES_DEBUG("%s:aStatusCode[%" PRIx32 "]\n", __func__,
+ static_cast<uint32_t>(aStatusCode));
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mInputStreamPump = nullptr;
+
+ if (mAsyncCopierActive && NS_SUCCEEDED(aStatusCode)) {
+ // If we have some buffered output still, and status is not an error, the
+ // other side has done a half-close, but we don't want to be in the close
+ // state until we are done sending everything that was buffered. We also
+ // don't want to call |NotifyTransportClosed| yet.
+ return NS_OK;
+ }
+
+ // We call this even if there is no error.
+ if (mReadyState != ReadyState::CLOSED) {
+ mCloseStatus = aStatusCode;
+ SetReadyState(ReadyState::CLOSED);
+ }
+ return NS_OK;
+}
+
+// nsIStreamListener
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aOffset,
+ uint32_t aCount) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mCallback)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCString data;
+ nsresult rv = mInputStreamScriptable->ReadBytes(aCount, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Pass the incoming data to the listener.
+ return mCallback->NotifyData(data, false);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationTCPSessionTransport.h b/dom/presentation/PresentationTCPSessionTransport.h
new file mode 100644
index 0000000000..c8662d4852
--- /dev/null
+++ b/dom/presentation/PresentationTCPSessionTransport.h
@@ -0,0 +1,104 @@
+/* -*- 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_PresentationSessionTransport_h
+#define mozilla_dom_PresentationSessionTransport_h
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIPresentationSessionTransport.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+#include "nsIStreamListener.h"
+#include "nsISupportsImpl.h"
+#include "nsITransport.h"
+
+class nsISocketTransport;
+class nsIInputStreamPump;
+class nsIScriptableInputStream;
+class nsIMultiplexInputStream;
+class nsIAsyncStreamCopier;
+class nsIInputStream;
+
+namespace mozilla {
+namespace dom {
+
+/*
+ * App-to-App transport channel for the presentation session. It's usually
+ * initialized with an |InitWithSocketTransport| call if at the presenting
+ * sender side; whereas it's initialized with an |InitWithChannelDescription| if
+ * at the presenting receiver side. The lifetime is managed in either
+ * |PresentationControllingInfo| (sender side) or |PresentationPresentingInfo|
+ * (receiver side) in PresentationSessionInfo.cpp.
+ */
+class PresentationTCPSessionTransport final
+ : public nsIPresentationSessionTransport,
+ public nsIPresentationTCPSessionTransportBuilder,
+ public nsITransportEventSink,
+ public nsIInputStreamCallback,
+ public nsIStreamListener {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PresentationTCPSessionTransport,
+ nsIPresentationSessionTransport)
+
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORT
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTBUILDER
+ NS_DECL_NSIPRESENTATIONTCPSESSIONTRANSPORTBUILDER
+ NS_DECL_NSITRANSPORTEVENTSINK
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ PresentationTCPSessionTransport();
+
+ void NotifyCopyComplete(nsresult aStatus);
+
+ private:
+ ~PresentationTCPSessionTransport();
+
+ nsresult CreateStream();
+
+ nsresult CreateInputStreamPump();
+
+ nsresult EnsureCopying();
+
+ enum class ReadyState { CONNECTING, OPEN, CLOSING, CLOSED };
+
+ void SetReadyState(ReadyState aReadyState);
+
+ bool IsReadyToNotifyData() {
+ return mDataNotificationEnabled && mReadyState == ReadyState::OPEN;
+ }
+
+ ReadyState mReadyState;
+ bool mAsyncCopierActive;
+ nsresult mCloseStatus;
+ bool mDataNotificationEnabled;
+
+ uint8_t mRole = 0;
+
+ // Raw socket streams
+ nsCOMPtr<nsISocketTransport> mTransport;
+ nsCOMPtr<nsIInputStream> mSocketInputStream;
+ nsCOMPtr<nsIOutputStream> mSocketOutputStream;
+
+ // Input stream machinery
+ nsCOMPtr<nsIInputStreamPump> mInputStreamPump;
+ nsCOMPtr<nsIScriptableInputStream> mInputStreamScriptable;
+
+ nsCOMPtr<nsIPresentationSessionTransportCallback> mCallback;
+ nsCOMPtr<nsIPresentationSessionTransportBuilderListener> mListener;
+
+ // The data to be sent.
+ nsTArray<nsCOMPtr<nsIInputStream>> mPendingData;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationSessionTransport_h
diff --git a/dom/presentation/PresentationTerminateRequest.cpp b/dom/presentation/PresentationTerminateRequest.cpp
new file mode 100644
index 0000000000..0a641dd095
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.cpp
@@ -0,0 +1,63 @@
+/* -*- 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() = default;
+
+// 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
index 0000000000..bd6e29fb78
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.h
@@ -0,0 +1,40 @@
+/* -*- 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/PresentationTransportBuilderConstructor.cpp b/dom/presentation/PresentationTransportBuilderConstructor.cpp
new file mode 100644
index 0000000000..7ceaf167cd
--- /dev/null
+++ b/dom/presentation/PresentationTransportBuilderConstructor.cpp
@@ -0,0 +1,78 @@
+/* -*- 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 "PresentationTransportBuilderConstructor.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationSessionTransport.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(DummyPresentationTransportBuilderConstructor,
+ nsIPresentationTransportBuilderConstructor)
+
+NS_IMETHODIMP
+DummyPresentationTransportBuilderConstructor::CreateTransportBuilder(
+ uint8_t aType, nsIPresentationSessionTransportBuilder** aRetval) {
+ MOZ_ASSERT(false, "Unexpected to be invoked.");
+ return NS_OK;
+}
+
+/* static */
+already_AddRefed<nsIPresentationTransportBuilderConstructor>
+PresentationTransportBuilderConstructor::Create() {
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> constructor;
+ if (XRE_IsContentProcess()) {
+ constructor = new DummyPresentationTransportBuilderConstructor();
+ } else {
+ constructor = new PresentationTransportBuilderConstructor();
+ }
+
+ return constructor.forget();
+}
+
+NS_IMETHODIMP
+PresentationTransportBuilderConstructor::CreateTransportBuilder(
+ uint8_t aType, nsIPresentationSessionTransportBuilder** aRetval) {
+ if (NS_WARN_IF(!aRetval)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aRetval = nullptr;
+
+ if (NS_WARN_IF(aType != nsIPresentationChannelDescription::TYPE_TCP &&
+ aType !=
+ nsIPresentationChannelDescription::TYPE_DATACHANNEL)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (XRE_IsContentProcess()) {
+ MOZ_ASSERT(false,
+ "CreateTransportBuilder can only be invoked in parent process.");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPresentationSessionTransportBuilder> builder;
+ if (aType == nsIPresentationChannelDescription::TYPE_TCP) {
+ builder = do_CreateInstance(PRESENTATION_TCP_SESSION_TRANSPORT_CONTRACTID);
+ } else {
+ builder = do_CreateInstance(
+ "@mozilla.org/presentation/datachanneltransportbuilder;1");
+ }
+
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_DOM_OPERATION_ERR;
+ }
+
+ builder.forget(aRetval);
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationTransportBuilderConstructor.h b/dom/presentation/PresentationTransportBuilderConstructor.h
new file mode 100644
index 0000000000..c57542e201
--- /dev/null
+++ b/dom/presentation/PresentationTransportBuilderConstructor.h
@@ -0,0 +1,47 @@
+/* -*- 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_PresentationTransportBuilderConstructor_h
+#define mozilla_dom_PresentationTransportBuilderConstructor_h
+
+#include "nsCOMPtr.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace dom {
+
+class DummyPresentationTransportBuilderConstructor
+ : public nsIPresentationTransportBuilderConstructor {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONTRANSPORTBUILDERCONSTRUCTOR
+
+ DummyPresentationTransportBuilderConstructor() = default;
+
+ protected:
+ virtual ~DummyPresentationTransportBuilderConstructor() = default;
+};
+
+class PresentationTransportBuilderConstructor final
+ : public DummyPresentationTransportBuilderConstructor {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(
+ PresentationTransportBuilderConstructor,
+ DummyPresentationTransportBuilderConstructor)
+ NS_DECL_NSIPRESENTATIONTRANSPORTBUILDERCONSTRUCTOR
+
+ static already_AddRefed<nsIPresentationTransportBuilderConstructor> Create();
+
+ private:
+ PresentationTransportBuilderConstructor() = default;
+ virtual ~PresentationTransportBuilderConstructor() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationTransportBuilderConstructor_h
diff --git a/dom/presentation/components.conf b/dom/presentation/components.conf
new file mode 100644
index 0000000000..c71ad39ef4
--- /dev/null
+++ b/dom/presentation/components.conf
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{a2b5ef38-a650-4bee-b488-3739f1f3afe7}',
+ 'contract_ids': ['@mozilla.org/presentation/mockedsockettransport;1'],
+ 'headers': ['/dom/presentation/MockedSocketTransport.h'],
+ 'type': 'mozilla::dom::MockedSocketTransport',
+ },
+ {
+ 'cid': '{dd2bbf2f-3399-4389-8f5f-d382afb8b2d6}',
+ 'contract_ids': ['@mozilla.org/presentation/datachanneltransport;1'],
+ 'jsm': 'resource://gre/modules/PresentationDataChannelSessionTransport.jsm',
+ 'constructor': 'PresentationTransport',
+ },
+ {
+ 'cid': '{215b2f62-46e2-4004-a3d1-6858e56c20f3}',
+ 'contract_ids': ['@mozilla.org/presentation/datachanneltransportbuilder;1'],
+ 'jsm': 'resource://gre/modules/PresentationDataChannelSessionTransport.jsm',
+ 'constructor': 'PresentationTransportBuilder',
+ },
+]
+
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'android':
+ Classes += [
+ {
+ 'cid': '{5fb96caa-6d49-4f6b-9a4b-65dd0d51f92d}',
+ 'contract_ids': ['@mozilla.org/presentation-device/networkHelper;1'],
+ 'jsm': 'resource://gre/modules/PresentationNetworkHelper.jsm',
+ 'constructor': 'PresentationNetworkHelper',
+ },
+ ]
diff --git a/dom/presentation/interfaces/moz.build b/dom/presentation/interfaces/moz.build
new file mode 100644
index 0000000000..bc09f5679f
--- /dev/null
+++ b/dom/presentation/interfaces/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsIPresentationControlChannel.idl",
+ "nsIPresentationControlService.idl",
+ "nsIPresentationDevice.idl",
+ "nsIPresentationDeviceManager.idl",
+ "nsIPresentationDevicePrompt.idl",
+ "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
new file mode 100644
index 0000000000..2b44fbd412
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -0,0 +1,139 @@
+/* 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 nsIArray;
+interface nsIInputStream;
+
+[scriptable, uuid(ae318e05-2a4e-4f85-95c0-e8b191ad812c)]
+interface nsIPresentationChannelDescription: nsISupports
+{
+ const unsigned short TYPE_TCP = 1;
+ const unsigned short TYPE_DATACHANNEL = 2;
+
+ // Type of transport channel.
+ readonly attribute uint8_t type;
+
+ // Addresses for TCP channel (as a list of nsISupportsCString).
+ // Should only be used while type == TYPE_TCP.
+ readonly attribute nsIArray tcpAddress;
+
+ // Port number for TCP channel.
+ // Should only be used while type == TYPE_TCP.
+ readonly attribute uint16_t tcpPort;
+
+ // SDP for Data Channel.
+ // Should only be used while type == TYPE_DATACHANNEL.
+ readonly attribute AString dataChannelSDP;
+};
+
+/*
+ * The callbacks for events on control channel.
+ */
+[scriptable, uuid(96dd548f-7d0f-43c1-b1ad-28e666cf1e82)]
+interface nsIPresentationControlChannelListener: nsISupports
+{
+ /*
+ * Callback for receiving offer from remote endpoint.
+ * @param offer The received offer.
+ */
+ void onOffer(in nsIPresentationChannelDescription offer);
+
+ /*
+ * Callback for receiving answer from remote endpoint.
+ * @param answer The received answer.
+ */
+ void onAnswer(in nsIPresentationChannelDescription answer);
+
+ /*
+ * Callback for receiving ICE candidate from remote endpoint.
+ * @param answer The received answer.
+ */
+ void onIceCandidate(in AString candidate);
+
+ /*
+ * The callback for notifying channel connected. This should be async called
+ * after nsIPresentationDevice::establishControlChannel.
+ */
+ void notifyConnected();
+
+ /*
+ * The callback for notifying channel disconnected.
+ * @param reason The reason of channel close, NS_OK represents normal close.
+ */
+ void notifyDisconnected(in nsresult reason);
+
+ /*
+ * The callback for notifying the reconnect command is acknowledged.
+ */
+ void notifyReconnected();
+};
+
+/*
+ * The control channel for establishing RTCPeerConnection for a presentation
+ * session. SDP Offer/Answer will be exchanged through this interface. The
+ * control channel should be in-order.
+ */
+[scriptable, uuid(e60e208c-a9f5-4bc6-9a3e-47f3e4ae9c57)]
+interface nsIPresentationControlChannel: nsISupports
+{
+ // The listener for handling events of this control channel.
+ // All the events should be pending until listener is assigned.
+ attribute nsIPresentationControlChannelListener listener;
+
+ /*
+ * Send offer to remote endpoint. |onOffer| should be invoked on remote
+ * endpoint.
+ * @param offer The offer to send.
+ * @throws NS_ERROR_FAILURE on failure
+ */
+ void sendOffer(in nsIPresentationChannelDescription offer);
+
+ /*
+ * Send answer to remote endpoint. |onAnswer| should be invoked on remote
+ * endpoint.
+ * @param answer The answer to send.
+ * @throws NS_ERROR_FAILURE on failure
+ */
+ void sendAnswer(in nsIPresentationChannelDescription answer);
+
+ /*
+ * 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 AString candidate);
+
+ /*
+ * 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 AString presentationId, in AString url);
+
+ /*
+ * Terminate a presentation on remote endpoint.
+ * @param presentationId The Id for representing this session.
+ * @throws NS_ERROR_FAILURE on failure
+ */
+ void terminate(in AString presentationId);
+
+ /*
+ * Disconnect the control channel.
+ * @param reason The reason of disconnecting channel; NS_OK represents normal.
+ */
+ void disconnect(in nsresult reason);
+
+ /*
+ * Reconnect a presentation on remote endpoint.
+ * Note that only controller is allowed to reconnect a session.
+ * @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 reconnect(in AString presentationId, in AString url);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationControlService.idl b/dom/presentation/interfaces/nsIPresentationControlService.idl
new file mode 100644
index 0000000000..6602b49776
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationControlService.idl
@@ -0,0 +1,156 @@
+/* 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 nsIPresentationControlChannel;
+
+%{C++
+#define PRESENTATION_CONTROL_SERVICE_CONTACT_ID \
+ "@mozilla.org/presentation/control-service;1"
+%}
+
+/*
+ * The device information required for establishing control channel.
+ */
+[scriptable, uuid(296fd171-e4d0-4de0-99ff-ad8ed52ddef3)]
+interface nsITCPDeviceInfo: nsISupports
+{
+ readonly attribute AUTF8String id;
+ readonly attribute AUTF8String address;
+ readonly attribute uint16_t port;
+ // SHA-256 fingerprint of server certificate. Empty string represents
+ // server doesn't support TLS or not available.
+ readonly attribute AUTF8String certFingerprint;
+};
+
+[scriptable, uuid(09bddfaf-fcc2-4dc9-b33e-a509a1c2fb6d)]
+interface nsIPresentationControlServerListener: nsISupports
+{
+ /**
+ * Callback while the server is ready or restarted.
+ * @param aPort
+ * The port of the server socket.
+ * @param aCertFingerprint
+ * The SHA-256 fingerprint of TLS server certificate.
+ * Empty string represents server started without encryption.
+ */
+ void onServerReady(in uint16_t aPort, in AUTF8String aCertFingerprint);
+
+ /**
+ * Callback while the server is stopped or fails to start.
+ * @param aResult
+ * The error cause of server stopped or the reason of
+ * start failure.
+ * NS_OK means the server is stopped by close.
+ */
+ void onServerStopped(in nsresult aResult);
+
+ /**
+ * Callback while the remote host is requesting to start a presentation session.
+ * @param aDeviceInfo The device information related to the remote host.
+ * @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 AString aUrl,
+ in AString 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 AString aPresentationId,
+ in nsIPresentationControlChannel aControlChannel,
+ in boolean aIsFromReceiver);
+
+ /**
+ * Callback while the remote host is requesting to reconnect a presentation session.
+ * @param aDeviceInfo The device information related to the remote host.
+ * @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 onReconnectRequest(in nsITCPDeviceInfo aDeviceInfo,
+ in AString url,
+ in AString aPresentationId,
+ in nsIPresentationControlChannel aControlChannel);
+};
+
+/**
+ * Presentation control service which can be used for both presentation
+ * control client and server.
+ */
+[scriptable, uuid(55d6b605-2389-4aae-a8fe-60d4440540ea)]
+interface nsIPresentationControlService: nsISupports
+{
+ /**
+ * This method initializes server socket. Caller should set listener and
+ * monitor onServerReady event to get the correct server info.
+ * @param aEncrypted
+ * True for using TLS control channel.
+ * @param aPort
+ * The port of the server socket. Pass 0 or opt-out to indicate no
+ * preference, and a port will be selected automatically.
+ * @throws NS_ERROR_FAILURE if the server socket has been inited or the
+ * server socket can not be inited.
+ */
+ void startServer(in boolean aEncrypted, [optional] in uint16_t aPort);
+
+ /**
+ * Request connection to designated remote presentation control receiver.
+ * @param aDeviceInfo
+ * The remtoe device info for establish connection.
+ * @returns The control channel for this session.
+ * @throws NS_ERROR_FAILURE if the Id hasn't been inited.
+ */
+ 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);
+
+ /**
+ * Close server socket and call |listener.onClose(NS_OK)|
+ */
+ void close();
+
+ /**
+ * Get the listen port of the TCP socket, valid after the server is ready.
+ * 0 indicates the server socket is not ready or is closed.
+ */
+ readonly attribute uint16_t port;
+
+ /**
+ * The protocol version implemented by this server.
+ */
+ readonly attribute uint32_t version;
+
+ /**
+ * The id of the TCP presentation server. |requestSession| won't
+ * work until the |id| is set.
+ */
+ attribute AUTF8String id;
+
+ /**
+ * The fingerprint of the TLS server certificate.
+ * Empty string indicates the server is not ready or not encrypted.
+ */
+ attribute AUTF8String certFingerprint;
+
+ /**
+ * The listener for handling events of this presentation control server.
+ * Listener must be provided before invoke |startServer| and |close|.
+ */
+ attribute nsIPresentationControlServerListener listener;
+};
diff --git a/dom/presentation/interfaces/nsIPresentationDevice.idl b/dom/presentation/interfaces/nsIPresentationDevice.idl
new file mode 100644
index 0000000000..e70820270a
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationDevice.idl
@@ -0,0 +1,43 @@
+/* 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 nsIPresentationControlChannel;
+
+/*
+ * Remote device.
+ */
+[scriptable, uuid(b1e0a7af-5936-4066-8f2e-f789fb9a7e8f)]
+interface nsIPresentationDevice : nsISupports
+{
+ // The unique Id for the device. UUID is recommanded.
+ readonly attribute AUTF8String id;
+
+ // The human-readable name of this device.
+ 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.
+ * @returns The control channel for this session.
+ * @throws NS_ERROR_FAILURE if the establishment fails
+ */
+ nsIPresentationControlChannel establishControlChannel();
+
+ // Do something when presentation session is disconnected.
+ void disconnect();
+
+ /*
+ * Query if requested presentation URL is supported.
+ * @params requestedUrl the designated URL for a presentation request.
+ * @returns true if designated URL is supported.
+ */
+ boolean isRequestedUrlSupported(in AString requestedUrl);
+};
+
+
diff --git a/dom/presentation/interfaces/nsIPresentationDeviceManager.idl b/dom/presentation/interfaces/nsIPresentationDeviceManager.idl
new file mode 100644
index 0000000000..adff9fc09e
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationDeviceManager.idl
@@ -0,0 +1,51 @@
+/* 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 nsIArray;
+interface nsIPresentationDeviceProvider;
+
+%{C++
+#define PRESENTATION_DEVICE_MANAGER_CONTRACTID "@mozilla.org/presentation-device/manager;1"
+#define PRESENTATION_DEVICE_CHANGE_TOPIC "presentation-device-change"
+%}
+
+/*
+ * Manager for the device availability. User can observe "presentation-device-change"
+ * for any update of the available devices.
+ */
+[scriptable, uuid(beb61db5-3d5f-454f-a15a-dbfa0337c569)]
+interface nsIPresentationDeviceManager : nsISupports
+{
+ // true if there is any device available.
+ readonly attribute boolean deviceAvailable;
+
+ /*
+ * Register a device provider manually.
+ * @param provider The device provider to add.
+ */
+ void addDeviceProvider(in nsIPresentationDeviceProvider provider);
+
+ /*
+ * Unregister a device provider manually.
+ * @param provider The device provider to remove.
+ */
+ void removeDeviceProvider(in nsIPresentationDeviceProvider provider);
+
+ /*
+ * Force all registered device providers to update device information.
+ */
+ void forceDiscovery();
+
+ /*
+ * Retrieve all available devices or all available devices that supports
+ * designated presentation URLs, return a list of nsIPresentationDevice.
+ * The returned list is a cached device list and could be out-of-date.
+ * Observe device change events to get following updates.
+ * @param presentationUrls the target presentation URLs for device filtering
+ */
+ nsIArray getAvailableDevices([optional] in nsIArray presentationUrls);
+};
+
diff --git a/dom/presentation/interfaces/nsIPresentationDevicePrompt.idl b/dom/presentation/interfaces/nsIPresentationDevicePrompt.idl
new file mode 100644
index 0000000000..9128b1cc75
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationDevicePrompt.idl
@@ -0,0 +1,59 @@
+/* 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 nsIArray;
+interface nsIPresentationDevice;
+interface nsIPrincipal;
+
+webidl EventTarget;
+
+%{C++
+#define PRESENTATION_DEVICE_PROMPT_CONTRACTID "@mozilla.org/presentation-device/prompt;1"
+%}
+
+/*
+ * The information and callbacks for device selection
+ */
+[scriptable, uuid(b2aa7f6a-9448-446a-bba4-9c29638b0ed4)]
+interface nsIPresentationDeviceRequest : nsISupports
+{
+ // The origin which initiate the request.
+ readonly attribute AString origin;
+
+ // The array of candidate URLs.
+ readonly attribute nsIArray requestURLs;
+
+ // The XUL browser element that the request was originated in.
+ readonly attribute EventTarget chromeEventHandler;
+
+ // The principal of the request.
+ readonly attribute nsIPrincipal principal;
+
+ /*
+ * Callback after selecting a device
+ * @param device The selected device.
+ */
+ void select(in nsIPresentationDevice device);
+
+ /*
+ * Callback after selection failed or canceled by user.
+ * @param reason The error cause for canceling this request.
+ */
+ void cancel(in nsresult reason);
+};
+
+/*
+ * UI prompt for device selection.
+ */
+[scriptable, uuid(ac1a7e44-de86-454f-a9f1-276de2539831)]
+interface nsIPresentationDevicePrompt : nsISupports
+{
+ /*
+ * Request a device selection.
+ * @param request The information and callbacks of this selection request.
+ */
+ void promptDeviceSelection(in nsIPresentationDeviceRequest request);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
new file mode 100644
index 0000000000..15d09241ef
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
@@ -0,0 +1,75 @@
+/* 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_DEVICE_PROVIDER_CATEGORY "presentation-device-provider"
+%}
+
+/*
+ * The callbacks for any device updates and session request.
+ */
+[scriptable, uuid(46fd372b-2e40-4179-9b36-0478d141e440)]
+interface nsIPresentationDeviceListener: nsISupports
+{
+ void addDevice(in nsIPresentationDevice device);
+ void removeDevice(in nsIPresentationDevice device);
+ void updateDevice(in nsIPresentationDevice device);
+
+ /*
+ * Callback while the remote device is requesting to start a presentation session.
+ * @param device The remote device that sent session request.
+ * @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 AString url,
+ in AString 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 AString presentationId,
+ in nsIPresentationControlChannel controlChannel,
+ in boolean aIsFromReceiver);
+
+ /*
+ * Callback while the remote device is requesting to reconnect a presentation session.
+ * @param device The remote device that sent session request.
+ * @param aUrl 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 onReconnectRequest(in nsIPresentationDevice device,
+ in AString url,
+ in AString presentationId,
+ in nsIPresentationControlChannel controlChannel);
+};
+
+/*
+ * 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
+{
+ // The listener for handling any device update.
+ attribute nsIPresentationDeviceListener listener;
+
+ /*
+ * Force to update device information.
+ */
+ void forceDiscovery();
+};
diff --git a/dom/presentation/interfaces/nsIPresentationListener.idl b/dom/presentation/interfaces/nsIPresentationListener.idl
new file mode 100644
index 0000000000..609ab27c78
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationListener.idl
@@ -0,0 +1,54 @@
+/* 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"
+
+%{C++
+#include "nsTArray.h"
+%}
+
+[ref] native URLArrayRef(const nsTArray<nsString>);
+
+[uuid(0105f837-4279-4715-9d5b-2dc3f8b65353)]
+interface nsIPresentationAvailabilityListener : nsISupports
+{
+ /*
+ * Called when device availability changes.
+ */
+ [noscript] void notifyAvailableChange(in URLArrayRef urls,
+ in bool available);
+};
+
+[scriptable, uuid(7dd48df8-8f8c-48c7-ac37-7b9fd1acf2f8)]
+interface nsIPresentationSessionListener : nsISupports
+{
+ const unsigned short STATE_CONNECTING = 0;
+ const unsigned short STATE_CONNECTED = 1;
+ const unsigned short STATE_CLOSED = 2;
+ const unsigned short STATE_TERMINATED = 3;
+
+ /*
+ * Called when session state changes.
+ */
+ void notifyStateChange(in AString sessionId,
+ in unsigned short state,
+ in nsresult reason);
+
+ /*
+ * Called when receive messages.
+ */
+ void notifyMessage(in AString sessionId,
+ in ACString data,
+ in boolean isBinary);
+};
+
+[scriptable, uuid(27f101d7-9ed1-429e-b4f8-43b00e8e111c)]
+interface nsIPresentationRespondingListener : nsISupports
+{
+ /*
+ * Called when an incoming session connects.
+ */
+ void notifySessionConnect(in unsigned long long windowId,
+ in AString sessionId);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationLocalDevice.idl b/dom/presentation/interfaces/nsIPresentationLocalDevice.idl
new file mode 100644
index 0000000000..80e3b4041f
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationLocalDevice.idl
@@ -0,0 +1,17 @@
+/* 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 "nsIPresentationDevice.idl"
+
+/*
+ * Local device.
+ * This device is used for 1-UA use case. The result for display is rendered by
+ * this host device.
+ */
+[scriptable, uuid(dd239720-cab6-4fb5-9025-cba23f1bbc2d)]
+interface nsIPresentationLocalDevice : nsIPresentationDevice
+{
+ // (1-UA only) The property is used to get the window ID of 1-UA device.
+ readonly attribute AUTF8String windowId;
+};
diff --git a/dom/presentation/interfaces/nsIPresentationNetworkHelper.idl b/dom/presentation/interfaces/nsIPresentationNetworkHelper.idl
new file mode 100644
index 0000000000..514075dfab
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationNetworkHelper.idl
@@ -0,0 +1,36 @@
+/* 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"
+
+%{C++
+#define PRESENTATION_NETWORK_HELPER_CONTRACTID \
+ "@mozilla.org/presentation-device/networkHelper;1"
+%}
+
+[scriptable, uuid(0a7e134f-ff80-4e73-91e6-12b3134fe568)]
+interface nsIPresentationNetworkHelperListener : nsISupports
+{
+ /**
+ * Called when error occurs.
+ * @param aReason error message.
+ */
+ void onError(in AUTF8String aReason);
+
+ /**
+ * Called when get Wi-Fi IP address.
+ * @param aIPAddress the IP address of Wi-Fi interface.
+ */
+ void onGetWifiIPAddress(in AUTF8String aIPAddress);
+};
+
+[scriptable, uuid(650dc16b-3d9c-49a6-9037-1d6f2d18c90c)]
+interface nsIPresentationNetworkHelper : nsISupports
+{
+ /**
+ * Get IP address of Wi-Fi interface.
+ * @param aListener the callback interface.
+ */
+ void getWifiIPAddress(in nsIPresentationNetworkHelperListener aListener);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationRequestUIGlue.idl b/dom/presentation/interfaces/nsIPresentationRequestUIGlue.idl
new file mode 100644
index 0000000000..162ffb0ad8
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationRequestUIGlue.idl
@@ -0,0 +1,29 @@
+/* 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;
+
+%{C++
+#define PRESENTATION_REQUEST_UI_GLUE_CONTRACTID \
+ "@mozilla.org/presentation/requestuiglue;1"
+%}
+
+[scriptable, uuid(faa45119-6fb5-496c-aa4c-f740177a38b5)]
+interface nsIPresentationRequestUIGlue : nsISupports
+{
+ /*
+ * This method is called to open the responding app/page when
+ * a presentation request comes in at receiver side.
+ *
+ * @param url The url of the request.
+ * @param sessionId The session ID of the request.
+ *
+ * @return A promise that resolves to the opening frame.
+ */
+ Promise sendRequest(in AString url,
+ in AString sessionId,
+ in nsIPresentationDevice device);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationService.idl b/dom/presentation/interfaces/nsIPresentationService.idl
new file mode 100644
index 0000000000..f1bf7ef9c2
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationService.idl
@@ -0,0 +1,276 @@
+/* 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 nsIInputStream;
+interface nsIPresentationAvailabilityListener;
+interface nsIPresentationRespondingListener;
+interface nsIPresentationSessionListener;
+interface nsIPresentationTransportBuilderConstructor;
+interface nsIPrincipal;
+
+webidl Blob;
+webidl EventTarget;
+
+%{C++
+#define PRESENTATION_SERVICE_CID \
+ { 0x1d9bb10c, 0xc0ab, 0x4fe8, \
+ { 0x9e, 0x4f, 0x40, 0x58, 0xb8, 0x51, 0x98, 0x32 } }
+#define PRESENTATION_SERVICE_CONTRACTID \
+ "@mozilla.org/presentation/presentationservice;1"
+
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+%}
+
+[ref] native URLArrayRef(const nsTArray<nsString>);
+
+[scriptable, uuid(12073206-0065-4b10-9488-a6eb9b23e65b)]
+interface nsIPresentationServiceCallback : nsISupports
+{
+ /*
+ * Called when the operation succeeds.
+ *
+ * @param url: the selected request url used to start or reconnect a session.
+ */
+ void notifySuccess(in AString url);
+
+ /*
+ * Called when the operation fails.
+ *
+ * @param error: error message.
+ */
+ void notifyError(in nsresult error);
+};
+
+[scriptable, uuid(de42b741-5619-4650-b961-c2cebb572c95)]
+interface nsIPresentationService : nsISupports
+{
+ const unsigned short ROLE_CONTROLLER = 0x1;
+ const unsigned short ROLE_RECEIVER = 0x2;
+
+ const unsigned short CLOSED_REASON_ERROR = 0x1;
+ const unsigned short CLOSED_REASON_CLOSED = 0x2;
+ const unsigned short CLOSED_REASON_WENTAWAY = 0x3;
+
+ /*
+ * Start a new presentation session and display a prompt box which asks users
+ * to select a device.
+ *
+ * @param urls: The candidate Urls of presenting page. Only one url would be used.
+ * @param sessionId: An ID to identify presentation session.
+ * @param origin: The url of requesting page.
+ * @param deviceId: The specified device of handling this request, null string
+ * for prompt device selection dialog.
+ * @param windowId: The inner window ID associated with the presentation
+ * session. (0 implies no window ID since no actual window
+ * uses 0 as its ID. Generally it's the case the window is
+ * located in different process from this service)
+ * @param eventTarget: The chrome event handler, in particular XUL browser
+ * element in parent process, that the request was
+ * originated in.
+ * @param principal: The principal that initiated the session.
+ * @param callback: Invoke the callback when the operation is completed.
+ * NotifySuccess() is called with |id| if a session is
+ * established successfully with the selected device.
+ * Otherwise, NotifyError() is called with a error message.
+ * @param constructor: The constructor for creating a transport builder.
+ */
+ [noscript] void startSession(in URLArrayRef urls,
+ in AString sessionId,
+ in AString origin,
+ in AString deviceId,
+ in unsigned long long windowId,
+ in EventTarget eventTarget,
+ in nsIPrincipal principal,
+ in nsIPresentationServiceCallback callback,
+ in nsIPresentationTransportBuilderConstructor constructor);
+
+ /*
+ * Send the message to the session.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param data: the message being sent out.
+ */
+ void sendSessionMessage(in AString sessionId,
+ in uint8_t role,
+ in AString data);
+
+ /*
+ * Send the binary message to the session.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param data: the message being sent out.
+ */
+ void sendSessionBinaryMsg(in AString sessionId,
+ in uint8_t role,
+ in ACString data);
+
+ /*
+ * Send the blob to the session.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param blob: The input blob to be sent.
+ */
+ void sendSessionBlob(in AString sessionId,
+ in uint8_t role,
+ in Blob blob);
+
+ /*
+ * Close the session.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ void closeSession(in AString sessionId,
+ in uint8_t role,
+ in uint8_t closedReason);
+
+ /*
+ * Terminate the session.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ void terminateSession(in AString sessionId,
+ in uint8_t role);
+
+ /*
+ * Reconnect the session.
+ *
+ * @param url: The request Urls.
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param callback: NotifySuccess() is called when a control channel
+ * is opened successfully.
+ * Otherwise, NotifyError() is called with a error message.
+ */
+ [noscript] void reconnectSession(in URLArrayRef urls,
+ in AString sessionId,
+ in uint8_t role,
+ in nsIPresentationServiceCallback callback);
+
+ /*
+ * Register an availability listener. Must be called from the main thread.
+ *
+ * @param availabilityUrls: The Urls that this listener is interested in.
+ * @param listener: The listener to register.
+ */
+ [noscript] void registerAvailabilityListener(
+ in URLArrayRef availabilityUrls,
+ in nsIPresentationAvailabilityListener listener);
+
+ /*
+ * Unregister an availability listener. Must be called from the main thread.
+ *
+ * @param availabilityUrls: The Urls that are registered before.
+ * @param listener: The listener to unregister.
+ */
+ [noscript] void unregisterAvailabilityListener(
+ in URLArrayRef availabilityUrls,
+ in nsIPresentationAvailabilityListener listener);
+
+ /*
+ * Register a session listener. Must be called from the main thread.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param listener: The listener to register.
+ */
+ void registerSessionListener(in AString sessionId,
+ in uint8_t role,
+ in nsIPresentationSessionListener listener);
+
+ /*
+ * Unregister a session listener. Must be called from the main thread.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ void unregisterSessionListener(in AString sessionId,
+ in uint8_t role);
+
+ /*
+ * Register a responding listener. Must be called from the main thread.
+ *
+ * @param windowId: The window ID associated with the listener.
+ * @param listener: The listener to register.
+ */
+ void registerRespondingListener(in unsigned long long windowId,
+ in nsIPresentationRespondingListener listener);
+
+ /*
+ * Unregister a responding listener. Must be called from the main thread.
+ * @param windowId: The window ID associated with the listener.
+ */
+ void unregisterRespondingListener(in unsigned long long windowId);
+
+ /*
+ * Notify the receiver page is ready for presentation use.
+ *
+ * @param sessionId An ID to identify presentation session.
+ * @param windowId The inner window ID associated with the presentation
+ * session.
+ * @param isLoading true if receiver page is loading successfully.
+ * @param constructor: The constructor for creating a transport builder.
+ */
+ void notifyReceiverReady(in AString sessionId,
+ in unsigned long long windowId,
+ in boolean isLoading,
+ in nsIPresentationTransportBuilderConstructor constructor);
+
+ /*
+ * Notify the transport is closed
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param reason: the error message. NS_OK indicates it is closed normally.
+ */
+ void NotifyTransportClosed(in AString sessionId,
+ in uint8_t role,
+ in nsresult reason);
+
+ /*
+ * Untrack the relevant info about the presentation session if there's any.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ void untrackSessionInfo(in AString sessionId, in uint8_t role);
+
+ /*
+ * The windowId for building RTCDataChannel session transport
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ unsigned long long getWindowIdBySessionId(in AString sessionId,
+ in uint8_t role);
+
+ /*
+ * Update the mapping of the session ID and window ID.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ * @param windowId: The inner window ID associated with the presentation
+ * session.
+ */
+ void updateWindowIdBySessionId(in AString sessionId,
+ in uint8_t role,
+ in unsigned long long windowId);
+
+ /*
+ * To build the session transport.
+ * NOTE: This function should be only called at controller side.
+ *
+ * @param sessionId: An ID to identify presentation session.
+ * @param role: Identify the function called by controller or receiver.
+ */
+ void buildTransport(in AString sessionId, in uint8_t role);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationSessionRequest.idl b/dom/presentation/interfaces/nsIPresentationSessionRequest.idl
new file mode 100644
index 0000000000..a145aaeed7
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationSessionRequest.idl
@@ -0,0 +1,35 @@
+/* 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_SESSION_REQUEST_TOPIC "presentation-session-request"
+#define PRESENTATION_RECONNECT_REQUEST_TOPIC "presentation-reconnect-request"
+%}
+
+/*
+ * The event of a device requesting for starting or reconnecting
+ * a presentation session. User can monitor the session request
+ * on every device by observing "presentation-sesion-request" for a
+ * new session and "presentation-reconnect-request" for reconnecting.
+ */
+[scriptable, uuid(d808a084-d0f8-455a-a8df-5879e05a755b)]
+interface nsIPresentationSessionRequest: nsISupports
+{
+ // The device which requesting the presentation session.
+ readonly attribute nsIPresentationDevice device;
+
+ // The URL requested to open by remote device.
+ readonly attribute AString url;
+
+ // The Id for representing this session.
+ readonly attribute AString presentationId;
+
+ // The control channel for this session.
+ readonly attribute nsIPresentationControlChannel controlChannel;
+};
diff --git a/dom/presentation/interfaces/nsIPresentationSessionTransport.idl b/dom/presentation/interfaces/nsIPresentationSessionTransport.idl
new file mode 100644
index 0000000000..f5b7187b21
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationSessionTransport.idl
@@ -0,0 +1,70 @@
+/* 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 nsIInputStream;
+interface nsINetAddr;
+
+webidl Blob;
+
+%{C++
+#define PRESENTATION_TCP_SESSION_TRANSPORT_CONTRACTID \
+ "@mozilla.org/presentation/presentationtcpsessiontransport;1"
+%}
+
+/*
+ * The callback for session transport events.
+ */
+[scriptable, uuid(9f158786-41a6-4a10-b29b-9497f25d4b67)]
+interface nsIPresentationSessionTransportCallback : nsISupports
+{
+ void notifyTransportReady();
+ void notifyTransportClosed(in nsresult reason);
+ void notifyData(in ACString data, in boolean isBinary);
+};
+
+/*
+ * App-to-App transport channel for the presentation session.
+ */
+[scriptable, uuid(670b7e1b-65be-42b6-a596-be571907fa18)]
+interface nsIPresentationSessionTransport : nsISupports
+{
+ // Should be set once the underlying session transport is built
+ attribute nsIPresentationSessionTransportCallback callback;
+
+ // valid for TCP session transport
+ readonly attribute nsINetAddr selfAddress;
+
+ /*
+ * Enable the notification for incoming data. |notifyData| of
+ * |nsIPresentationSessionTransportCallback| can start getting invoked.
+ * Should set callback before |enableDataNotification| is called.
+ */
+ void enableDataNotification();
+
+ /*
+ * Send message to the remote endpoint.
+ * @param data The message to send.
+ */
+ void send(in AString data);
+
+ /*
+ * Send the binary message to the remote endpoint.
+ * @param data: the message being sent out.
+ */
+ void sendBinaryMsg(in ACString data);
+
+ /*
+ * Send the blob to the remote endpoint.
+ * @param blob: The input blob to be sent.
+ */
+ void sendBlob(in Blob blob);
+
+ /*
+ * Close this session transport.
+ * @param reason The reason for closing this session transport.
+ */
+ void close(in nsresult reason);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl b/dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl
new file mode 100644
index 0000000000..4b9d46db98
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl
@@ -0,0 +1,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 "nsISupports.idl"
+
+interface nsIPresentationChannelDescription;
+interface nsISocketTransport;
+interface mozIDOMWindow;
+interface nsIPresentationControlChannel;
+interface nsIPresentationSessionTransport;
+
+[scriptable, uuid(673f6de1-e253-41b8-9be8-b7ff161fa8dc)]
+interface nsIPresentationSessionTransportBuilderListener : nsISupports
+{
+ // Should set |transport.callback| in |onSessionTransport|.
+ void onSessionTransport(in nsIPresentationSessionTransport transport);
+ void onError(in nsresult reason);
+
+ void sendOffer(in nsIPresentationChannelDescription offer);
+ void sendAnswer(in nsIPresentationChannelDescription answer);
+ void sendIceCandidate(in AString candidate);
+ void close(in nsresult reason);
+};
+
+[scriptable, uuid(2fdbe67d-80f9-48dc-8237-5bef8fa19801)]
+interface nsIPresentationSessionTransportBuilder : nsISupports
+{
+};
+
+/**
+ * The constructor for creating a transport builder.
+ */
+[scriptable, uuid(706482b2-1b51-4bed-a21d-785a9cfcfac7)]
+interface nsIPresentationTransportBuilderConstructor : nsISupports
+{
+ nsIPresentationSessionTransportBuilder createTransportBuilder(in uint8_t type);
+};
+
+/**
+ * Builder for TCP session transport
+ */
+[scriptable, uuid(cde36d6e-f471-4262-a70d-f932a26b21d9)]
+interface nsIPresentationTCPSessionTransportBuilder : nsIPresentationSessionTransportBuilder
+{
+ /**
+ * The following creation functions will trigger |listener.onSessionTransport|
+ * if the session transport is successfully built, |listener.onError| if some
+ * error occurs during building session transport.
+ */
+ void buildTCPSenderTransport(in nsISocketTransport aTransport,
+ in nsIPresentationSessionTransportBuilderListener aListener);
+
+ void buildTCPReceiverTransport(in nsIPresentationChannelDescription aDescription,
+ in nsIPresentationSessionTransportBuilderListener aListener);
+};
+
+/**
+ * Builder for WebRTC data channel session transport
+ */
+[scriptable, uuid(8131c4e0-3a8c-4bc1-a92a-8431473d2fe8)]
+interface nsIPresentationDataChannelSessionTransportBuilder : nsIPresentationSessionTransportBuilder
+{
+ /**
+ * The following creation function will trigger |listener.onSessionTransport|
+ * if the session transport is successfully built, |listener.onError| if some
+ * error occurs during creating session transport. The |notifyConnected| of
+ * |aControlChannel| should be called before calling
+ * |buildDataChannelTransport|.
+ */
+ void buildDataChannelTransport(in uint8_t aRole,
+ in mozIDOMWindow aWindow,
+ in nsIPresentationSessionTransportBuilderListener aListener);
+
+ // Bug 1275150 - Merge TCP builder with the following APIs
+ void onOffer(in nsIPresentationChannelDescription offer);
+ void onAnswer(in nsIPresentationChannelDescription answer);
+ void onIceCandidate(in AString candidate);
+ void notifyDisconnected(in nsresult reason);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
new file mode 100644
index 0000000000..cbfd486e9e
--- /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 AString 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/ipc/PPresentation.ipdl b/dom/presentation/ipc/PPresentation.ipdl
new file mode 100644
index 0000000000..bfaca8fb57
--- /dev/null
+++ b/dom/presentation/ipc/PPresentation.ipdl
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* 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 protocol PContent;
+include protocol PPresentationRequest;
+include protocol PPresentationBuilder;
+
+include InputStreamParams;
+
+include "mozilla/dom/PermissionMessageUtils.h";
+
+using refcounted class nsIPrincipal from "nsIPrincipal.h";
+using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
+
+namespace mozilla {
+namespace dom {
+
+struct StartSessionRequest
+{
+ nsString[] urls;
+ nsString sessionId;
+ nsString origin;
+ nsString deviceId;
+ uint64_t windowId;
+ TabId tabId;
+ nsIPrincipal principal;
+};
+
+struct SendSessionMessageRequest
+{
+ nsString sessionId;
+ uint8_t role;
+ nsString data;
+};
+
+struct CloseSessionRequest
+{
+ nsString sessionId;
+ uint8_t role;
+ uint8_t closedReason;
+};
+
+struct TerminateSessionRequest
+{
+ nsString sessionId;
+ uint8_t role;
+};
+
+struct ReconnectSessionRequest
+{
+ nsString[] urls;
+ nsString sessionId;
+ uint8_t role;
+};
+
+struct BuildTransportRequest
+{
+ nsString sessionId;
+ uint8_t role;
+};
+
+union PresentationIPCRequest
+{
+ StartSessionRequest;
+ SendSessionMessageRequest;
+ CloseSessionRequest;
+ TerminateSessionRequest;
+ ReconnectSessionRequest;
+ BuildTransportRequest;
+};
+
+sync protocol PPresentation
+{
+ manager PContent;
+ manages PPresentationBuilder;
+ manages PPresentationRequest;
+
+child:
+ async NotifyAvailableChange(nsString[] aAvailabilityUrls,
+ bool aAvailable);
+ async NotifySessionStateChange(nsString aSessionId,
+ uint16_t aState,
+ nsresult aReason);
+ async NotifyMessage(nsString aSessionId, nsCString aData, bool aIsBinary);
+ async NotifySessionConnect(uint64_t aWindowId, nsString aSessionId);
+ async NotifyCloseSessionTransport(nsString aSessionId,
+ uint8_t aRole,
+ nsresult aReason);
+
+ async PPresentationBuilder(nsString aSessionId, uint8_t aRole);
+
+parent:
+ async __delete__();
+
+ async RegisterAvailabilityHandler(nsString[] aAvailabilityUrls);
+ async UnregisterAvailabilityHandler(nsString[] aAvailabilityUrls);
+
+ async RegisterSessionHandler(nsString aSessionId, uint8_t aRole);
+ async UnregisterSessionHandler(nsString aSessionId, uint8_t aRole);
+
+ async RegisterRespondingHandler(uint64_t aWindowId);
+ async UnregisterRespondingHandler(uint64_t aWindowId);
+
+ async PPresentationRequest(PresentationIPCRequest aRequest);
+
+ async NotifyReceiverReady(nsString aSessionId, uint64_t aWindowId, bool aIsLoading);
+ async NotifyTransportClosed(nsString aSessionId, uint8_t aRole, nsresult aReason);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PPresentationBuilder.ipdl b/dom/presentation/ipc/PPresentationBuilder.ipdl
new file mode 100644
index 0000000000..e32b02e8f3
--- /dev/null
+++ b/dom/presentation/ipc/PPresentationBuilder.ipdl
@@ -0,0 +1,34 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PPresentation;
+
+namespace mozilla {
+namespace dom {
+
+async protocol PPresentationBuilder
+{
+ manager PPresentation;
+
+parent:
+ async SendOffer(nsString aSDP);
+ async SendAnswer(nsString aSDP);
+ async SendIceCandidate(nsString aCandidate);
+ async Close(nsresult aReason);
+
+ async OnSessionTransport();
+ async OnSessionTransportError(nsresult aReason);
+
+child:
+ async OnOffer(nsString aSDP);
+ async OnAnswer(nsString aSDP);
+ async OnIceCandidate(nsString aCandidate);
+
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PPresentationRequest.ipdl b/dom/presentation/ipc/PPresentationRequest.ipdl
new file mode 100644
index 0000000000..fa99dfcabb
--- /dev/null
+++ b/dom/presentation/ipc/PPresentationRequest.ipdl
@@ -0,0 +1,22 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PPresentation;
+
+namespace mozilla {
+namespace dom {
+
+sync protocol PPresentationRequest
+{
+ manager PPresentation;
+
+child:
+ async __delete__(nsresult result);
+ async NotifyRequestUrlSelected(nsString aUrl);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationBuilderChild.cpp b/dom/presentation/ipc/PresentationBuilderChild.cpp
new file mode 100644
index 0000000000..d4e1120392
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderChild.cpp
@@ -0,0 +1,172 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "nsComponentManagerUtils.h"
+#include "nsGlobalWindow.h"
+#include "PresentationBuilderChild.h"
+#include "PresentationIPCService.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationBuilderChild,
+ nsIPresentationSessionTransportBuilderListener)
+
+PresentationBuilderChild::PresentationBuilderChild(const nsString& aSessionId,
+ uint8_t aRole)
+ : mSessionId(aSessionId), mRole(aRole) {}
+
+nsresult PresentationBuilderChild::Init() {
+ mBuilder = do_CreateInstance(
+ "@mozilla.org/presentation/datachanneltransportbuilder;1");
+ if (NS_WARN_IF(!mBuilder)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ uint64_t windowId = 0;
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(
+ service->GetWindowIdBySessionId(mSessionId, mRole, &windowId)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsPIDOMWindowInner* window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowId);
+ if (NS_WARN_IF(!window)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mBuilder->BuildDataChannelTransport(mRole, window, this);
+}
+
+void PresentationBuilderChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mBuilder = nullptr;
+ mActorDestroyed = true;
+}
+
+mozilla::ipc::IPCResult PresentationBuilderChild::RecvOnOffer(
+ const nsString& aSDP) {
+ if (NS_WARN_IF(!mBuilder)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+
+ if (NS_WARN_IF(NS_FAILED(mBuilder->OnOffer(description)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderChild::RecvOnAnswer(
+ const nsString& aSDP) {
+ if (NS_WARN_IF(!mBuilder)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+
+ if (NS_WARN_IF(NS_FAILED(mBuilder->OnAnswer(description)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderChild::RecvOnIceCandidate(
+ const nsString& aCandidate) {
+ if (NS_WARN_IF(mBuilder && NS_FAILED(mBuilder->OnIceCandidate(aCandidate)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+// nsPresentationSessionTransportBuilderListener
+NS_IMETHODIMP
+PresentationBuilderChild::OnSessionTransport(
+ nsIPresentationSessionTransport* aTransport) {
+ if (NS_WARN_IF(mActorDestroyed || !SendOnSessionTransport())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ NS_WARNING_ASSERTION(service, "no presentation service");
+ if (service) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(static_cast<PresentationIPCService*>(service.get())
+ ->NotifySessionTransport(mSessionId, mRole, aTransport)));
+ }
+ mBuilder = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderChild::OnError(nsresult reason) {
+ mBuilder = nullptr;
+
+ if (NS_WARN_IF(mActorDestroyed || !SendOnSessionTransportError(reason))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderChild::SendOffer(nsIPresentationChannelDescription* aOffer) {
+ nsAutoString SDP;
+ nsresult rv = aOffer->GetDataChannelSDP(SDP);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(mActorDestroyed || !SendSendOffer(SDP))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderChild::SendAnswer(
+ nsIPresentationChannelDescription* aAnswer) {
+ nsAutoString SDP;
+ nsresult rv = aAnswer->GetDataChannelSDP(SDP);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(mActorDestroyed || !SendSendAnswer(SDP))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderChild::SendIceCandidate(const nsAString& candidate) {
+ if (NS_WARN_IF(mActorDestroyed ||
+ !SendSendIceCandidate(nsString(candidate)))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderChild::Close(nsresult reason) {
+ if (NS_WARN_IF(mActorDestroyed || !SendClose(reason))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationBuilderChild.h b/dom/presentation/ipc/PresentationBuilderChild.h
new file mode 100644
index 0000000000..8da43245da
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderChild.h
@@ -0,0 +1,47 @@
+/* -*- 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_PresentationBuilderChild_h
+#define mozilla_dom_PresentationBuilderChild_h
+
+#include "mozilla/dom/PPresentationBuilderChild.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationBuilderChild final
+ : public PPresentationBuilderChild,
+ public nsIPresentationSessionTransportBuilderListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTBUILDERLISTENER
+
+ explicit PresentationBuilderChild(const nsString& aSessionId, uint8_t aRole);
+
+ nsresult Init();
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvOnOffer(const nsString& aSDP);
+
+ mozilla::ipc::IPCResult RecvOnAnswer(const nsString& aSDP);
+
+ mozilla::ipc::IPCResult RecvOnIceCandidate(const nsString& aCandidate);
+
+ private:
+ virtual ~PresentationBuilderChild() = default;
+
+ nsString mSessionId;
+ uint8_t mRole;
+ bool mActorDestroyed = false;
+ nsCOMPtr<nsIPresentationDataChannelSessionTransportBuilder> mBuilder;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationBuilderChild_h
diff --git a/dom/presentation/ipc/PresentationBuilderParent.cpp b/dom/presentation/ipc/PresentationBuilderParent.cpp
new file mode 100644
index 0000000000..cc451d7465
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderParent.cpp
@@ -0,0 +1,228 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "PresentationBuilderParent.h"
+#include "PresentationSessionInfo.h"
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+class PresentationSessionTransportIPC final
+ : public nsIPresentationSessionTransport {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORT
+
+ PresentationSessionTransportIPC(PresentationParent* aParent,
+ const nsAString& aSessionId, uint8_t aRole)
+ : mParent(aParent), mSessionId(aSessionId), mRole(aRole) {
+ MOZ_ASSERT(mParent);
+ }
+
+ private:
+ virtual ~PresentationSessionTransportIPC() = default;
+
+ RefPtr<PresentationParent> mParent;
+ nsString mSessionId;
+ uint8_t mRole;
+};
+
+NS_IMPL_ISUPPORTS(PresentationSessionTransportIPC,
+ nsIPresentationSessionTransport)
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::GetCallback(
+ nsIPresentationSessionTransportCallback** aCallback) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::SetCallback(
+ nsIPresentationSessionTransportCallback* aCallback) {
+ if (aCallback) {
+ aCallback->NotifyTransportReady();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::GetSelfAddress(nsINetAddr** aSelfAddress) {
+ MOZ_ASSERT(false, "Not expected.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::EnableDataNotification() { return NS_OK; }
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::Send(const nsAString& aData) { return NS_OK; }
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::SendBinaryMsg(const nsACString& aData) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::SendBlob(Blob* aBlob) { return NS_OK; }
+
+NS_IMETHODIMP
+PresentationSessionTransportIPC::Close(nsresult aReason) {
+ if (NS_WARN_IF(!mParent->SendNotifyCloseSessionTransport(mSessionId, mRole,
+ aReason))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+NS_IMPL_ISUPPORTS(PresentationBuilderParent,
+ nsIPresentationSessionTransportBuilder,
+ nsIPresentationDataChannelSessionTransportBuilder)
+
+PresentationBuilderParent::PresentationBuilderParent(
+ PresentationParent* aParent)
+ : mParent(aParent) {}
+
+PresentationBuilderParent::~PresentationBuilderParent() {
+ if (mNeedDestroyActor) {
+ Unused << NS_WARN_IF(!Send__delete__(this));
+ }
+}
+
+NS_IMETHODIMP
+PresentationBuilderParent::BuildDataChannelTransport(
+ uint8_t aRole, mozIDOMWindow* aWindow, /* unused */
+ nsIPresentationSessionTransportBuilderListener* aListener) {
+ mBuilderListener = aListener;
+
+ RefPtr<PresentationSessionInfo> info =
+ static_cast<PresentationSessionInfo*>(aListener);
+ nsAutoString sessionId(info->GetSessionId());
+ if (NS_WARN_IF(!mParent->SendPPresentationBuilderConstructor(this, sessionId,
+ aRole))) {
+ return NS_ERROR_FAILURE;
+ }
+ mIPCSessionTransport =
+ new PresentationSessionTransportIPC(mParent, sessionId, aRole);
+ mNeedDestroyActor = true;
+ mParent = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderParent::OnIceCandidate(const nsAString& aCandidate) {
+ if (NS_WARN_IF(!SendOnIceCandidate(nsString(aCandidate)))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderParent::OnOffer(
+ nsIPresentationChannelDescription* aDescription) {
+ nsAutoString SDP;
+ nsresult rv = aDescription->GetDataChannelSDP(SDP);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!SendOnOffer(SDP))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderParent::OnAnswer(
+ nsIPresentationChannelDescription* aDescription) {
+ nsAutoString SDP;
+ nsresult rv = aDescription->GetDataChannelSDP(SDP);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!SendOnAnswer(SDP))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationBuilderParent::NotifyDisconnected(nsresult aReason) {
+ return NS_OK;
+}
+
+void PresentationBuilderParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mNeedDestroyActor = false;
+ mParent = nullptr;
+ mBuilderListener = nullptr;
+}
+
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvSendOffer(
+ const nsString& aSDP) {
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendOffer(description)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvSendAnswer(
+ const nsString& aSDP) {
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendAnswer(description)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvSendIceCandidate(
+ const nsString& aCandidate) {
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendIceCandidate(aCandidate)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvClose(
+ const nsresult& aReason) {
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->Close(aReason)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+// Delegate to nsIPresentationSessionTransportBuilderListener
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvOnSessionTransport() {
+ RefPtr<PresentationBuilderParent> kungFuDeathGrip = this;
+ Unused << NS_WARN_IF(
+ !mBuilderListener ||
+ NS_FAILED(mBuilderListener->OnSessionTransport(mIPCSessionTransport)));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationBuilderParent::RecvOnSessionTransportError(
+ const nsresult& aReason) {
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->OnError(aReason)))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationBuilderParent.h b/dom/presentation/ipc/PresentationBuilderParent.h
new file mode 100644
index 0000000000..6f8e097c16
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderParent.h
@@ -0,0 +1,52 @@
+/* -*- 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_PresentationBuilderParent_h__
+#define mozilla_dom_PresentationBuilderParent_h__
+
+#include "mozilla/dom/PPresentationBuilderParent.h"
+#include "PresentationParent.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationBuilderParent final
+ : public PPresentationBuilderParent,
+ public nsIPresentationDataChannelSessionTransportBuilder {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTBUILDER
+ NS_DECL_NSIPRESENTATIONDATACHANNELSESSIONTRANSPORTBUILDER
+
+ explicit PresentationBuilderParent(PresentationParent* aParent);
+
+ mozilla::ipc::IPCResult RecvSendOffer(const nsString& aSDP);
+
+ mozilla::ipc::IPCResult RecvSendAnswer(const nsString& aSDP);
+
+ mozilla::ipc::IPCResult RecvSendIceCandidate(const nsString& aCandidate);
+
+ mozilla::ipc::IPCResult RecvClose(const nsresult& aReason);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvOnSessionTransport();
+
+ mozilla::ipc::IPCResult RecvOnSessionTransportError(const nsresult& aReason);
+
+ private:
+ virtual ~PresentationBuilderParent();
+ bool mNeedDestroyActor = false;
+ RefPtr<PresentationParent> mParent;
+ nsCOMPtr<nsIPresentationSessionTransportBuilderListener> mBuilderListener;
+ nsCOMPtr<nsIPresentationSessionTransport> mIPCSessionTransport;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationBuilderParent_h__
diff --git a/dom/presentation/ipc/PresentationChild.cpp b/dom/presentation/ipc/PresentationChild.cpp
new file mode 100644
index 0000000000..77737e2e5e
--- /dev/null
+++ b/dom/presentation/ipc/PresentationChild.cpp
@@ -0,0 +1,173 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "mozilla/StaticPtr.h"
+#include "PresentationBuilderChild.h"
+#include "PresentationChild.h"
+#include "PresentationIPCService.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+/*
+ * Implementation of PresentationChild
+ */
+
+PresentationChild::PresentationChild(PresentationIPCService* aService)
+ : mActorDestroyed(false), mService(aService) {
+ MOZ_ASSERT(mService);
+
+ MOZ_COUNT_CTOR(PresentationChild);
+}
+
+PresentationChild::~PresentationChild() {
+ MOZ_COUNT_DTOR(PresentationChild);
+
+ if (!mActorDestroyed) {
+ Send__delete__(this);
+ }
+ mService = nullptr;
+}
+
+void PresentationChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mActorDestroyed = true;
+ mService->NotifyPresentationChildDestroyed();
+ mService = nullptr;
+}
+
+PPresentationRequestChild* PresentationChild::AllocPPresentationRequestChild(
+ const PresentationIPCRequest& aRequest) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We should never be manually allocating "
+ "PPresentationRequestChild actors");
+ return nullptr;
+}
+
+bool PresentationChild::DeallocPPresentationRequestChild(
+ PPresentationRequestChild* aActor) {
+ delete aActor;
+ return true;
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvPPresentationBuilderConstructor(
+ PPresentationBuilderChild* aActor, const nsString& aSessionId,
+ const uint8_t& aRole) {
+ // Child will build the session transport
+ PresentationBuilderChild* actor =
+ static_cast<PresentationBuilderChild*>(aActor);
+ if (NS_WARN_IF(NS_FAILED(actor->Init()))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+PPresentationBuilderChild* PresentationChild::AllocPPresentationBuilderChild(
+ const nsString& aSessionId, const uint8_t& aRole) {
+ RefPtr<PresentationBuilderChild> actor =
+ new PresentationBuilderChild(aSessionId, aRole);
+
+ return actor.forget().take();
+}
+
+bool PresentationChild::DeallocPPresentationBuilderChild(
+ PPresentationBuilderChild* aActor) {
+ RefPtr<PresentationBuilderChild> actor =
+ dont_AddRef(static_cast<PresentationBuilderChild*>(aActor));
+ return true;
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvNotifyAvailableChange(
+ nsTArray<nsString>&& aAvailabilityUrls, const bool& aAvailable) {
+ if (mService) {
+ Unused << NS_WARN_IF(NS_FAILED(
+ mService->NotifyAvailableChange(aAvailabilityUrls, aAvailable)));
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvNotifySessionStateChange(
+ const nsString& aSessionId, const uint16_t& aState,
+ const nsresult& aReason) {
+ if (mService) {
+ Unused << NS_WARN_IF(NS_FAILED(
+ mService->NotifySessionStateChange(aSessionId, aState, aReason)));
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvNotifyMessage(
+ const nsString& aSessionId, const nsCString& aData, const bool& aIsBinary) {
+ if (mService) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->NotifyMessage(aSessionId, aData, aIsBinary)));
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvNotifySessionConnect(
+ const uint64_t& aWindowId, const nsString& aSessionId) {
+ if (mService) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->NotifySessionConnect(aWindowId, aSessionId)));
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationChild::RecvNotifyCloseSessionTransport(
+ const nsString& aSessionId, const uint8_t& aRole, const nsresult& aReason) {
+ if (mService) {
+ Unused << NS_WARN_IF(NS_FAILED(
+ mService->CloseContentSessionTransport(aSessionId, aRole, aReason)));
+ }
+ return IPC_OK();
+}
+
+/*
+ * Implementation of PresentationRequestChild
+ */
+
+PresentationRequestChild::PresentationRequestChild(
+ nsIPresentationServiceCallback* aCallback)
+ : mActorDestroyed(false), mCallback(aCallback) {
+ MOZ_COUNT_CTOR(PresentationRequestChild);
+}
+
+PresentationRequestChild::~PresentationRequestChild() {
+ MOZ_COUNT_DTOR(PresentationRequestChild);
+
+ mCallback = nullptr;
+}
+
+void PresentationRequestChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mActorDestroyed = true;
+ mCallback = nullptr;
+}
+
+mozilla::ipc::IPCResult PresentationRequestChild::Recv__delete__(
+ const nsresult& aResult) {
+ if (mActorDestroyed) {
+ return IPC_OK();
+ }
+
+ if (mCallback) {
+ if (NS_FAILED(aResult)) {
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifyError(aResult)));
+ }
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationRequestChild::RecvNotifyRequestUrlSelected(
+ const nsString& aUrl) {
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifySuccess(aUrl)));
+ return IPC_OK();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationChild.h b/dom/presentation/ipc/PresentationChild.h
new file mode 100644
index 0000000000..3c02353350
--- /dev/null
+++ b/dom/presentation/ipc/PresentationChild.h
@@ -0,0 +1,88 @@
+/* -*- 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_PresentationChild_h
+#define mozilla_dom_PresentationChild_h
+
+#include "mozilla/dom/PPresentationBuilderChild.h"
+#include "mozilla/dom/PPresentationChild.h"
+#include "mozilla/dom/PPresentationRequestChild.h"
+
+class nsIPresentationServiceCallback;
+
+namespace mozilla {
+namespace dom {
+
+class PresentationIPCService;
+
+class PresentationChild final : public PPresentationChild {
+ public:
+ explicit PresentationChild(PresentationIPCService* aService);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ PPresentationRequestChild* AllocPPresentationRequestChild(
+ const PresentationIPCRequest& aRequest);
+
+ bool DeallocPPresentationRequestChild(PPresentationRequestChild* aActor);
+
+ mozilla::ipc::IPCResult RecvPPresentationBuilderConstructor(
+ PPresentationBuilderChild* aActor, const nsString& aSessionId,
+ const uint8_t& aRole) override;
+
+ PPresentationBuilderChild* AllocPPresentationBuilderChild(
+ const nsString& aSessionId, const uint8_t& aRole);
+
+ bool DeallocPPresentationBuilderChild(PPresentationBuilderChild* aActor);
+
+ mozilla::ipc::IPCResult RecvNotifyAvailableChange(
+ nsTArray<nsString>&& aAvailabilityUrls, const bool& aAvailable);
+
+ mozilla::ipc::IPCResult RecvNotifySessionStateChange(
+ const nsString& aSessionId, const uint16_t& aState,
+ const nsresult& aReason);
+
+ mozilla::ipc::IPCResult RecvNotifyMessage(const nsString& aSessionId,
+ const nsCString& aData,
+ const bool& aIsBinary);
+
+ mozilla::ipc::IPCResult RecvNotifySessionConnect(const uint64_t& aWindowId,
+ const nsString& aSessionId);
+
+ mozilla::ipc::IPCResult RecvNotifyCloseSessionTransport(
+ const nsString& aSessionId, const uint8_t& aRole,
+ const nsresult& aReason);
+
+ private:
+ virtual ~PresentationChild();
+
+ bool mActorDestroyed = false;
+ RefPtr<PresentationIPCService> mService;
+};
+
+class PresentationRequestChild final : public PPresentationRequestChild {
+ friend class PresentationChild;
+
+ public:
+ explicit PresentationRequestChild(nsIPresentationServiceCallback* aCallback);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult Recv__delete__(const nsresult& aResult);
+
+ mozilla::ipc::IPCResult RecvNotifyRequestUrlSelected(const nsString& aUrl);
+
+ private:
+ virtual ~PresentationRequestChild();
+
+ bool mActorDestroyed = false;
+ nsCOMPtr<nsIPresentationServiceCallback> mCallback;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationChild_h
diff --git a/dom/presentation/ipc/PresentationContentSessionInfo.cpp b/dom/presentation/ipc/PresentationContentSessionInfo.cpp
new file mode 100644
index 0000000000..8190aedbf3
--- /dev/null
+++ b/dom/presentation/ipc/PresentationContentSessionInfo.cpp
@@ -0,0 +1,98 @@
+/* -*- 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 "nsServiceManagerUtils.h"
+#include "PresentationContentSessionInfo.h"
+#include "PresentationIPCService.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationContentSessionInfo,
+ nsIPresentationSessionTransportCallback);
+
+nsresult PresentationContentSessionInfo::Init() {
+ if (NS_WARN_IF(NS_FAILED(mTransport->SetCallback(this)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_WARN_IF(NS_FAILED(mTransport->EnableDataNotification()))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+nsresult PresentationContentSessionInfo::Send(const nsAString& aData) {
+ if (!mTransport) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->Send(aData);
+}
+
+nsresult PresentationContentSessionInfo::SendBinaryMsg(
+ const nsACString& aData) {
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->SendBinaryMsg(aData);
+}
+
+nsresult PresentationContentSessionInfo::SendBlob(Blob* aBlob) {
+ if (NS_WARN_IF(!mTransport)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->SendBlob(aBlob);
+}
+
+nsresult PresentationContentSessionInfo::Close(nsresult aReason) {
+ if (!mTransport) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return mTransport->Close(aReason);
+}
+
+// nsIPresentationSessionTransportCallback
+NS_IMETHODIMP
+PresentationContentSessionInfo::NotifyTransportReady() {
+ // do nothing since |onSessionTransport| implies this
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationContentSessionInfo::NotifyTransportClosed(nsresult aReason) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Nullify |mTransport| here so it won't try to re-close |mTransport| in
+ // potential subsequent |Shutdown| calls.
+ mTransport = nullptr;
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return static_cast<PresentationIPCService*>(service.get())
+ ->NotifyTransportClosed(mSessionId, mRole, aReason);
+}
+
+NS_IMETHODIMP
+PresentationContentSessionInfo::NotifyData(const nsACString& aData,
+ bool aIsBinary) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPresentationService> service =
+ do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!service)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return static_cast<PresentationIPCService*>(service.get())
+ ->NotifyMessage(mSessionId, aData, aIsBinary);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationContentSessionInfo.h b/dom/presentation/ipc/PresentationContentSessionInfo.h
new file mode 100644
index 0000000000..7f15da1859
--- /dev/null
+++ b/dom/presentation/ipc/PresentationContentSessionInfo.h
@@ -0,0 +1,62 @@
+/* -*- 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_PresentationContentSessionInfo_h
+#define mozilla_dom_PresentationContentSessionInfo_h
+
+#include "nsCOMPtr.h"
+#include "nsIPresentationSessionTransport.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "nsIPresentationService.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * PresentationContentSessionInfo manages nsIPresentationSessionTransport and
+ * delegates the callbacks to PresentationIPCService. Only lives in content
+ * process.
+ */
+class PresentationContentSessionInfo final
+ : public nsIPresentationSessionTransportCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSESSIONTRANSPORTCALLBACK
+
+ PresentationContentSessionInfo(const nsAString& aSessionId, uint8_t aRole,
+ nsIPresentationSessionTransport* aTransport)
+ : mSessionId(aSessionId), mRole(aRole), mTransport(aTransport) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ MOZ_ASSERT(aTransport);
+ }
+
+ nsresult Init();
+
+ nsresult Send(const nsAString& aData);
+
+ nsresult SendBinaryMsg(const nsACString& aData);
+
+ nsresult SendBlob(Blob* aBlob);
+
+ nsresult Close(nsresult aReason);
+
+ private:
+ virtual ~PresentationContentSessionInfo() = default;
+
+ nsString mSessionId;
+ uint8_t mRole;
+ nsCOMPtr<nsIPresentationSessionTransport> mTransport;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationContentSessionInfo_h
diff --git a/dom/presentation/ipc/PresentationIPCService.cpp b/dom/presentation/ipc/PresentationIPCService.cpp
new file mode 100644
index 0000000000..c99ce1c2ae
--- /dev/null
+++ b/dom/presentation/ipc/PresentationIPCService.cpp
@@ -0,0 +1,477 @@
+/* -*- 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 "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/PPresentation.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/ipc/InputStreamUtils.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsIPresentationListener.h"
+#include "PresentationCallbacks.h"
+#include "PresentationChild.h"
+#include "PresentationContentSessionInfo.h"
+#include "PresentationIPCService.h"
+#include "PresentationLog.h"
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+PresentationChild* sPresentationChild;
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(PresentationIPCService, nsIPresentationService,
+ nsIPresentationAvailabilityListener)
+
+PresentationIPCService::PresentationIPCService() {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ if (NS_WARN_IF(!contentChild || contentChild->IsShuttingDown())) {
+ return;
+ }
+ sPresentationChild = new PresentationChild(this);
+ Unused << NS_WARN_IF(
+ !contentChild->SendPPresentationConstructor(sPresentationChild));
+}
+
+/* virtual */
+PresentationIPCService::~PresentationIPCService() {
+ Shutdown();
+
+ mSessionListeners.Clear();
+ mSessionInfoAtController.Clear();
+ mSessionInfoAtReceiver.Clear();
+ sPresentationChild = nullptr;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::StartSession(
+ const nsTArray<nsString>& aUrls, const nsAString& aSessionId,
+ const nsAString& aOrigin, const nsAString& aDeviceId, uint64_t aWindowId,
+ EventTarget* aEventTarget, nsIPrincipal* aPrincipal,
+ nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor) {
+ if (aWindowId != 0) {
+ AddRespondingSessionId(aWindowId, aSessionId,
+ nsIPresentationService::ROLE_CONTROLLER);
+ }
+
+ nsPIDOMWindowInner* window =
+ nsGlobalWindowInner::GetInnerWindowWithId(aWindowId);
+ TabId tabId = BrowserParent::GetTabIdFrom(window->GetDocShell());
+
+ SendRequest(
+ aCallback,
+ StartSessionRequest(aUrls, nsString(aSessionId), nsString(aOrigin),
+ nsString(aDeviceId), aWindowId, tabId, aPrincipal));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::SendSessionMessage(const nsAString& aSessionId,
+ uint8_t aRole,
+ const nsAString& aData) {
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(!aData.IsEmpty());
+
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ // data channel session transport is maintained by content process
+ if (info) {
+ return info->Send(aData);
+ }
+
+ SendRequest(nullptr, SendSessionMessageRequest(nsString(aSessionId), aRole,
+ nsString(aData)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::SendSessionBinaryMsg(const nsAString& aSessionId,
+ uint8_t aRole,
+ const nsACString& aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aData.IsEmpty());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ // data channel session transport is maintained by content process
+ if (info) {
+ return info->SendBinaryMsg(aData);
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::SendSessionBlob(const nsAString& aSessionId,
+ uint8_t aRole, Blob* aBlob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+ MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
+ aRole == nsIPresentationService::ROLE_RECEIVER);
+ MOZ_ASSERT(aBlob);
+
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ // data channel session transport is maintained by content process
+ if (info) {
+ return info->SendBlob(aBlob);
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::CloseSession(const nsAString& aSessionId, uint8_t aRole,
+ uint8_t aClosedReason) {
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+
+ SendRequest(nullptr,
+ CloseSessionRequest(nsString(aSessionId), aRole, aClosedReason));
+
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ if (info) {
+ return info->Close(NS_OK);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::TerminateSession(const nsAString& aSessionId,
+ uint8_t aRole) {
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+
+ SendRequest(nullptr, TerminateSessionRequest(nsString(aSessionId), aRole));
+
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ if (info) {
+ return info->Close(NS_OK);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::ReconnectSession(
+ const nsTArray<nsString>& aUrls, const nsAString& aSessionId, uint8_t aRole,
+ nsIPresentationServiceCallback* aCallback) {
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "Only controller can call ReconnectSession.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ SendRequest(aCallback,
+ ReconnectSessionRequest(aUrls, nsString(aSessionId), aRole));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::BuildTransport(const nsAString& aSessionId,
+ uint8_t aRole) {
+ MOZ_ASSERT(!aSessionId.IsEmpty());
+
+ if (aRole != nsIPresentationService::ROLE_CONTROLLER) {
+ MOZ_ASSERT(false, "Only controller can call ReconnectSession.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ SendRequest(nullptr, BuildTransportRequest(nsString(aSessionId), aRole));
+ return NS_OK;
+}
+
+void PresentationIPCService::SendRequest(
+ nsIPresentationServiceCallback* aCallback,
+ const PresentationIPCRequest& aRequest) {
+ if (sPresentationChild) {
+ PresentationRequestChild* actor = new PresentationRequestChild(aCallback);
+ Unused << NS_WARN_IF(
+ !sPresentationChild->SendPPresentationRequestConstructor(actor,
+ aRequest));
+ }
+}
+
+NS_IMETHODIMP
+PresentationIPCService::RegisterAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aAvailabilityUrls.IsEmpty());
+ MOZ_ASSERT(aListener);
+
+ nsTArray<nsString> addedUrls;
+ mAvailabilityManager.AddAvailabilityListener(aAvailabilityUrls, aListener,
+ addedUrls);
+
+ if (sPresentationChild && !addedUrls.IsEmpty()) {
+ Unused << NS_WARN_IF(
+ !sPresentationChild->SendRegisterAvailabilityHandler(addedUrls));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::UnregisterAvailabilityListener(
+ const nsTArray<nsString>& aAvailabilityUrls,
+ nsIPresentationAvailabilityListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsTArray<nsString> removedUrls;
+ mAvailabilityManager.RemoveAvailabilityListener(aAvailabilityUrls, aListener,
+ removedUrls);
+
+ if (sPresentationChild && !removedUrls.IsEmpty()) {
+ Unused << NS_WARN_IF(
+ !sPresentationChild->SendUnregisterAvailabilityHandler(removedUrls));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::RegisterSessionListener(
+ const nsAString& aSessionId, uint8_t aRole,
+ nsIPresentationSessionListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+
+ nsCOMPtr<nsIPresentationSessionListener> listener;
+ if (mSessionListeners.Get(aSessionId, getter_AddRefs(listener))) {
+ mSessionListeners.Put(aSessionId, RefPtr{aListener});
+ return NS_OK;
+ }
+
+ mSessionListeners.Put(aSessionId, RefPtr{aListener});
+ if (sPresentationChild) {
+ Unused << NS_WARN_IF(!sPresentationChild->SendRegisterSessionHandler(
+ nsString(aSessionId), aRole));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::UnregisterSessionListener(const nsAString& aSessionId,
+ uint8_t aRole) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ UntrackSessionInfo(aSessionId, aRole);
+
+ mSessionListeners.Remove(aSessionId);
+ if (sPresentationChild) {
+ Unused << NS_WARN_IF(!sPresentationChild->SendUnregisterSessionHandler(
+ nsString(aSessionId), aRole));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::RegisterRespondingListener(
+ uint64_t aWindowId, nsIPresentationRespondingListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRespondingListeners.Put(aWindowId, RefPtr{aListener});
+ if (sPresentationChild) {
+ Unused << NS_WARN_IF(
+ !sPresentationChild->SendRegisterRespondingHandler(aWindowId));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::UnregisterRespondingListener(uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRespondingListeners.Remove(aWindowId);
+ if (sPresentationChild) {
+ Unused << NS_WARN_IF(
+ !sPresentationChild->SendUnregisterRespondingHandler(aWindowId));
+ }
+ return NS_OK;
+}
+
+nsresult PresentationIPCService::NotifySessionTransport(
+ const nsString& aSessionId, const uint8_t& aRole,
+ nsIPresentationSessionTransport* aTransport) {
+ RefPtr<PresentationContentSessionInfo> info =
+ new PresentationContentSessionInfo(aSessionId, aRole, aTransport);
+
+ if (NS_WARN_IF(NS_FAILED(info->Init()))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aRole == nsIPresentationService::ROLE_CONTROLLER) {
+ mSessionInfoAtController.Put(aSessionId, std::move(info));
+ } else {
+ mSessionInfoAtReceiver.Put(aSessionId, std::move(info));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::GetWindowIdBySessionId(const nsAString& aSessionId,
+ uint8_t aRole,
+ uint64_t* aWindowId) {
+ return GetWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+}
+
+NS_IMETHODIMP
+PresentationIPCService::UpdateWindowIdBySessionId(const nsAString& aSessionId,
+ uint8_t aRole,
+ const uint64_t aWindowId) {
+ UpdateWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+ return NS_OK;
+}
+
+nsresult PresentationIPCService::NotifySessionStateChange(
+ const nsAString& aSessionId, uint16_t aState, nsresult aReason) {
+ nsCOMPtr<nsIPresentationSessionListener> listener;
+ if (NS_WARN_IF(
+ !mSessionListeners.Get(aSessionId, getter_AddRefs(listener)))) {
+ return NS_OK;
+ }
+
+ return listener->NotifyStateChange(aSessionId, aState, aReason);
+}
+
+// Only used for OOP RTCDataChannel session transport case.
+nsresult PresentationIPCService::NotifyMessage(const nsAString& aSessionId,
+ const nsACString& aData,
+ const bool& aIsBinary) {
+ nsCOMPtr<nsIPresentationSessionListener> listener;
+ if (NS_WARN_IF(
+ !mSessionListeners.Get(aSessionId, getter_AddRefs(listener)))) {
+ return NS_OK;
+ }
+
+ return listener->NotifyMessage(aSessionId, aData, aIsBinary);
+}
+
+// Only used for OOP RTCDataChannel session transport case.
+nsresult PresentationIPCService::NotifyTransportClosed(
+ const nsAString& aSessionId, uint8_t aRole, nsresult aReason) {
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ Unused << NS_WARN_IF(!sPresentationChild->SendNotifyTransportClosed(
+ nsString(aSessionId), aRole, aReason));
+ return NS_OK;
+}
+
+nsresult PresentationIPCService::NotifySessionConnect(
+ uint64_t aWindowId, const nsAString& aSessionId) {
+ nsCOMPtr<nsIPresentationRespondingListener> listener;
+ if (NS_WARN_IF(
+ !mRespondingListeners.Get(aWindowId, getter_AddRefs(listener)))) {
+ return NS_OK;
+ }
+
+ return listener->NotifySessionConnect(aWindowId, aSessionId);
+}
+
+NS_IMETHODIMP
+PresentationIPCService::NotifyAvailableChange(
+ const nsTArray<nsString>& aAvailabilityUrls, bool aAvailable) {
+ mAvailabilityManager.DoNotifyAvailableChange(aAvailabilityUrls, aAvailable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::NotifyReceiverReady(
+ const nsAString& aSessionId, uint64_t aWindowId, bool aIsLoading,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // No actual window uses 0 as its ID.
+ if (NS_WARN_IF(aWindowId == 0)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Track the responding info for an OOP receiver page.
+ AddRespondingSessionId(aWindowId, aSessionId,
+ nsIPresentationService::ROLE_RECEIVER);
+
+ Unused << NS_WARN_IF(!sPresentationChild->SendNotifyReceiverReady(
+ nsString(aSessionId), aWindowId, aIsLoading));
+
+ // Release mCallback after using aSessionId
+ // because aSessionId is held by mCallback.
+ mCallback = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationIPCService::UntrackSessionInfo(const nsAString& aSessionId,
+ uint8_t aRole) {
+ PRES_DEBUG("content %s:id[%s], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aRole);
+
+ if (nsIPresentationService::ROLE_RECEIVER == aRole) {
+ // Terminate receiver page.
+ uint64_t windowId;
+ if (NS_SUCCEEDED(
+ GetWindowIdBySessionIdInternal(aSessionId, aRole, &windowId))) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "dom::PresentationIPCService::UntrackSessionInfo",
+ [windowId]() -> void {
+ PRES_DEBUG("Attempt to close window[%" PRIu64 "]\n", windowId);
+
+ if (auto* window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowId)) {
+ window->Close();
+ }
+ }));
+ }
+ }
+
+ // Remove the OOP responding info (if it has never been used).
+ RemoveRespondingSessionId(aSessionId, aRole);
+
+ if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
+ mSessionInfoAtController.Remove(aSessionId);
+ } else {
+ mSessionInfoAtReceiver.Remove(aSessionId);
+ }
+
+ return NS_OK;
+}
+
+void PresentationIPCService::NotifyPresentationChildDestroyed() {
+ sPresentationChild = nullptr;
+}
+
+nsresult PresentationIPCService::MonitorResponderLoading(
+ const nsAString& aSessionId, nsIDocShell* aDocShell) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mCallback = new PresentationResponderLoadingCallback(aSessionId);
+ return mCallback->Init(aDocShell);
+}
+
+nsresult PresentationIPCService::CloseContentSessionTransport(
+ const nsString& aSessionId, uint8_t aRole, nsresult aReason) {
+ RefPtr<PresentationContentSessionInfo> info =
+ GetSessionInfo(aSessionId, aRole);
+ if (NS_WARN_IF(!info)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return info->Close(aReason);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationIPCService.h b/dom/presentation/ipc/PresentationIPCService.h
new file mode 100644
index 0000000000..9b80209a52
--- /dev/null
+++ b/dom/presentation/ipc/PresentationIPCService.h
@@ -0,0 +1,71 @@
+/* -*- 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_PresentationIPCService_h
+#define mozilla_dom_PresentationIPCService_h
+
+#include "mozilla/dom/PresentationServiceBase.h"
+#include "nsIPresentationListener.h"
+#include "nsIPresentationSessionTransport.h"
+#include "nsIPresentationService.h"
+
+class nsIDocShell;
+
+namespace mozilla {
+namespace dom {
+
+class PresentationIPCRequest;
+class PresentationContentSessionInfo;
+class PresentationResponderLoadingCallback;
+
+class PresentationIPCService final
+ : public nsIPresentationAvailabilityListener,
+ public nsIPresentationService,
+ public PresentationServiceBase<PresentationContentSessionInfo> {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONAVAILABILITYLISTENER
+ NS_DECL_NSIPRESENTATIONSERVICE
+
+ PresentationIPCService();
+
+ nsresult NotifySessionStateChange(const nsAString& aSessionId,
+ uint16_t aState, nsresult aReason);
+
+ nsresult NotifyMessage(const nsAString& aSessionId, const nsACString& aData,
+ const bool& aIsBinary);
+
+ nsresult NotifySessionConnect(uint64_t aWindowId,
+ const nsAString& aSessionId);
+
+ void NotifyPresentationChildDestroyed();
+
+ nsresult MonitorResponderLoading(const nsAString& aSessionId,
+ nsIDocShell* aDocShell);
+
+ nsresult NotifySessionTransport(const nsString& aSessionId,
+ const uint8_t& aRole,
+ nsIPresentationSessionTransport* transport);
+
+ nsresult CloseContentSessionTransport(const nsString& aSessionId,
+ uint8_t aRole, nsresult aReason);
+
+ private:
+ virtual ~PresentationIPCService();
+ void SendRequest(nsIPresentationServiceCallback* aCallback,
+ const PresentationIPCRequest& aRequest);
+
+ nsRefPtrHashtable<nsStringHashKey, nsIPresentationSessionListener>
+ mSessionListeners;
+ nsRefPtrHashtable<nsUint64HashKey, nsIPresentationRespondingListener>
+ mRespondingListeners;
+ RefPtr<PresentationResponderLoadingCallback> mCallback;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationIPCService_h
diff --git a/dom/presentation/ipc/PresentationParent.cpp b/dom/presentation/ipc/PresentationParent.cpp
new file mode 100644
index 0000000000..8d643bd8b3
--- /dev/null
+++ b/dom/presentation/ipc/PresentationParent.cpp
@@ -0,0 +1,497 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ipc/InputStreamUtils.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIPresentationSessionTransport.h"
+#include "nsIPresentationSessionTransportBuilder.h"
+#include "nsServiceManagerUtils.h"
+#include "PresentationBuilderParent.h"
+#include "PresentationParent.h"
+#include "PresentationService.h"
+#include "PresentationSessionInfo.h"
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+class PresentationTransportBuilderConstructorIPC final
+ : public nsIPresentationTransportBuilderConstructor {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONTRANSPORTBUILDERCONSTRUCTOR
+
+ explicit PresentationTransportBuilderConstructorIPC(
+ PresentationParent* aParent)
+ : mParent(aParent) {}
+
+ private:
+ virtual ~PresentationTransportBuilderConstructorIPC() = default;
+
+ RefPtr<PresentationParent> mParent;
+};
+
+NS_IMPL_ISUPPORTS(PresentationTransportBuilderConstructorIPC,
+ nsIPresentationTransportBuilderConstructor)
+
+NS_IMETHODIMP
+PresentationTransportBuilderConstructorIPC::CreateTransportBuilder(
+ uint8_t aType, nsIPresentationSessionTransportBuilder** aRetval) {
+ if (NS_WARN_IF(!aRetval)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aRetval = nullptr;
+
+ if (NS_WARN_IF(aType != nsIPresentationChannelDescription::TYPE_TCP &&
+ aType !=
+ nsIPresentationChannelDescription::TYPE_DATACHANNEL)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (XRE_IsContentProcess()) {
+ MOZ_ASSERT(false,
+ "CreateTransportBuilder can only be invoked in parent process.");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPresentationSessionTransportBuilder> builder;
+ if (aType == nsIPresentationChannelDescription::TYPE_TCP) {
+ builder = do_CreateInstance(PRESENTATION_TCP_SESSION_TRANSPORT_CONTRACTID);
+ } else {
+ builder = new PresentationBuilderParent(mParent);
+ }
+
+ if (NS_WARN_IF(!builder)) {
+ return NS_ERROR_DOM_OPERATION_ERR;
+ }
+
+ builder.forget(aRetval);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+/*
+ * Implementation of PresentationParent
+ */
+
+NS_IMPL_ISUPPORTS(PresentationParent, nsIPresentationAvailabilityListener,
+ nsIPresentationSessionListener,
+ nsIPresentationRespondingListener)
+
+PresentationParent::PresentationParent() = default;
+
+/* virtual */ PresentationParent::~PresentationParent() = default;
+
+bool PresentationParent::Init(ContentParentId aContentParentId) {
+ MOZ_ASSERT(!mService);
+ mService = do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ mChildId = aContentParentId;
+ return !NS_WARN_IF(!mService);
+}
+
+void PresentationParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mActorDestroyed = true;
+
+ for (uint32_t i = 0; i < mSessionIdsAtController.Length(); i++) {
+ Unused << NS_WARN_IF(NS_FAILED(mService->UnregisterSessionListener(
+ mSessionIdsAtController[i], nsIPresentationService::ROLE_CONTROLLER)));
+ }
+ mSessionIdsAtController.Clear();
+
+ for (uint32_t i = 0; i < mSessionIdsAtReceiver.Length(); i++) {
+ Unused << NS_WARN_IF(NS_FAILED(mService->UnregisterSessionListener(
+ mSessionIdsAtReceiver[i], nsIPresentationService::ROLE_RECEIVER)));
+ }
+ mSessionIdsAtReceiver.Clear();
+
+ for (uint32_t i = 0; i < mWindowIds.Length(); i++) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->UnregisterRespondingListener(mWindowIds[i])));
+ }
+ mWindowIds.Clear();
+
+ if (!mContentAvailabilityUrls.IsEmpty()) {
+ mService->UnregisterAvailabilityListener(mContentAvailabilityUrls, this);
+ }
+ mService = nullptr;
+}
+
+mozilla::ipc::IPCResult PresentationParent::RecvPPresentationRequestConstructor(
+ PPresentationRequestParent* aActor,
+ const PresentationIPCRequest& aRequest) {
+ PresentationRequestParent* actor =
+ static_cast<PresentationRequestParent*>(aActor);
+
+ nsresult rv = NS_ERROR_FAILURE;
+ switch (aRequest.type()) {
+ case PresentationIPCRequest::TStartSessionRequest:
+ rv = actor->DoRequest(aRequest.get_StartSessionRequest());
+ break;
+ case PresentationIPCRequest::TSendSessionMessageRequest:
+ rv = actor->DoRequest(aRequest.get_SendSessionMessageRequest());
+ break;
+ case PresentationIPCRequest::TCloseSessionRequest:
+ rv = actor->DoRequest(aRequest.get_CloseSessionRequest());
+ break;
+ case PresentationIPCRequest::TTerminateSessionRequest:
+ rv = actor->DoRequest(aRequest.get_TerminateSessionRequest());
+ break;
+ case PresentationIPCRequest::TReconnectSessionRequest:
+ rv = actor->DoRequest(aRequest.get_ReconnectSessionRequest());
+ break;
+ case PresentationIPCRequest::TBuildTransportRequest:
+ rv = actor->DoRequest(aRequest.get_BuildTransportRequest());
+ break;
+ default:
+ MOZ_CRASH("Unknown PresentationIPCRequest type");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+PPresentationRequestParent* PresentationParent::AllocPPresentationRequestParent(
+ const PresentationIPCRequest& aRequest) {
+ MOZ_ASSERT(mService);
+ RefPtr<PresentationRequestParent> actor =
+ new PresentationRequestParent(mService, mChildId);
+ return actor.forget().take();
+}
+
+bool PresentationParent::DeallocPPresentationRequestParent(
+ PPresentationRequestParent* aActor) {
+ RefPtr<PresentationRequestParent> actor =
+ dont_AddRef(static_cast<PresentationRequestParent*>(aActor));
+ return true;
+}
+
+PPresentationBuilderParent* PresentationParent::AllocPPresentationBuilderParent(
+ const nsString& aSessionId, const uint8_t& aRole) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We should never be manually allocating "
+ "AllocPPresentationBuilderParent actors");
+ return nullptr;
+}
+
+bool PresentationParent::DeallocPPresentationBuilderParent(
+ PPresentationBuilderParent* aActor) {
+ return true;
+}
+
+mozilla::ipc::IPCResult PresentationParent::Recv__delete__() {
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationParent::RecvRegisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls) {
+ MOZ_ASSERT(mService);
+
+ Unused << NS_WARN_IF(NS_FAILED(
+ mService->RegisterAvailabilityListener(aAvailabilityUrls, this)));
+ mContentAvailabilityUrls.AppendElements(aAvailabilityUrls);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationParent::RecvUnregisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls) {
+ MOZ_ASSERT(mService);
+
+ Unused << NS_WARN_IF(NS_FAILED(
+ mService->UnregisterAvailabilityListener(aAvailabilityUrls, this)));
+ for (const auto& url : aAvailabilityUrls) {
+ mContentAvailabilityUrls.RemoveElement(url);
+ }
+ return IPC_OK();
+}
+
+/* virtual */ mozilla::ipc::IPCResult
+PresentationParent::RecvRegisterSessionHandler(const nsString& aSessionId,
+ const uint8_t& aRole) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aSessionId, aRole, OtherPid()))) {
+ return IPC_OK();
+ }
+
+ if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
+ mSessionIdsAtController.AppendElement(aSessionId);
+ } else {
+ mSessionIdsAtReceiver.AppendElement(aSessionId);
+ }
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->RegisterSessionListener(aSessionId, aRole, this)));
+ return IPC_OK();
+}
+
+/* virtual */ mozilla::ipc::IPCResult
+PresentationParent::RecvUnregisterSessionHandler(const nsString& aSessionId,
+ const uint8_t& aRole) {
+ MOZ_ASSERT(mService);
+ if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
+ mSessionIdsAtController.RemoveElement(aSessionId);
+ } else {
+ mSessionIdsAtReceiver.RemoveElement(aSessionId);
+ }
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->UnregisterSessionListener(aSessionId, aRole)));
+ return IPC_OK();
+}
+
+/* virtual */ mozilla::ipc::IPCResult
+PresentationParent::RecvRegisterRespondingHandler(const uint64_t& aWindowId) {
+ MOZ_ASSERT(mService);
+
+ mWindowIds.AppendElement(aWindowId);
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->RegisterRespondingListener(aWindowId, this)));
+ return IPC_OK();
+}
+
+/* virtual */ mozilla::ipc::IPCResult
+PresentationParent::RecvUnregisterRespondingHandler(const uint64_t& aWindowId) {
+ MOZ_ASSERT(mService);
+ mWindowIds.RemoveElement(aWindowId);
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->UnregisterRespondingListener(aWindowId)));
+ return IPC_OK();
+}
+
+NS_IMETHODIMP
+PresentationParent::NotifyAvailableChange(
+ const nsTArray<nsString>& aAvailabilityUrls, bool aAvailable) {
+ if (NS_WARN_IF(mActorDestroyed ||
+ !SendNotifyAvailableChange(aAvailabilityUrls, aAvailable))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationParent::NotifyStateChange(const nsAString& aSessionId,
+ uint16_t aState, nsresult aReason) {
+ if (NS_WARN_IF(mActorDestroyed ||
+ !SendNotifySessionStateChange(nsString(aSessionId), aState,
+ aReason))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationParent::NotifyMessage(const nsAString& aSessionId,
+ const nsACString& aData, bool aIsBinary) {
+ if (NS_WARN_IF(mActorDestroyed ||
+ !SendNotifyMessage(nsString(aSessionId), nsCString(aData),
+ aIsBinary))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationParent::NotifySessionConnect(uint64_t aWindowId,
+ const nsAString& aSessionId) {
+ if (NS_WARN_IF(mActorDestroyed ||
+ !SendNotifySessionConnect(aWindowId, nsString(aSessionId)))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult PresentationParent::RecvNotifyReceiverReady(
+ const nsString& aSessionId, const uint64_t& aWindowId,
+ const bool& aIsLoading) {
+ MOZ_ASSERT(mService);
+
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> constructor =
+ new PresentationTransportBuilderConstructorIPC(this);
+ Unused << NS_WARN_IF(NS_FAILED(mService->NotifyReceiverReady(
+ aSessionId, aWindowId, aIsLoading, constructor)));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult PresentationParent::RecvNotifyTransportClosed(
+ const nsString& aSessionId, const uint8_t& aRole, const nsresult& aReason) {
+ MOZ_ASSERT(mService);
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(mService->NotifyTransportClosed(aSessionId, aRole, aReason)));
+ return IPC_OK();
+}
+
+/*
+ * Implementation of PresentationRequestParent
+ */
+
+NS_IMPL_ISUPPORTS(PresentationRequestParent, nsIPresentationServiceCallback)
+
+PresentationRequestParent::PresentationRequestParent(
+ nsIPresentationService* aService, ContentParentId aContentParentId)
+ : mService(aService), mChildId(aContentParentId) {}
+
+PresentationRequestParent::~PresentationRequestParent() = default;
+
+void PresentationRequestParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mActorDestroyed = true;
+ mService = nullptr;
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const StartSessionRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ mSessionId = aRequest.sessionId();
+
+ RefPtr<EventTarget> eventTarget;
+ ContentProcessManager* cpm = ContentProcessManager::GetSingleton();
+ RefPtr<BrowserParent> tp = cpm->GetTopLevelBrowserParentByProcessAndTabId(
+ mChildId, aRequest.tabId());
+ if (tp) {
+ eventTarget = tp->GetOwnerElement();
+ }
+
+ RefPtr<PresentationParent> parent =
+ static_cast<PresentationParent*>(Manager());
+ nsCOMPtr<nsIPresentationTransportBuilderConstructor> constructor =
+ new PresentationTransportBuilderConstructorIPC(parent);
+ return mService->StartSession(aRequest.urls(), aRequest.sessionId(),
+ aRequest.origin(), aRequest.deviceId(),
+ aRequest.windowId(), eventTarget,
+ aRequest.principal(), this, constructor);
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const SendSessionMessageRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aRequest.sessionId(),
+ aRequest.role(), OtherPid()))) {
+ return SendResponse(NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ nsresult rv = mService->SendSessionMessage(aRequest.sessionId(),
+ aRequest.role(), aRequest.data());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendResponse(rv);
+ }
+ return SendResponse(NS_OK);
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const CloseSessionRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aRequest.sessionId(),
+ aRequest.role(), OtherPid()))) {
+ return SendResponse(NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ nsresult rv = mService->CloseSession(aRequest.sessionId(), aRequest.role(),
+ aRequest.closedReason());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendResponse(rv);
+ }
+ return SendResponse(NS_OK);
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const TerminateSessionRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aRequest.sessionId(),
+ aRequest.role(), OtherPid()))) {
+ return SendResponse(NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ nsresult rv =
+ mService->TerminateSession(aRequest.sessionId(), aRequest.role());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendResponse(rv);
+ }
+ return SendResponse(NS_OK);
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const ReconnectSessionRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aRequest.sessionId(),
+ aRequest.role(), OtherPid()))) {
+ // NOTE: Return NS_ERROR_DOM_NOT_FOUND_ERR here to match the spec.
+ // https://w3c.github.io/presentation-api/#reconnecting-to-a-presentation
+ return SendResponse(NS_ERROR_DOM_NOT_FOUND_ERR);
+ }
+
+ mSessionId = aRequest.sessionId();
+ return mService->ReconnectSession(aRequest.urls(), aRequest.sessionId(),
+ aRequest.role(), this);
+}
+
+nsresult PresentationRequestParent::DoRequest(
+ const BuildTransportRequest& aRequest) {
+ MOZ_ASSERT(mService);
+
+ // Validate the accessibility (primarily for receiver side) so that a
+ // compromised child process can't fake the ID.
+ if (NS_WARN_IF(!static_cast<PresentationService*>(mService.get())
+ ->IsSessionAccessible(aRequest.sessionId(),
+ aRequest.role(), OtherPid()))) {
+ return SendResponse(NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ nsresult rv = mService->BuildTransport(aRequest.sessionId(), aRequest.role());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return SendResponse(rv);
+ }
+ return SendResponse(NS_OK);
+}
+
+NS_IMETHODIMP
+PresentationRequestParent::NotifySuccess(const nsAString& aUrl) {
+ Unused << SendNotifyRequestUrlSelected(nsString(aUrl));
+ return SendResponse(NS_OK);
+}
+
+NS_IMETHODIMP
+PresentationRequestParent::NotifyError(nsresult aError) {
+ return SendResponse(aError);
+}
+
+nsresult PresentationRequestParent::SendResponse(nsresult aResult) {
+ if (NS_WARN_IF(mActorDestroyed || !Send__delete__(this, aResult))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationParent.h b/dom/presentation/ipc/PresentationParent.h
new file mode 100644
index 0000000000..c50d374923
--- /dev/null
+++ b/dom/presentation/ipc/PresentationParent.h
@@ -0,0 +1,133 @@
+/* -*- 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_PresentationParent_h__
+#define mozilla_dom_PresentationParent_h__
+
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/PPresentationBuilderParent.h"
+#include "mozilla/dom/PPresentationParent.h"
+#include "mozilla/dom/PPresentationRequestParent.h"
+#include "nsIPresentationListener.h"
+#include "nsIPresentationService.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationParent final : public PPresentationParent,
+ public nsIPresentationAvailabilityListener,
+ public nsIPresentationSessionListener,
+ public nsIPresentationRespondingListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONAVAILABILITYLISTENER
+ NS_DECL_NSIPRESENTATIONSESSIONLISTENER
+ NS_DECL_NSIPRESENTATIONRESPONDINGLISTENER
+
+ PresentationParent();
+
+ bool Init(ContentParentId aContentParentId);
+
+ bool RegisterTransportBuilder(const nsString& aSessionId,
+ const uint8_t& aRole);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual mozilla::ipc::IPCResult RecvPPresentationRequestConstructor(
+ PPresentationRequestParent* aActor,
+ const PresentationIPCRequest& aRequest) override;
+
+ PPresentationRequestParent* AllocPPresentationRequestParent(
+ const PresentationIPCRequest& aRequest);
+
+ bool DeallocPPresentationRequestParent(PPresentationRequestParent* aActor);
+
+ PPresentationBuilderParent* AllocPPresentationBuilderParent(
+ const nsString& aSessionId, const uint8_t& aRole);
+
+ bool DeallocPPresentationBuilderParent(PPresentationBuilderParent* aActor);
+
+ virtual mozilla::ipc::IPCResult Recv__delete__() override;
+
+ mozilla::ipc::IPCResult RecvRegisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls);
+
+ mozilla::ipc::IPCResult RecvUnregisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls);
+
+ mozilla::ipc::IPCResult RecvRegisterSessionHandler(const nsString& aSessionId,
+ const uint8_t& aRole);
+
+ mozilla::ipc::IPCResult RecvUnregisterSessionHandler(
+ const nsString& aSessionId, const uint8_t& aRole);
+
+ mozilla::ipc::IPCResult RecvRegisterRespondingHandler(
+ const uint64_t& aWindowId);
+
+ mozilla::ipc::IPCResult RecvUnregisterRespondingHandler(
+ const uint64_t& aWindowId);
+
+ mozilla::ipc::IPCResult RecvNotifyReceiverReady(const nsString& aSessionId,
+ const uint64_t& aWindowId,
+ const bool& aIsLoading);
+
+ mozilla::ipc::IPCResult RecvNotifyTransportClosed(const nsString& aSessionId,
+ const uint8_t& aRole,
+ const nsresult& aReason);
+
+ private:
+ virtual ~PresentationParent();
+
+ bool mActorDestroyed = false;
+ nsCOMPtr<nsIPresentationService> mService;
+ nsTArray<nsString> mSessionIdsAtController;
+ nsTArray<nsString> mSessionIdsAtReceiver;
+ nsTArray<uint64_t> mWindowIds;
+ ContentParentId mChildId;
+ nsTArray<nsString> mContentAvailabilityUrls;
+};
+
+class PresentationRequestParent final : public PPresentationRequestParent,
+ public nsIPresentationServiceCallback {
+ friend class PresentationParent;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONSERVICECALLBACK
+
+ explicit PresentationRequestParent(nsIPresentationService* aService,
+ ContentParentId aContentParentId);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~PresentationRequestParent();
+
+ nsresult SendResponse(nsresult aResult);
+
+ nsresult DoRequest(const StartSessionRequest& aRequest);
+
+ nsresult DoRequest(const SendSessionMessageRequest& aRequest);
+
+ nsresult DoRequest(const CloseSessionRequest& aRequest);
+
+ nsresult DoRequest(const TerminateSessionRequest& aRequest);
+
+ nsresult DoRequest(const ReconnectSessionRequest& aRequest);
+
+ nsresult DoRequest(const BuildTransportRequest& aRequest);
+
+ bool mActorDestroyed = false;
+ bool mNeedRegisterBuilder = false;
+ nsString mSessionId;
+ nsCOMPtr<nsIPresentationService> mService;
+ ContentParentId mChildId;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PresentationParent_h__
diff --git a/dom/presentation/moz.build b/dom/presentation/moz.build
new file mode 100644
index 0000000000..2b8ad19fd1
--- /dev/null
+++ b/dom/presentation/moz.build
@@ -0,0 +1,88 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+DIRS += ["interfaces", "provider"]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.ini"]
+MOCHITEST_CHROME_MANIFESTS += ["tests/mochitest/chrome.ini"]
+
+EXPORTS.mozilla.dom += [
+ "DCPresentationChannelDescription.h",
+ "ipc/PresentationBuilderChild.h",
+ "ipc/PresentationBuilderParent.h",
+ "ipc/PresentationChild.h",
+ "ipc/PresentationIPCService.h",
+ "ipc/PresentationParent.h",
+ "MockedSocketTransport.h",
+ "Presentation.h",
+ "PresentationAvailability.h",
+ "PresentationCallbacks.h",
+ "PresentationConnection.h",
+ "PresentationConnectionList.h",
+ "PresentationDeviceManager.h",
+ "PresentationReceiver.h",
+ "PresentationRequest.h",
+ "PresentationService.h",
+ "PresentationServiceBase.h",
+ "PresentationSessionInfo.h",
+ "PresentationTCPSessionTransport.h",
+]
+
+UNIFIED_SOURCES += [
+ "AvailabilityCollection.cpp",
+ "ControllerConnectionCollection.cpp",
+ "DCPresentationChannelDescription.cpp",
+ "ipc/PresentationBuilderChild.cpp",
+ "ipc/PresentationBuilderParent.cpp",
+ "ipc/PresentationChild.cpp",
+ "ipc/PresentationContentSessionInfo.cpp",
+ "ipc/PresentationIPCService.cpp",
+ "ipc/PresentationParent.cpp",
+ "MockedSocketTransport.cpp",
+ "Presentation.cpp",
+ "PresentationAvailability.cpp",
+ "PresentationCallbacks.cpp",
+ "PresentationConnection.cpp",
+ "PresentationConnectionList.cpp",
+ "PresentationDeviceManager.cpp",
+ "PresentationReceiver.cpp",
+ "PresentationRequest.cpp",
+ "PresentationService.cpp",
+ "PresentationSessionInfo.cpp",
+ "PresentationSessionRequest.cpp",
+ "PresentationTCPSessionTransport.cpp",
+ "PresentationTerminateRequest.cpp",
+ "PresentationTransportBuilderConstructor.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "PresentationDataChannelSessionTransport.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ EXTRA_JS_MODULES += [
+ "PresentationNetworkHelper.jsm",
+ ]
+
+IPDL_SOURCES += [
+ "ipc/PPresentation.ipdl",
+ "ipc/PPresentationBuilder.ipdl",
+ "ipc/PPresentationRequest.ipdl",
+]
+
+LOCAL_INCLUDES += ["../base"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/presentation/provider/ControllerStateMachine.jsm b/dom/presentation/provider/ControllerStateMachine.jsm
new file mode 100644
index 0000000000..03f6b35bd5
--- /dev/null
+++ b/dom/presentation/provider/ControllerStateMachine.jsm
@@ -0,0 +1,236 @@
+/* -*- 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ControllerStateMachine"];
+
+const { CommandType, State } = ChromeUtils.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));
+ },
+ function _connectingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.CONNECT_ACK:
+ stateMachine.state = State.CONNECTED;
+ stateMachine._notifyDeviceConnected();
+ break;
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _connectedHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ case CommandType.LAUNCH_ACK:
+ stateMachine._notifyLaunch(command.presentationId);
+ break;
+ case CommandType.TERMINATE:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.TERMINATE_ACK:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.ANSWER:
+ case CommandType.ICE_CANDIDATE:
+ stateMachine._notifyChannelDescriptor(command);
+ break;
+ case CommandType.RECONNECT_ACK:
+ stateMachine._notifyReconnect(command.presentationId);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _closingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _closedHandler(stateMachine, command) {
+ // ignore every command in closed state.
+ DEBUG && debug("unexpected command: " + JSON.stringify(command));
+ },
+];
+
+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,
+ url,
+ });
+ }
+ },
+
+ terminate: function _terminate(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE,
+ presentationId,
+ });
+ }
+ },
+
+ terminateAck: function _terminateAck(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE_ACK,
+ presentationId,
+ });
+ }
+ },
+
+ reconnect: function _reconnect(presentationId, url) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.RECONNECT,
+ presentationId,
+ url,
+ });
+ }
+ },
+
+ sendOffer: function _sendOffer(offer) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.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,
+ });
+ }
+ },
+
+ onCommand: function _onCommand(command) {
+ handlers[this.state](this, command);
+ },
+
+ onChannelReady: function _onChannelReady() {
+ if (this.state === State.INIT) {
+ this._sendCommand({
+ type: CommandType.CONNECT,
+ deviceId: this._deviceId,
+ });
+ this.state = State.CONNECTING;
+ }
+ },
+
+ onChannelClosed: function _onChannelClose(reason, isByRemote) {
+ switch (this.state) {
+ case State.CONNECTED:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ this._notifyDisconnected(reason);
+ } else {
+ this._sendCommand({
+ type: CommandType.DISCONNECT,
+ reason,
+ });
+ this.state = State.CLOSING;
+ this._closeReason = reason;
+ }
+ break;
+ case State.CLOSING:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ if (this._closeReason) {
+ reason = this._closeReason;
+ delete this._closeReason;
+ }
+ this._notifyDisconnected(reason);
+ }
+ break;
+ default:
+ DEBUG &&
+ debug("unexpected channel close: " + reason + ", " + isByRemote);
+ break;
+ }
+ },
+
+ _sendCommand: function _sendCommand(command) {
+ this._channel.sendCommand(command);
+ },
+
+ _notifyDeviceConnected: function _notifyDeviceConnected() {
+ // XXX trigger following command
+ this._channel.notifyDeviceConnected();
+ },
+
+ _notifyDisconnected: function _notifyDisconnected(reason) {
+ this._channel.notifyDisconnected(reason);
+ },
+
+ _notifyLaunch: function _notifyLaunch(presentationId) {
+ this._channel.notifyLaunch(presentationId);
+ },
+
+ _notifyTerminate: function _notifyTerminate(presentationId) {
+ this._channel.notifyTerminate(presentationId);
+ },
+
+ _notifyReconnect: function _notifyReconnect(presentationId) {
+ this._channel.notifyReconnect(presentationId);
+ },
+
+ _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
+ switch (command.type) {
+ case CommandType.ANSWER:
+ this._channel.notifyAnswer(command.answer);
+ break;
+ case CommandType.ICE_CANDIDATE:
+ this._channel.notifyIceCandidate(command.candidate);
+ break;
+ }
+ },
+};
diff --git a/dom/presentation/provider/DeviceProviderHelpers.cpp b/dom/presentation/provider/DeviceProviderHelpers.cpp
new file mode 100644
index 0000000000..011afc7040
--- /dev/null
+++ b/dom/presentation/provider/DeviceProviderHelpers.cpp
@@ -0,0 +1,53 @@
+/* -*- 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 "DeviceProviderHelpers.h"
+
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+static const char* const kFxTVPresentationAppUrls[] = {
+ "app://fling-player.gaiamobile.org/index.html",
+ "app://notification-receiver.gaiamobile.org/index.html", nullptr};
+
+/* static */
+bool DeviceProviderHelpers::IsCommonlySupportedScheme(const nsAString& aUrl) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl);
+ if (NS_FAILED(rv) || !uri) {
+ return false;
+ }
+
+ nsAutoCString scheme;
+ uri->GetScheme(scheme);
+ if (scheme.LowerCaseEqualsLiteral("http") ||
+ scheme.LowerCaseEqualsLiteral("https")) {
+ return true;
+ }
+
+ return false;
+}
+
+/* static */
+bool DeviceProviderHelpers::IsFxTVSupportedAppUrl(const nsAString& aUrl) {
+ // Check if matched with any presentation Apps on TV.
+ for (uint32_t i = 0; kFxTVPresentationAppUrls[i]; i++) {
+ if (aUrl.EqualsASCII(kFxTVPresentationAppUrls[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/DeviceProviderHelpers.h b/dom/presentation/provider/DeviceProviderHelpers.h
new file mode 100644
index 0000000000..4bf6f9ae5b
--- /dev/null
+++ b/dom/presentation/provider/DeviceProviderHelpers.h
@@ -0,0 +1,29 @@
+/* -*- 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_presentation_DeviceProviderHelpers_h
+#define mozilla_dom_presentation_DeviceProviderHelpers_h
+
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class DeviceProviderHelpers final {
+ public:
+ static bool IsCommonlySupportedScheme(const nsAString& aUrl);
+ static bool IsFxTVSupportedAppUrl(const nsAString& aUrl);
+
+ private:
+ DeviceProviderHelpers() = delete;
+};
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_presentation_DeviceProviderHelpers_h
diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
new file mode 100644
index 0000000000..d5f443dfd2
--- /dev/null
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ -0,0 +1,1153 @@
+/* -*- 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 "MulticastDNSDeviceProvider.h"
+
+#include "DeviceProviderHelpers.h"
+#include "MainThreadUtils.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTCPDeviceInfo.h"
+#include "nsThreadUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "nsIPropertyBag2.h"
+#endif // MOZ_WIDGET_ANDROID
+
+#define PREF_PRESENTATION_DISCOVERY "dom.presentation.discovery.enabled"
+#define PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS \
+ "dom.presentation.discovery.timeout_ms"
+#define PREF_PRESENTATION_DISCOVERABLE "dom.presentation.discoverable"
+#define PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED \
+ "dom.presentation.discoverable.encrypted"
+#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS \
+ "dom.presentation.discoverable.retry_ms"
+#define PREF_PRESENTATION_DEVICE_NAME "dom.presentation.device.name"
+
+#define SERVICE_TYPE "_presentation-ctrl._tcp"
+#define PROTOCOL_VERSION_TAG u"version"
+#define CERT_FINGERPRINT_TAG u"certFingerprint"
+
+static mozilla::LazyLogModule sMulticastDNSProviderLogModule(
+ "MulticastDNSDeviceProvider");
+
+#undef LOG_I
+#define LOG_I(...) \
+ MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Debug, \
+ (__VA_ARGS__))
+#undef LOG_E
+#define LOG_E(...) \
+ MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Error, \
+ (__VA_ARGS__))
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+static const char* kObservedPrefs[] = {
+ PREF_PRESENTATION_DISCOVERY, PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS,
+ PREF_PRESENTATION_DISCOVERABLE, PREF_PRESENTATION_DEVICE_NAME, nullptr};
+
+namespace {
+
+#ifdef MOZ_WIDGET_ANDROID
+static void GetAndroidDeviceName(nsACString& aRetVal) {
+ nsCOMPtr<nsIPropertyBag2> infoService =
+ do_GetService("@mozilla.org/system-info;1");
+ MOZ_ASSERT(infoService, "Could not find a system info service");
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(infoService->GetPropertyAsACString(u"device"_ns, aRetVal)));
+}
+#endif // MOZ_WIDGET_ANDROID
+
+} // anonymous namespace
+
+/**
+ * This wrapper is used to break circular-reference problem.
+ */
+class DNSServiceWrappedListener final
+ : public nsIDNSServiceDiscoveryListener,
+ public nsIDNSRegistrationListener,
+ public nsIDNSServiceResolveListener,
+ public nsIPresentationControlServerListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIDNSSERVICEDISCOVERYLISTENER(mListener)
+ NS_FORWARD_SAFE_NSIDNSREGISTRATIONLISTENER(mListener)
+ NS_FORWARD_SAFE_NSIDNSSERVICERESOLVELISTENER(mListener)
+ NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener)
+
+ explicit DNSServiceWrappedListener() = default;
+
+ nsresult SetListener(MulticastDNSDeviceProvider* aListener) {
+ mListener = aListener;
+ return NS_OK;
+ }
+
+ private:
+ virtual ~DNSServiceWrappedListener() = default;
+
+ MulticastDNSDeviceProvider* mListener = nullptr;
+};
+
+NS_IMPL_ISUPPORTS(DNSServiceWrappedListener, nsIDNSServiceDiscoveryListener,
+ nsIDNSRegistrationListener, nsIDNSServiceResolveListener,
+ nsIPresentationControlServerListener)
+
+NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider, nsIPresentationDeviceProvider,
+ nsIDNSServiceDiscoveryListener, nsIDNSRegistrationListener,
+ nsIDNSServiceResolveListener,
+ nsIPresentationControlServerListener, nsIObserver)
+
+MulticastDNSDeviceProvider::MulticastDNSDeviceProvider() = default;
+MulticastDNSDeviceProvider::~MulticastDNSDeviceProvider() { Uninit(); }
+
+nsresult MulticastDNSDeviceProvider::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mInitialized) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ mMulticastDNS = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mWrappedListener = new DNSServiceWrappedListener();
+ if (NS_WARN_IF(NS_FAILED(rv = mWrappedListener->SetListener(this)))) {
+ return rv;
+ }
+
+ mPresentationService =
+ do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mDiscoveryTimer = NS_NewTimer();
+ if (NS_WARN_IF(!mDiscoveryTimer)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mServerRetryTimer = NS_NewTimer();
+ if (NS_WARN_IF(!mServerRetryTimer)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ Preferences::AddStrongObservers(this, kObservedPrefs);
+
+ mDiscoveryEnabled = Preferences::GetBool(PREF_PRESENTATION_DISCOVERY);
+ mDiscoveryTimeoutMs =
+ Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS);
+ mDiscoverable = Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE);
+ mDiscoverableEncrypted =
+ Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED);
+ mServerRetryMs =
+ Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS);
+ mServiceName.Truncate();
+ Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME, mServiceName);
+
+#ifdef MOZ_WIDGET_ANDROID
+ // FIXME: Bug 1185806 - Provide a common device name setting.
+ if (mServiceName.IsEmpty()) {
+ GetAndroidDeviceName(mServiceName);
+ Unused << Preferences::SetCString(PREF_PRESENTATION_DEVICE_NAME,
+ mServiceName);
+ }
+#endif // MOZ_WIDGET_ANDROID
+
+ Unused << mPresentationService->SetId(mServiceName);
+
+ if (mDiscoveryEnabled && NS_WARN_IF(NS_FAILED(rv = ForceDiscovery()))) {
+ return rv;
+ }
+
+ if (mDiscoverable && NS_WARN_IF(NS_FAILED(rv = StartServer()))) {
+ return rv;
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+void MulticastDNSDeviceProvider::Uninit() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mInitialized) {
+ return;
+ }
+
+ ClearDevices();
+
+ Preferences::RemoveObservers(this, kObservedPrefs);
+
+ StopDiscovery(NS_OK);
+ StopServer();
+
+ mMulticastDNS = nullptr;
+
+ if (mWrappedListener) {
+ mWrappedListener->SetListener(nullptr);
+ mWrappedListener = nullptr;
+ }
+
+ mInitialized = false;
+}
+
+nsresult MulticastDNSDeviceProvider::StartServer() {
+ LOG_I("StartServer: %s (%d)", mServiceName.get(), mDiscoverable);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDiscoverable) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ uint16_t servicePort;
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->GetPort(&servicePort)))) {
+ return rv;
+ }
+
+ /**
+ * If |servicePort| is non-zero, it means PresentationControlService is
+ * running. Otherwise, we should make it start serving.
+ */
+ if (servicePort) {
+ return RegisterMDNSService();
+ }
+
+ if (NS_WARN_IF(NS_FAILED(
+ rv = mPresentationService->SetListener(mWrappedListener)))) {
+ return rv;
+ }
+
+ AbortServerRetry();
+
+ if (NS_WARN_IF(NS_FAILED(
+ rv = mPresentationService->StartServer(mDiscoverableEncrypted, 0)))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void MulticastDNSDeviceProvider::StopServer() {
+ LOG_I("StopServer: %s", mServiceName.get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ UnregisterMDNSService(NS_OK);
+
+ AbortServerRetry();
+
+ if (mPresentationService) {
+ mPresentationService->SetListener(nullptr);
+ mPresentationService->Close();
+ }
+}
+
+void MulticastDNSDeviceProvider::AbortServerRetry() {
+ if (mIsServerRetrying) {
+ mIsServerRetrying = false;
+ mServerRetryTimer->Cancel();
+ }
+}
+
+nsresult MulticastDNSDeviceProvider::RegisterMDNSService() {
+ LOG_I("RegisterMDNSService: %s", mServiceName.get());
+
+ if (!mDiscoverable) {
+ return NS_OK;
+ }
+
+ // Cancel on going service registration.
+ UnregisterMDNSService(NS_OK);
+
+ nsresult rv;
+
+ uint16_t servicePort;
+ if (NS_FAILED(rv = mPresentationService->GetPort(&servicePort)) ||
+ !servicePort) {
+ // Abort service registration if server port is not available.
+ return rv;
+ }
+
+ /**
+ * Register the presentation control channel server as an mDNS service.
+ */
+ nsCOMPtr<nsIDNSServiceInfo> serviceInfo =
+ do_CreateInstance(DNSSERVICEINFO_CONTRACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(
+ rv = serviceInfo->SetServiceType(nsLiteralCString(SERVICE_TYPE))))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetServiceName(mServiceName)))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetPort(servicePort)))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+ MOZ_ASSERT(propBag);
+
+ uint32_t version;
+ rv = mPresentationService->GetVersion(&version);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = propBag->SetPropertyAsUint32(nsLiteralString(PROTOCOL_VERSION_TAG),
+ version);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ if (mDiscoverableEncrypted) {
+ nsAutoCString certFingerprint;
+ rv = mPresentationService->GetCertFingerprint(certFingerprint);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = propBag->SetPropertyAsACString(nsLiteralString(CERT_FINGERPRINT_TAG),
+ certFingerprint);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetAttributes(propBag)))) {
+ return rv;
+ }
+
+ return mMulticastDNS->RegisterService(serviceInfo, mWrappedListener,
+ getter_AddRefs(mRegisterRequest));
+}
+
+void MulticastDNSDeviceProvider::UnregisterMDNSService(nsresult aReason) {
+ LOG_I("UnregisterMDNSService: %s (0x%08" PRIx32 ")", mServiceName.get(),
+ static_cast<uint32_t>(aReason));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mRegisterRequest) {
+ mRegisterRequest->Cancel(aReason);
+ mRegisterRequest = nullptr;
+ }
+}
+
+nsresult MulticastDNSDeviceProvider::StopDiscovery(nsresult aReason) {
+ LOG_I("StopDiscovery (0x%08" PRIx32 ")", static_cast<uint32_t>(aReason));
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (mDiscoveryRequest) {
+ mDiscoveryRequest->Cancel(aReason);
+ mDiscoveryRequest = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::Connect(
+ Device* aDevice, nsIPresentationControlChannel** aRetVal) {
+ MOZ_ASSERT(aDevice);
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<TCPDeviceInfo> deviceInfo =
+ new TCPDeviceInfo(aDevice->Id(), aDevice->Address(), aDevice->Port(),
+ aDevice->CertFingerprint());
+
+ return mPresentationService->Connect(deviceInfo, aRetVal);
+}
+
+bool MulticastDNSDeviceProvider::IsCompatibleServer(
+ nsIDNSServiceInfo* aServiceInfo) {
+ MOZ_ASSERT(aServiceInfo);
+
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ if (NS_WARN_IF(
+ NS_FAILED(aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) ||
+ !propBag) {
+ return false;
+ }
+
+ uint32_t remoteVersion;
+ if (NS_WARN_IF(NS_FAILED(propBag->GetPropertyAsUint32(
+ nsLiteralString(PROTOCOL_VERSION_TAG), &remoteVersion)))) {
+ return false;
+ }
+
+ bool isCompatible = false;
+ Unused << NS_WARN_IF(NS_FAILED(
+ mPresentationService->IsCompatibleServer(remoteVersion, &isCompatible)));
+
+ return isCompatible;
+}
+
+nsresult MulticastDNSDeviceProvider::AddDevice(
+ const nsACString& aId, const nsACString& aServiceName,
+ const nsACString& aServiceType, const nsACString& aAddress,
+ const uint16_t aPort, const nsACString& aCertFingerprint) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<Device> device =
+ new Device(aId, /* ID */
+ aServiceName, aServiceType, aAddress, aPort, aCertFingerprint,
+ DeviceState::eActive, this);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->AddDevice(device);
+ }
+
+ mDevices.AppendElement(device);
+
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::UpdateDevice(
+ const uint32_t aIndex, const nsACString& aServiceName,
+ const nsACString& aServiceType, const nsACString& aAddress,
+ const uint16_t aPort, const nsACString& aCertFingerprint) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+ device->Update(aServiceName, aServiceType, aAddress, aPort, aCertFingerprint);
+ device->ChangeState(DeviceState::eActive);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->UpdateDevice(device);
+ }
+
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::RemoveDevice(const uint32_t aIndex) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+
+ LOG_I("RemoveDevice: %s", device->Id().get());
+ mDevices.RemoveElementAt(aIndex);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->RemoveDevice(device);
+ }
+
+ return NS_OK;
+}
+
+bool MulticastDNSDeviceProvider::FindDeviceById(const nsACString& aId,
+ uint32_t& aIndex) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(aId,
+ /* aName = */ ""_ns,
+ /* aType = */ ""_ns,
+ /* aHost = */ ""_ns,
+ /* aPort = */ 0,
+ /* aCertFingerprint */ ""_ns,
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceIdComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+bool MulticastDNSDeviceProvider::FindDeviceByAddress(const nsACString& aAddress,
+ uint32_t& aIndex) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(/* aId = */ ""_ns,
+ /* aName = */ ""_ns,
+ /* aType = */ ""_ns, aAddress,
+ /* aPort = */ 0,
+ /* aCertFingerprint */ ""_ns,
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceAddressComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+void MulticastDNSDeviceProvider::MarkAllDevicesUnknown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (auto& device : mDevices) {
+ device->ChangeState(DeviceState::eUnknown);
+ }
+}
+
+void MulticastDNSDeviceProvider::ClearUnknownDevices() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ if (mDevices[i]->State() == DeviceState::eUnknown) {
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+ }
+}
+
+void MulticastDNSDeviceProvider::ClearDevices() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+}
+
+// nsIPresentationDeviceProvider
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::GetListener(
+ nsIPresentationDeviceListener** aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener =
+ do_QueryReferent(mDeviceListener, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ listener.forget(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::SetListener(
+ nsIPresentationDeviceListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDeviceListener = do_GetWeakReference(aListener);
+
+ nsresult rv;
+ if (mDeviceListener) {
+ if (NS_WARN_IF(NS_FAILED(rv = Init()))) {
+ return rv;
+ }
+ } else {
+ Uninit();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::ForceDiscovery() {
+ LOG_I("ForceDiscovery (%d)", mDiscoveryEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDiscoveryEnabled) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mDiscoveryTimer);
+ MOZ_ASSERT(mMulticastDNS);
+
+ // if it's already discovering, extend existing discovery timeout.
+ nsresult rv;
+ if (mIsDiscovering) {
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (NS_WARN_IF(
+ NS_FAILED(rv = mDiscoveryTimer->Init(this, mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ StopDiscovery(NS_OK);
+
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->StartDiscovery(
+ nsLiteralCString(SERVICE_TYPE), mWrappedListener,
+ getter_AddRefs(mDiscoveryRequest))))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// nsIDNSServiceDiscoveryListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnDiscoveryStarted(const nsACString& aServiceType) {
+ LOG_I("OnDiscoveryStarted");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ MarkAllDevicesUnknown();
+
+ nsresult rv;
+ if (NS_WARN_IF(
+ NS_FAILED(rv = mDiscoveryTimer->Init(this, mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+
+ mIsDiscovering = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnDiscoveryStopped(const nsACString& aServiceType) {
+ LOG_I("OnDiscoveryStopped");
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ClearUnknownDevices();
+
+ mIsDiscovering = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceFound(nsIDNSServiceInfo* aServiceInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceFound: %s", serviceName.get());
+
+ if (mMulticastDNS) {
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService(
+ aServiceInfo, mWrappedListener)))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceLost(nsIDNSServiceInfo* aServiceInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceLost: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ uint32_t index;
+ if (!FindDeviceById(host, index)) {
+ // given device was not found
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = RemoveDevice(index)))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnStartDiscoveryFailed(
+ const nsACString& aServiceType, int32_t aErrorCode) {
+ LOG_E("OnStartDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnStopDiscoveryFailed(
+ const nsACString& aServiceType, int32_t aErrorCode) {
+ LOG_E("OnStopDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIDNSRegistrationListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceRegistered(
+ nsIDNSServiceInfo* aServiceInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv;
+
+ nsAutoCString name;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(name)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceRegistered (%s)", name.get());
+ mRegisteredName = name;
+
+ if (mMulticastDNS) {
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService(
+ aServiceInfo, mWrappedListener)))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceUnregistered(
+ nsIDNSServiceInfo* aServiceInfo) {
+ LOG_I("OnServiceUnregistered");
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnRegistrationFailed(
+ nsIDNSServiceInfo* aServiceInfo, int32_t aErrorCode) {
+ LOG_E("OnRegistrationFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRegisterRequest = nullptr;
+
+ if (aErrorCode == nsIDNSRegistrationListener::ERROR_SERVICE_NOT_RUNNING) {
+ return NS_DispatchToMainThread(NewRunnableMethod(
+ "dom::presentation::MulticastDNSDeviceProvider::RegisterMDNSService",
+ this, &MulticastDNSDeviceProvider::RegisterMDNSService));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnUnregistrationFailed(
+ nsIDNSServiceInfo* aServiceInfo, int32_t aErrorCode) {
+ LOG_E("OnUnregistrationFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIDNSServiceResolveListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceResolved: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ if (mRegisteredName == serviceName) {
+ LOG_I("ignore self");
+
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->SetId(host)))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ if (!IsCompatibleServer(aServiceInfo)) {
+ LOG_I("ignore incompatible service: %s", serviceName.get());
+ return NS_OK;
+ }
+
+ nsAutoCString address;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetAddress(address)))) {
+ return rv;
+ }
+
+ uint16_t port;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetPort(&port)))) {
+ return rv;
+ }
+
+ nsAutoCString serviceType;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceType(serviceType)))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ if (NS_WARN_IF(
+ NS_FAILED(aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) ||
+ !propBag) {
+ return rv;
+ }
+
+ nsAutoCString certFingerprint;
+ Unused << propBag->GetPropertyAsACString(
+ nsLiteralString(CERT_FINGERPRINT_TAG), certFingerprint);
+
+ uint32_t index;
+ if (FindDeviceById(host, index)) {
+ return UpdateDevice(index, serviceName, serviceType, address, port,
+ certFingerprint);
+ }
+ return AddDevice(host, serviceName, serviceType, address, port,
+ certFingerprint);
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo,
+ int32_t aErrorCode) {
+ LOG_E("OnResolveFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIPresentationControlServerListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServerReady(uint16_t aPort,
+ const nsACString& aCertFingerprint) {
+ LOG_I("OnServerReady: %d, %s", aPort,
+ PromiseFlatCString(aCertFingerprint).get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mDiscoverable) {
+ RegisterMDNSService();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServerStopped(nsresult aResult) {
+ LOG_I("OnServerStopped: (0x%08" PRIx32 ")", static_cast<uint32_t>(aResult));
+
+ UnregisterMDNSService(aResult);
+
+ // Try restart server if it is stopped abnormally.
+ if (NS_FAILED(aResult) && mDiscoverable) {
+ mIsServerRetrying = true;
+ mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT);
+ }
+
+ return NS_OK;
+}
+
+// Create a new device if we were unable to find one with the address.
+already_AddRefed<MulticastDNSDeviceProvider::Device>
+MulticastDNSDeviceProvider::GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo) {
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ RefPtr<Device> device;
+ uint32_t index;
+ if (FindDeviceByAddress(address, index)) {
+ device = mDevices[index];
+ } else {
+ // Create a one-time device object for non-discoverable controller.
+ // This device will not be in the list of available devices and cannot
+ // be used for requesting session.
+ nsAutoCString id;
+ Unused << aDeviceInfo->GetId(id);
+ uint16_t port;
+ Unused << aDeviceInfo->GetPort(&port);
+
+ device = new Device(id,
+ /* aName = */ id,
+ /* aType = */ ""_ns, address, port,
+ /* aCertFingerprint */ ""_ns, DeviceState::eActive,
+ /* aProvider = */ nullptr);
+ }
+
+ return device.forget();
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnSessionRequest(
+ nsITCPDeviceInfo* aDeviceInfo, const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnSessionRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnSessionRequest(device, aUrl, aPresentationId,
+ aControlChannel);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnTerminateRequest(
+ nsITCPDeviceInfo* aDeviceInfo, const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel, bool aIsFromReceiver) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnTerminateRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnTerminateRequest(device, aPresentationId,
+ aControlChannel, aIsFromReceiver);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnReconnectRequest(
+ nsITCPDeviceInfo* aDeviceInfo, const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnReconnectRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnReconnectRequest(device, aUrl, aPresentationId,
+ aControlChannel);
+ }
+
+ return NS_OK;
+}
+
+// nsIObserver
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ConvertUTF16toUTF8 data(aData);
+ LOG_I("Observe: topic = %s, data = %s", aTopic, data.get());
+
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY)) {
+ OnDiscoveryChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERY));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)) {
+ OnDiscoveryTimeoutChanged(
+ Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERABLE)) {
+ OnDiscoverableChanged(
+ Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DEVICE_NAME)) {
+ nsAutoCString newServiceName;
+ Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME, newServiceName);
+ if (!mServiceName.Equals(newServiceName)) {
+ OnServiceNameChanged(newServiceName);
+ }
+ }
+ } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
+ nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject);
+ if (!timer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (timer == mDiscoveryTimer) {
+ StopDiscovery(NS_OK);
+ } else if (timer == mServerRetryTimer) {
+ mIsServerRetrying = false;
+ StartServer();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::OnDiscoveryChanged(bool aEnabled) {
+ LOG_I("DiscoveryEnabled = %d\n", aEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryEnabled = aEnabled;
+
+ if (mDiscoveryEnabled) {
+ return ForceDiscovery();
+ }
+
+ return StopDiscovery(NS_OK);
+}
+
+nsresult MulticastDNSDeviceProvider::OnDiscoveryTimeoutChanged(
+ uint32_t aTimeoutMs) {
+ LOG_I("OnDiscoveryTimeoutChanged = %d\n", aTimeoutMs);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryTimeoutMs = aTimeoutMs;
+
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::OnDiscoverableChanged(bool aEnabled) {
+ LOG_I("Discoverable = %d\n", aEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoverable = aEnabled;
+
+ if (mDiscoverable) {
+ return StartServer();
+ }
+
+ StopServer();
+ return NS_OK;
+}
+
+nsresult MulticastDNSDeviceProvider::OnServiceNameChanged(
+ const nsACString& aServiceName) {
+ LOG_I("serviceName = %s\n", PromiseFlatCString(aServiceName).get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mServiceName = aServiceName;
+
+ UnregisterMDNSService(NS_OK);
+
+ if (mDiscoverable) {
+ return RegisterMDNSService();
+ }
+
+ return NS_OK;
+}
+
+// MulticastDNSDeviceProvider::Device
+NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider::Device, nsIPresentationDevice)
+
+// nsIPresentationDevice
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetId(nsACString& aId) {
+ aId = mId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetName(nsACString& aName) {
+ aName = mName;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetType(nsACString& aType) {
+ aType = mType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::EstablishControlChannel(
+ nsIPresentationControlChannel** aRetVal) {
+ if (!mProvider) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return mProvider->Connect(this, aRetVal);
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::Disconnect() {
+ // No need to do anything when disconnect.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::IsRequestedUrlSupported(
+ const nsAString& aRequestedUrl, bool* aRetVal) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aRetVal) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ // TV 2.6 also supports presentation Apps and HTTP/HTTPS hosted receiver page.
+ if (DeviceProviderHelpers::IsFxTVSupportedAppUrl(aRequestedUrl) ||
+ DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl)) {
+ *aRetVal = true;
+ }
+
+ return NS_OK;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.h b/dom/presentation/provider/MulticastDNSDeviceProvider.h
new file mode 100644
index 0000000000..ff2866d799
--- /dev/null
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.h
@@ -0,0 +1,185 @@
+/* -*- 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_presentation_provider_MulticastDNSDeviceProvider_h
+#define mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsICancelable.h"
+#include "nsIDNSServiceDiscovery.h"
+#include "nsIObserver.h"
+#include "nsIPresentationDevice.h"
+#include "nsIPresentationDeviceProvider.h"
+#include "nsIPresentationControlService.h"
+#include "nsITimer.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsITCPDeviceInfo;
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class DNSServiceWrappedListener;
+class MulticastDNSService;
+
+class MulticastDNSDeviceProvider final
+ : public nsIPresentationDeviceProvider,
+ public nsIDNSServiceDiscoveryListener,
+ public nsIDNSRegistrationListener,
+ public nsIDNSServiceResolveListener,
+ public nsIPresentationControlServerListener,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICEPROVIDER
+ NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER
+ NS_DECL_NSIDNSREGISTRATIONLISTENER
+ NS_DECL_NSIDNSSERVICERESOLVELISTENER
+ NS_DECL_NSIPRESENTATIONCONTROLSERVERLISTENER
+ NS_DECL_NSIOBSERVER
+
+ explicit MulticastDNSDeviceProvider();
+ nsresult Init();
+ void Uninit();
+
+ private:
+ enum class DeviceState : uint32_t { eUnknown, eActive };
+
+ class Device final : public nsIPresentationDevice {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICE
+
+ explicit Device(const nsACString& aId, const nsACString& aName,
+ const nsACString& aType, const nsACString& aAddress,
+ const uint16_t aPort, const nsACString& aCertFingerprint,
+ DeviceState aState, MulticastDNSDeviceProvider* aProvider)
+ : mId(aId),
+ mName(aName),
+ mType(aType),
+ mAddress(aAddress),
+ mPort(aPort),
+ mCertFingerprint(aCertFingerprint),
+ mState(aState),
+ mProvider(aProvider) {}
+
+ const nsCString& Id() const { return mId; }
+
+ const nsCString& Address() const { return mAddress; }
+
+ uint16_t Port() const { return mPort; }
+
+ const nsCString& CertFingerprint() const { return mCertFingerprint; }
+
+ DeviceState State() const { return mState; }
+
+ void ChangeState(DeviceState aState) { mState = aState; }
+
+ void Update(const nsACString& aName, const nsACString& aType,
+ const nsACString& aAddress, const uint16_t aPort,
+ const nsACString& aCertFingerprint) {
+ mName = aName;
+ mType = aType;
+ mAddress = aAddress;
+ mPort = aPort;
+ mCertFingerprint = aCertFingerprint;
+ }
+
+ private:
+ virtual ~Device() = default;
+
+ nsCString mId;
+ nsCString mName;
+ nsCString mType;
+ nsCString mAddress;
+ uint16_t mPort;
+ nsCString mCertFingerprint;
+ DeviceState mState;
+ MulticastDNSDeviceProvider* mProvider;
+ };
+
+ struct DeviceIdComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Id() == aB->Id();
+ }
+ };
+
+ struct DeviceAddressComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Address() == aB->Address();
+ }
+ };
+
+ virtual ~MulticastDNSDeviceProvider();
+ nsresult StartServer();
+ void StopServer();
+ void AbortServerRetry();
+ nsresult RegisterMDNSService();
+ void UnregisterMDNSService(nsresult aReason);
+ nsresult StopDiscovery(nsresult aReason);
+ nsresult Connect(Device* aDevice, nsIPresentationControlChannel** aRetVal);
+ bool IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo);
+
+ // device manipulation
+ nsresult AddDevice(const nsACString& aId, const nsACString& aServiceName,
+ const nsACString& aServiceType, const nsACString& aAddress,
+ const uint16_t aPort, const nsACString& aCertFingerprint);
+ nsresult UpdateDevice(const uint32_t aIndex, const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress, const uint16_t aPort,
+ const nsACString& aCertFingerprint);
+ nsresult RemoveDevice(const uint32_t aIndex);
+ bool FindDeviceById(const nsACString& aId, uint32_t& aIndex);
+
+ bool FindDeviceByAddress(const nsACString& aAddress, uint32_t& aIndex);
+
+ already_AddRefed<Device> GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo);
+
+ void MarkAllDevicesUnknown();
+ void ClearUnknownDevices();
+ void ClearDevices();
+
+ // preferences
+ nsresult OnDiscoveryChanged(bool aEnabled);
+ nsresult OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs);
+ nsresult OnDiscoverableChanged(bool aEnabled);
+ nsresult OnServiceNameChanged(const nsACString& aServiceName);
+
+ bool mInitialized = false;
+ nsWeakPtr mDeviceListener;
+ nsCOMPtr<nsIPresentationControlService> mPresentationService;
+ nsCOMPtr<nsIDNSServiceDiscovery> mMulticastDNS;
+ RefPtr<DNSServiceWrappedListener> mWrappedListener;
+
+ nsCOMPtr<nsICancelable> mDiscoveryRequest;
+ nsCOMPtr<nsICancelable> mRegisterRequest;
+
+ nsTArray<RefPtr<Device>> mDevices;
+
+ bool mDiscoveryEnabled = false;
+ bool mIsDiscovering = false;
+ uint32_t mDiscoveryTimeoutMs;
+ nsCOMPtr<nsITimer> mDiscoveryTimer;
+
+ bool mDiscoverable = false;
+ bool mDiscoverableEncrypted = false;
+ bool mIsServerRetrying = false;
+ uint32_t mServerRetryMs;
+ nsCOMPtr<nsITimer> mServerRetryTimer;
+
+ nsCString mServiceName;
+ nsCString mRegisteredName;
+};
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h
diff --git a/dom/presentation/provider/PresentationControlService.jsm b/dom/presentation/provider/PresentationControlService.jsm
new file mode 100644
index 0000000000..07e46d1f1c
--- /dev/null
+++ b/dom/presentation/provider/PresentationControlService.jsm
@@ -0,0 +1,1054 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const { clearTimeout, setTimeout } = ChromeUtils.import(
+ "resource://gre/modules/Timer.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ControllerStateMachine",
+ "resource://gre/modules/presentation/ControllerStateMachine.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ReceiverStateMachine",
+ "resource://gre/modules/presentation/ReceiverStateMachine.jsm"
+);
+
+const kProtocolVersion = 1; // need to review isCompatibleServer while fiddling the version number.
+const kLocalCertName = "presentation";
+
+const DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug");
+function log(aMsg) {
+ dump("-*- PresentationControlService.js: " + aMsg + "\n");
+}
+
+function TCPDeviceInfo(aAddress, aPort, aId, aCertFingerprint) {
+ this.address = aAddress;
+ this.port = aPort;
+ this.id = aId;
+ this.certFingerprint = aCertFingerprint || "";
+}
+
+function PresentationControlService() {
+ this._id = null;
+ this._port = 0;
+ this._serverSocket = null;
+}
+
+PresentationControlService.prototype = {
+ /**
+ * If a user agent connects to this server, we create a control channel but
+ * hand it to |TCPDevice.listener| when the initial information exchange
+ * finishes. Therefore, we hold the control channels in this period.
+ */
+ _controlChannels: [],
+
+ startServer(aEncrypted, aPort) {
+ if (this._isServiceInit()) {
+ DEBUG &&
+ log("PresentationControlService - server socket has been initialized");
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ /**
+ * 0 or undefined indicates opt-out parameter, and a port will be selected
+ * automatically.
+ */
+ let serverSocketPort =
+ typeof aPort !== "undefined" && aPort !== 0 ? aPort : -1;
+
+ if (aEncrypted) {
+ let self = this;
+ let localCertService = Cc[
+ "@mozilla.org/security/local-cert-service;1"
+ ].getService(Ci.nsILocalCertService);
+ localCertService.getOrCreateCert(kLocalCertName, {
+ handleCert(aCert, aRv) {
+ DEBUG && log("PresentationControlService - handleCert");
+ if (aRv) {
+ self._notifyServerStopped(aRv);
+ } else {
+ self._serverSocket = Cc[
+ "@mozilla.org/network/tls-server-socket;1"
+ ].createInstance(Ci.nsITLSServerSocket);
+
+ self._serverSocketInit(serverSocketPort, aCert);
+ }
+ },
+ });
+ } else {
+ this._serverSocket = Cc[
+ "@mozilla.org/network/server-socket;1"
+ ].createInstance(Ci.nsIServerSocket);
+
+ this._serverSocketInit(serverSocketPort, null);
+ }
+ },
+
+ _serverSocketInit(aPort, aCert) {
+ if (!this._serverSocket) {
+ DEBUG && log("PresentationControlService - create server socket fail.");
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ try {
+ this._serverSocket.init(aPort, false, -1);
+
+ if (aCert) {
+ this._serverSocket.serverCert = aCert;
+ this._serverSocket.setSessionTickets(false);
+ let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+ this._serverSocket.setRequestClientCertificate(requestCert);
+ }
+
+ this._serverSocket.asyncListen(this);
+ } catch (e) {
+ // NS_ERROR_SOCKET_ADDRESS_IN_USE
+ DEBUG &&
+ log("PresentationControlService - init server socket fail: " + e);
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ this._port = this._serverSocket.port;
+
+ DEBUG &&
+ log("PresentationControlService - service start on port: " + this._port);
+
+ // Monitor network interface change to restart server socket.
+ Services.obs.addObserver(this, "network:offline-status-changed");
+
+ this._notifyServerReady();
+ },
+
+ _notifyServerReady() {
+ Services.tm.dispatchToMainThread(() => {
+ if (this._listener) {
+ this._listener.onServerReady(this._port, this.certFingerprint);
+ }
+ });
+ },
+
+ _notifyServerStopped(aRv) {
+ Services.tm.dispatchToMainThread(() => {
+ if (this._listener) {
+ this._listener.onServerStopped(aRv);
+ }
+ });
+ },
+
+ isCompatibleServer(aVersion) {
+ // No compatibility issue for the first version of control protocol
+ return this.version === aVersion;
+ },
+
+ get id() {
+ return this._id;
+ },
+
+ set id(aId) {
+ this._id = aId;
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ get version() {
+ return kProtocolVersion;
+ },
+
+ get certFingerprint() {
+ if (!this._serverSocket.serverCert) {
+ return null;
+ }
+
+ return this._serverSocket.serverCert.sha256Fingerprint;
+ },
+
+ set listener(aListener) {
+ this._listener = aListener;
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ _isServiceInit() {
+ return this._serverSocket !== null;
+ },
+
+ connect(aDeviceInfo) {
+ if (!this.id) {
+ DEBUG &&
+ log(
+ "PresentationControlService - Id has not initialized; connect fails"
+ );
+ return null;
+ }
+ DEBUG && log("PresentationControlService - connect to " + aDeviceInfo.id);
+
+ let socketTransport = this._attemptConnect(aDeviceInfo);
+ return new TCPControlChannel(this, socketTransport, aDeviceInfo, "sender");
+ },
+
+ _attemptConnect(aDeviceInfo) {
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+
+ let socketTransport;
+ try {
+ if (aDeviceInfo.certFingerprint) {
+ let overrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ overrideService.rememberTemporaryValidityOverrideUsingFingerprint(
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ aDeviceInfo.certFingerprint,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH
+ );
+
+ socketTransport = sts.createTransport(
+ ["ssl"],
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ null
+ );
+ } else {
+ socketTransport = sts.createTransport(
+ [],
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ null
+ );
+ }
+ // Shorten the connection failure procedure.
+ socketTransport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
+ } catch (e) {
+ DEBUG && log("PresentationControlService - createTransport throws: " + e);
+ // Pop the exception to |TCPDevice.establishControlChannel|
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ return socketTransport;
+ },
+
+ responseSession(aDeviceInfo, aSocketTransport) {
+ if (!this._isServiceInit()) {
+ DEBUG &&
+ log(
+ "PresentationControlService - should never receive remote " +
+ "session request before server socket initialization"
+ );
+ return null;
+ }
+ DEBUG &&
+ log(
+ "PresentationControlService - responseSession to " +
+ JSON.stringify(aDeviceInfo)
+ );
+ return new TCPControlChannel(
+ this,
+ aSocketTransport,
+ aDeviceInfo,
+ "receiver"
+ );
+ },
+
+ // Triggered by TCPControlChannel
+ onSessionRequest(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
+ DEBUG &&
+ log(
+ "PresentationControlService - onSessionRequest: " +
+ aDeviceInfo.address +
+ ":" +
+ aDeviceInfo.port
+ );
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onSessionRequest(
+ aDeviceInfo,
+ aUrl,
+ aPresentationId,
+ aControlChannel
+ );
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ onSessionTerminate(
+ aDeviceInfo,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver
+ ) {
+ DEBUG &&
+ log(
+ "TCPPresentationServer - onSessionTerminate: " +
+ aDeviceInfo.address +
+ ":" +
+ aDeviceInfo.port
+ );
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onTerminateRequest(
+ aDeviceInfo,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver
+ );
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ onSessionReconnect(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
+ DEBUG &&
+ log(
+ "TCPPresentationServer - onSessionReconnect: " +
+ aDeviceInfo.address +
+ ":" +
+ aDeviceInfo.port
+ );
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onReconnectRequest(
+ aDeviceInfo,
+ aUrl,
+ aPresentationId,
+ aControlChannel
+ );
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ // nsIServerSocketListener (Triggered by nsIServerSocket.init)
+ onSocketAccepted(aServerSocket, aClientSocket) {
+ DEBUG &&
+ log(
+ "PresentationControlService - onSocketAccepted: " +
+ aClientSocket.host +
+ ":" +
+ aClientSocket.port
+ );
+ let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
+ this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket));
+ },
+
+ holdControlChannel(aControlChannel) {
+ this._controlChannels.push(aControlChannel);
+ },
+
+ releaseControlChannel(aControlChannel) {
+ let index = this._controlChannels.indexOf(aControlChannel);
+ if (index !== -1) {
+ delete this._controlChannels[index];
+ }
+ },
+
+ // nsIServerSocketListener (Triggered by nsIServerSocket.init)
+ onStopListening(aServerSocket, aStatus) {
+ DEBUG && log("PresentationControlService - onStopListening: " + aStatus);
+ },
+
+ close() {
+ DEBUG && log("PresentationControlService - close");
+ if (this._isServiceInit()) {
+ DEBUG && log("PresentationControlService - close server socket");
+ this._serverSocket.close();
+ this._serverSocket = null;
+
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+
+ this._notifyServerStopped(Cr.NS_OK);
+ }
+ this._port = 0;
+ },
+
+ // nsIObserver
+ observe(aSubject, aTopic, aData) {
+ DEBUG && log("PresentationControlService - observe: " + aTopic);
+ switch (aTopic) {
+ case "network:offline-status-changed": {
+ if (aData == "offline") {
+ DEBUG && log("network offline");
+ return;
+ }
+ this._restartServer();
+ break;
+ }
+ }
+ },
+
+ _restartServer() {
+ DEBUG && log("PresentationControlService - restart service");
+
+ // restart server socket
+ if (this._isServiceInit()) {
+ this.close();
+
+ try {
+ this.startServer();
+ } catch (e) {
+ DEBUG && log("PresentationControlService - restart service fail: " + e);
+ }
+ }
+ },
+
+ classID: Components.ID("{f4079b8b-ede5-4b90-a112-5b415a931deb}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIServerSocketListener",
+ "nsIPresentationControlService",
+ "nsIObserver",
+ ]),
+};
+
+function ChannelDescription(aInit) {
+ this._type = aInit.type;
+ switch (this._type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ this._tcpAddresses = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ for (let address of aInit.tcpAddress) {
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"].createInstance(
+ Ci.nsISupportsCString
+ );
+ wrapper.data = address;
+ this._tcpAddresses.appendElement(wrapper);
+ }
+
+ this._tcpPort = aInit.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ this._dataChannelSDP = aInit.dataChannelSDP;
+ break;
+ }
+}
+
+ChannelDescription.prototype = {
+ _type: 0,
+ _tcpAddresses: null,
+ _tcpPort: 0,
+ _dataChannelSDP: "",
+
+ get type() {
+ return this._type;
+ },
+
+ get tcpAddress() {
+ return this._tcpAddresses;
+ },
+
+ get tcpPort() {
+ return this._tcpPort;
+ },
+
+ get dataChannelSDP() {
+ return this._dataChannelSDP;
+ },
+
+ classID: Components.ID("{82507aea-78a2-487e-904a-858a6c5bf4e1}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+};
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function discriptionAsJson(aDescription) {
+ let json = {};
+ json.type = aDescription.type;
+ switch (aDescription.type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+ json.tcpAddress = [];
+ for (let idx = 0; idx < addresses.length; idx++) {
+ let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+ json.tcpAddress.push(address.data);
+ }
+ json.tcpPort = aDescription.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ json.dataChannelSDP = aDescription.dataChannelSDP;
+ break;
+ }
+ return json;
+}
+
+const kDisconnectTimeout = 5000;
+const kTerminateTimeout = 5000;
+
+function TCPControlChannel(
+ presentationService,
+ transport,
+ deviceInfo,
+ direction
+) {
+ DEBUG && log("create TCPControlChannel for : " + direction);
+ this._deviceInfo = deviceInfo;
+ this._direction = direction;
+ this._transport = transport;
+
+ this._presentationService = presentationService;
+
+ if (direction === "receiver") {
+ // Need to set security observer before I/O stream operation.
+ this._setSecurityObserver(this);
+ }
+
+ let currentThread = Services.tm.currentThread;
+ transport.setEventSink(this, currentThread);
+
+ this._input = this._transport
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this._input.asyncWait(
+ this.QueryInterface(Ci.nsIStreamListener),
+ Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY,
+ 0,
+ currentThread
+ );
+
+ this._output = this._transport
+ .openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED, 0, 0)
+ .QueryInterface(Ci.nsIAsyncOutputStream);
+
+ this._outgoingMsgs = [];
+
+ this._stateMachine =
+ direction === "sender"
+ ? new ControllerStateMachine(this, presentationService.id)
+ : new ReceiverStateMachine(this);
+
+ if (direction === "receiver" && !transport.securityInfo) {
+ // Since the transport created by server socket is already CONNECTED_TO.
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ }
+}
+
+TCPControlChannel.prototype = {
+ _outgoingEnabled: false,
+ _incomingEnabled: false,
+ _pendingOpen: false,
+ _pendingOffer: null,
+ _pendingAnswer: null,
+ _pendingClose: null,
+ _pendingCloseReason: null,
+ _pendingReconnect: false,
+
+ sendOffer(aOffer) {
+ this._stateMachine.sendOffer(discriptionAsJson(aOffer));
+ },
+
+ sendAnswer(aAnswer) {
+ this._stateMachine.sendAnswer(discriptionAsJson(aAnswer));
+ },
+
+ sendIceCandidate(aCandidate) {
+ this._stateMachine.updateIceCandidate(aCandidate);
+ },
+
+ launch(aPresentationId, aUrl) {
+ this._stateMachine.launch(aPresentationId, aUrl);
+ },
+
+ terminate(aPresentationId) {
+ if (!this._terminatingId) {
+ this._terminatingId = aPresentationId;
+ this._stateMachine.terminate(aPresentationId);
+
+ // Start a guard timer to ensure terminateAck is processed.
+ this._terminateTimer = setTimeout(() => {
+ DEBUG &&
+ log("TCPControlChannel - terminate timeout: " + aPresentationId);
+ delete this._terminateTimer;
+ if (this._pendingDisconnect) {
+ this._pendingDisconnect();
+ } else {
+ this.disconnect(Cr.NS_OK);
+ }
+ }, kTerminateTimeout);
+ } else {
+ this._stateMachine.terminateAck(aPresentationId);
+ delete this._terminatingId;
+ }
+ },
+
+ _flushOutgoing() {
+ if (!this._outgoingEnabled || this._outgoingMsgs.length === 0) {
+ return;
+ }
+
+ this._output.asyncWait(this, 0, 0, Services.tm.currentThread);
+ },
+
+ // may throw an exception
+ _send(aMsg) {
+ DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2));
+
+ /**
+ * XXX In TCP streaming, it is possible that more than one message in one
+ * TCP packet. We use line delimited JSON to identify where one JSON encoded
+ * object ends and the next begins. Therefore, we do not allow newline
+ * characters whithin the whole message, and add a newline at the end.
+ * Please see the parser code in |onDataAvailable|.
+ */
+ let message = JSON.stringify(aMsg).replace(["\n"], "") + "\n";
+ try {
+ this._output.write(message, message.length);
+ } catch (e) {
+ DEBUG && log("TCPControlChannel - Failed to send message: " + e.name);
+ throw e;
+ }
+ },
+
+ _setSecurityObserver(observer) {
+ if (this._transport && this._transport.securityInfo) {
+ DEBUG && log("TCPControlChannel - setSecurityObserver: " + observer);
+ let connectionInfo = this._transport.securityInfo.QueryInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(observer);
+ }
+ },
+
+ // nsITLSServerSecurityObserver
+ onHandshakeDone(socket, clientStatus) {
+ log(
+ "TCPControlChannel - onHandshakeDone: TLS version: " +
+ clientStatus.tlsVersionUsed.toString(16)
+ );
+ this._setSecurityObserver(null);
+
+ // Process input/output after TLS handshake is complete.
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ },
+
+ // nsIAsyncOutputStream
+ onOutputStreamReady() {
+ DEBUG && log("TCPControlChannel - onOutputStreamReady");
+ if (this._outgoingMsgs.length === 0) {
+ return;
+ }
+
+ try {
+ this._send(this._outgoingMsgs[0]);
+ } catch (e) {
+ if (e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this._output.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ }
+
+ this._closeTransport();
+ return;
+ }
+ this._outgoingMsgs.shift();
+ this._flushOutgoing();
+ },
+
+ // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait)
+ // Only used for detecting connection refused
+ onInputStreamReady(aStream) {
+ DEBUG && log("TCPControlChannel - onInputStreamReady");
+ try {
+ aStream.available();
+ } catch (e) {
+ DEBUG && log("TCPControlChannel - onInputStreamReady error: " + e.name);
+ // NS_ERROR_CONNECTION_REFUSED
+ this._notifyDisconnected(e.result);
+ }
+ },
+
+ // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink)
+ onTransportStatus(aTransport, aStatus) {
+ DEBUG &&
+ log(
+ "TCPControlChannel - onTransportStatus: " +
+ aStatus.toString(16) +
+ " with role: " +
+ this._direction
+ );
+ if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ }
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStartRequest() {
+ DEBUG &&
+ log("TCPControlChannel - onStartRequest with role: " + this._direction);
+ this._incomingEnabled = true;
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStopRequest(aRequest, aContext, aStatus) {
+ DEBUG &&
+ log(
+ "TCPControlChannel - onStopRequest: " +
+ aStatus +
+ " with role: " +
+ this._direction
+ );
+ this._stateMachine.onChannelClosed(aStatus, true);
+ },
+
+ // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead)
+ onDataAvailable(aRequest, aInputStream) {
+ let data = NetUtil.readInputStreamToString(
+ aInputStream,
+ aInputStream.available()
+ );
+ DEBUG && log("TCPControlChannel - onDataAvailable: " + data);
+
+ // Parser of line delimited JSON. Please see |_send| for more informaiton.
+ let jsonArray = data.split("\n");
+ jsonArray.pop();
+ for (let json of jsonArray) {
+ let msg;
+ try {
+ msg = JSON.parse(json);
+ } catch (e) {
+ DEBUG && log("TCPSignalingChannel - error in parsing json: " + e);
+ }
+
+ this._handleMessage(msg);
+ }
+ },
+
+ _createInputStreamPump() {
+ if (this._pump) {
+ return;
+ }
+
+ DEBUG &&
+ log("TCPControlChannel - create pump with role: " + this._direction);
+ this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this._pump.init(this._input, 0, 0, false);
+ this._pump.asyncRead(this, null);
+ this._stateMachine.onChannelReady();
+ },
+
+ // Handle command from remote side
+ _handleMessage(aMsg) {
+ DEBUG &&
+ log(
+ "TCPControlChannel - handleMessage from " +
+ JSON.stringify(this._deviceInfo) +
+ ": " +
+ JSON.stringify(aMsg)
+ );
+ this._stateMachine.onCommand(aMsg);
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ set listener(aListener) {
+ DEBUG && log("TCPControlChannel - set listener: " + aListener);
+ if (!aListener) {
+ this._listener = null;
+ return;
+ }
+
+ this._listener = aListener;
+ if (this._pendingOpen) {
+ this._pendingOpen = false;
+ DEBUG && log("TCPControlChannel - notify pending opened");
+ this._listener.notifyConnected();
+ }
+
+ if (this._pendingOffer) {
+ let offer = this._pendingOffer;
+ DEBUG &&
+ log(
+ "TCPControlChannel - notify pending offer: " + JSON.stringify(offer)
+ );
+ this._listener.onOffer(new ChannelDescription(offer));
+ this._pendingOffer = null;
+ }
+
+ if (this._pendingAnswer) {
+ let answer = this._pendingAnswer;
+ DEBUG &&
+ log(
+ "TCPControlChannel - notify pending answer: " + JSON.stringify(answer)
+ );
+ this._listener.onAnswer(new ChannelDescription(answer));
+ this._pendingAnswer = null;
+ }
+
+ if (this._pendingClose) {
+ DEBUG && log("TCPControlChannel - notify pending closed");
+ this._notifyDisconnected(this._pendingCloseReason);
+ this._pendingClose = null;
+ }
+
+ if (this._pendingReconnect) {
+ DEBUG && log("TCPControlChannel - notify pending reconnected");
+ this._notifyReconnected();
+ this._pendingReconnect = false;
+ }
+ },
+
+ /**
+ * These functions are designed to handle the interaction with listener
+ * appropriately. |_FUNC| is to handle |this._listener.FUNC|.
+ */
+ _onOffer(aOffer) {
+ if (!this._incomingEnabled) {
+ return;
+ }
+ if (!this._listener) {
+ this._pendingOffer = aOffer;
+ return;
+ }
+ DEBUG && log("TCPControlChannel - notify offer: " + JSON.stringify(aOffer));
+ this._listener.onOffer(new ChannelDescription(aOffer));
+ },
+
+ _onAnswer(aAnswer) {
+ if (!this._incomingEnabled) {
+ return;
+ }
+ if (!this._listener) {
+ this._pendingAnswer = aAnswer;
+ return;
+ }
+ DEBUG &&
+ log("TCPControlChannel - notify answer: " + JSON.stringify(aAnswer));
+ this._listener.onAnswer(new ChannelDescription(aAnswer));
+ },
+
+ _notifyConnected() {
+ this._pendingClose = false;
+ this._pendingCloseReason = Cr.NS_OK;
+
+ if (!this._listener) {
+ this._pendingOpen = true;
+ return;
+ }
+
+ DEBUG &&
+ log("TCPControlChannel - notify opened with role: " + this._direction);
+ this._listener.notifyConnected();
+ },
+
+ _notifyDisconnected(aReason) {
+ this._pendingOpen = false;
+ this._pendingOffer = null;
+ this._pendingAnswer = null;
+
+ // Remote endpoint closes the control channel with abnormal reason.
+ if (aReason == Cr.NS_OK && this._pendingCloseReason != Cr.NS_OK) {
+ aReason = this._pendingCloseReason;
+ }
+
+ if (!this._listener) {
+ this._pendingClose = true;
+ this._pendingCloseReason = aReason;
+ return;
+ }
+
+ DEBUG &&
+ log("TCPControlChannel - notify closed with role: " + this._direction);
+ this._listener.notifyDisconnected(aReason);
+ },
+
+ _notifyReconnected() {
+ if (!this._listener) {
+ this._pendingReconnect = true;
+ return;
+ }
+
+ DEBUG &&
+ log(
+ "TCPControlChannel - notify reconnected with role: " + this._direction
+ );
+ this._listener.notifyReconnected();
+ },
+
+ _closeOutgoing() {
+ if (this._outgoingEnabled) {
+ this._output.close();
+ this._outgoingEnabled = false;
+ }
+ },
+ _closeIncoming() {
+ if (this._incomingEnabled) {
+ this._pump = null;
+ this._input.close();
+ this._incomingEnabled = false;
+ }
+ },
+ _closeTransport() {
+ if (this._disconnectTimer) {
+ clearTimeout(this._disconnectTimer);
+ delete this._disconnectTimer;
+ }
+
+ if (this._terminateTimer) {
+ clearTimeout(this._terminateTimer);
+ delete this._terminateTimer;
+ }
+
+ delete this._pendingDisconnect;
+
+ this._transport.setEventSink(null, null);
+
+ this._closeIncoming();
+ this._closeOutgoing();
+ this._presentationService.releaseControlChannel(this);
+ },
+
+ disconnect(aReason) {
+ DEBUG && log("TCPControlChannel - disconnect with reason: " + aReason);
+
+ // Pending disconnect during termination procedure.
+ if (this._terminateTimer) {
+ // Store only the first disconnect action.
+ if (!this._pendingDisconnect) {
+ this._pendingDisconnect = this.disconnect.bind(this, aReason);
+ }
+ return;
+ }
+
+ if (this._outgoingEnabled && !this._disconnectTimer) {
+ // default reason is NS_OK
+ aReason = !aReason ? Cr.NS_OK : aReason;
+
+ this._stateMachine.onChannelClosed(aReason, false);
+
+ // Start a guard timer to ensure the transport will be closed.
+ this._disconnectTimer = setTimeout(() => {
+ DEBUG && log("TCPControlChannel - disconnect timeout");
+ this._closeTransport();
+ }, kDisconnectTimeout);
+ }
+ },
+
+ reconnect(aPresentationId, aUrl) {
+ DEBUG && log("TCPControlChannel - reconnect with role: " + this._direction);
+ if (this._direction != "sender") {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ this._stateMachine.reconnect(aPresentationId, aUrl);
+ },
+
+ // callback from state machine
+ sendCommand(command) {
+ this._outgoingMsgs.push(command);
+ this._flushOutgoing();
+ },
+
+ notifyDeviceConnected(deviceId) {
+ switch (this._direction) {
+ case "receiver":
+ this._deviceInfo.id = deviceId;
+ break;
+ }
+ this._notifyConnected();
+ },
+
+ notifyDisconnected(reason) {
+ this._closeTransport();
+ this._notifyDisconnected(reason);
+ },
+
+ notifyLaunch(presentationId, url) {
+ switch (this._direction) {
+ case "receiver":
+ this._presentationService.onSessionRequest(
+ this._deviceInfo,
+ url,
+ presentationId,
+ this
+ );
+ break;
+ }
+ },
+
+ notifyTerminate(presentationId) {
+ if (!this._terminatingId) {
+ this._terminatingId = presentationId;
+ this._presentationService.onSessionTerminate(
+ this._deviceInfo,
+ presentationId,
+ this,
+ this._direction === "sender"
+ );
+ return;
+ }
+
+ // Cancel terminate guard timer after receiving terminate-ack.
+ if (this._terminateTimer) {
+ clearTimeout(this._terminateTimer);
+ delete this._terminateTimer;
+ }
+
+ if (this._terminatingId !== presentationId) {
+ // Requested presentation Id doesn't matched with the one in ACK.
+ // Disconnect the control channel with error.
+ DEBUG &&
+ log("TCPControlChannel - unmatched terminatingId: " + presentationId);
+ this.disconnect(Cr.NS_ERROR_FAILURE);
+ }
+
+ delete this._terminatingId;
+ if (this._pendingDisconnect) {
+ this._pendingDisconnect();
+ }
+ },
+
+ notifyReconnect(presentationId, url) {
+ switch (this._direction) {
+ case "receiver":
+ this._presentationService.onSessionReconnect(
+ this._deviceInfo,
+ url,
+ presentationId,
+ this
+ );
+ break;
+ case "sender":
+ this._notifyReconnected();
+ break;
+ }
+ },
+
+ notifyOffer(offer) {
+ this._onOffer(offer);
+ },
+
+ notifyAnswer(answer) {
+ this._onAnswer(answer);
+ },
+
+ notifyIceCandidate(candidate) {
+ this._listener.onIceCandidate(candidate);
+ },
+
+ classID: Components.ID("{fefb8286-0bdc-488b-98bf-0c11b485c955}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannel",
+ "nsIStreamListener",
+ ]),
+};
+
+var EXPORTED_SYMBOLS = ["PresentationControlService"];
diff --git a/dom/presentation/provider/ReceiverStateMachine.jsm b/dom/presentation/provider/ReceiverStateMachine.jsm
new file mode 100644
index 0000000000..aaad83a20e
--- /dev/null
+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
@@ -0,0 +1,232 @@
+/* -*- 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ReceiverStateMachine"];
+
+const { CommandType, State } = ChromeUtils.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));
+ },
+ function _connectingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.CONNECT:
+ stateMachine._sendCommand({
+ type: CommandType.CONNECT_ACK,
+ });
+ stateMachine.state = State.CONNECTED;
+ stateMachine._notifyDeviceConnected(command.deviceId);
+ break;
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _connectedHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ case CommandType.LAUNCH:
+ stateMachine._notifyLaunch(command.presentationId, command.url);
+ stateMachine._sendCommand({
+ type: CommandType.LAUNCH_ACK,
+ presentationId: command.presentationId,
+ });
+ break;
+ case CommandType.TERMINATE:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.TERMINATE_ACK:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.OFFER:
+ case CommandType.ICE_CANDIDATE:
+ stateMachine._notifyChannelDescriptor(command);
+ break;
+ case CommandType.RECONNECT:
+ stateMachine._notifyReconnect(command.presentationId, command.url);
+ stateMachine._sendCommand({
+ type: CommandType.RECONNECT_ACK,
+ presentationId: command.presentationId,
+ });
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _closingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _closedHandler(stateMachine, command) {
+ // ignore every command in closed state.
+ DEBUG && debug("unexpected command: " + JSON.stringify(command));
+ },
+];
+
+function ReceiverStateMachine(channel) {
+ this.state = State.INIT;
+ this._channel = channel;
+}
+
+ReceiverStateMachine.prototype = {
+ launch: function _launch() {
+ // presentation session can only be launched by controlling UA.
+ debug("receiver shouldn't trigger launch");
+ },
+
+ terminate: function _terminate(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE,
+ presentationId,
+ });
+ }
+ },
+
+ terminateAck: function _terminateAck(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE_ACK,
+ presentationId,
+ });
+ }
+ },
+
+ reconnect: function _reconnect() {
+ debug("receiver shouldn't trigger reconnect");
+ },
+
+ sendOffer: function _sendOffer() {
+ // offer can only be sent by controlling UA.
+ debug("receiver shouldn't generate offer");
+ },
+
+ sendAnswer: function _sendAnswer(answer) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.ANSWER,
+ answer,
+ });
+ }
+ },
+
+ updateIceCandidate: function _updateIceCandidate(candidate) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.ICE_CANDIDATE,
+ candidate,
+ });
+ }
+ },
+
+ onCommand: function _onCommand(command) {
+ handlers[this.state](this, command);
+ },
+
+ onChannelReady: function _onChannelReady() {
+ if (this.state === State.INIT) {
+ this.state = State.CONNECTING;
+ }
+ },
+
+ onChannelClosed: function _onChannelClose(reason, isByRemote) {
+ switch (this.state) {
+ case State.CONNECTED:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ this._notifyDisconnected(reason);
+ } else {
+ this._sendCommand({
+ type: CommandType.DISCONNECT,
+ reason,
+ });
+ this.state = State.CLOSING;
+ this._closeReason = reason;
+ }
+ break;
+ case State.CLOSING:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ if (this._closeReason) {
+ reason = this._closeReason;
+ delete this._closeReason;
+ }
+ this._notifyDisconnected(reason);
+ } else {
+ // do nothing and wait for remote channel closed.
+ }
+ break;
+ default:
+ DEBUG &&
+ debug("unexpected channel close: " + reason + ", " + isByRemote);
+ break;
+ }
+ },
+
+ _sendCommand: function _sendCommand(command) {
+ this._channel.sendCommand(command);
+ },
+
+ _notifyDeviceConnected: function _notifyDeviceConnected(deviceName) {
+ this._channel.notifyDeviceConnected(deviceName);
+ },
+
+ _notifyDisconnected: function _notifyDisconnected(reason) {
+ this._channel.notifyDisconnected(reason);
+ },
+
+ _notifyLaunch: function _notifyLaunch(presentationId, url) {
+ this._channel.notifyLaunch(presentationId, url);
+ },
+
+ _notifyTerminate: function _notifyTerminate(presentationId) {
+ this._channel.notifyTerminate(presentationId);
+ },
+
+ _notifyReconnect: function _notifyReconnect(presentationId, url) {
+ this._channel.notifyReconnect(presentationId, url);
+ },
+
+ _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
+ switch (command.type) {
+ case CommandType.OFFER:
+ this._channel.notifyOffer(command.offer);
+ break;
+ case CommandType.ICE_CANDIDATE:
+ this._channel.notifyIceCandidate(command.candidate);
+ break;
+ }
+ },
+};
diff --git a/dom/presentation/provider/StateMachineHelper.jsm b/dom/presentation/provider/StateMachineHelper.jsm
new file mode 100644
index 0000000000..cc2dc02895
--- /dev/null
+++ b/dom/presentation/provider/StateMachineHelper.jsm
@@ -0,0 +1,38 @@
+/* -*- 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["State", "CommandType"];
+
+const State = Object.freeze({
+ INIT: 0,
+ CONNECTING: 1,
+ CONNECTED: 2,
+ CLOSING: 3,
+ CLOSED: 4,
+});
+
+const CommandType = Object.freeze({
+ // control channel life cycle
+ CONNECT: "connect", // { deviceId: <string> }
+ CONNECT_ACK: "connect-ack", // { presentationId: <string> }
+ DISCONNECT: "disconnect", // { reason: <int> }
+ // presentation session life cycle
+ LAUNCH: "launch", // { presentationId: <string>, url: <string> }
+ LAUNCH_ACK: "launch-ack", // { presentationId: <string> }
+ TERMINATE: "terminate", // { presentationId: <string> }
+ TERMINATE_ACK: "terminate-ack", // { presentationId: <string> }
+ RECONNECT: "reconnect", // { presentationId: <string> }
+ RECONNECT_ACK: "reconnect-ack", // { presentationId: <string> }
+ // session transport establishment
+ OFFER: "offer", // { offer: <json> }
+ ANSWER: "answer", // { answer: <json> }
+ ICE_CANDIDATE: "ice-candidate", // { candidate: <string> }
+});
+
+this.State = State;
+this.CommandType = CommandType;
diff --git a/dom/presentation/provider/components.conf b/dom/presentation/provider/components.conf
new file mode 100644
index 0000000000..04cb28ec75
--- /dev/null
+++ b/dom/presentation/provider/components.conf
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+categories = {}
+
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] in ('cocoa', 'android'):
+ categories["presentation-device-provider"] = "MulticastDNSDeviceProvider"
+
+Classes = [
+ {
+ 'cid': '{f4079b8b-ede5-4b90-a112-5b415a931deb}',
+ 'contract_ids': ['@mozilla.org/presentation/control-service;1'],
+ 'jsm': 'resource://gre/modules/PresentationControlService.jsm',
+ 'constructor': 'PresentationControlService',
+ },
+ {
+ 'cid': '{814f947a-52f7-41c9-94a1-3684797284ac}',
+ 'contract_ids': ['@mozilla.org/presentation-device/multicastdns-provider;1'],
+ 'type': 'mozilla::dom::presentation::MulticastDNSDeviceProvider',
+ 'headers': ['/dom/presentation/provider/MulticastDNSDeviceProvider.h'],
+ 'categories': categories,
+ },
+]
diff --git a/dom/presentation/provider/moz.build b/dom/presentation/provider/moz.build
new file mode 100644
index 0000000000..f6c4527d2c
--- /dev/null
+++ b/dom/presentation/provider/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += ["PresentationControlService.jsm"]
+
+UNIFIED_SOURCES += [
+ "DeviceProviderHelpers.cpp",
+ "MulticastDNSDeviceProvider.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_JS_MODULES.presentation += [
+ "ControllerStateMachine.jsm",
+ "ReceiverStateMachine.jsm",
+ "StateMachineHelper.jsm",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+FINAL_LIBRARY = "xul"
diff --git a/dom/presentation/provider/nsTCPDeviceInfo.h b/dom/presentation/provider/nsTCPDeviceInfo.h
new file mode 100644
index 0000000000..ef646f0bf7
--- /dev/null
+++ b/dom/presentation/provider/nsTCPDeviceInfo.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __TCPDeviceInfo_h__
+#define __TCPDeviceInfo_h__
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class TCPDeviceInfo final : public nsITCPDeviceInfo {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITCPDEVICEINFO
+
+ explicit TCPDeviceInfo(const nsACString& aId, const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint)
+ : mId(aId),
+ mAddress(aAddress),
+ mPort(aPort),
+ mCertFingerprint(aCertFingerprint) {}
+
+ private:
+ virtual ~TCPDeviceInfo() = default;
+
+ nsCString mId;
+ nsCString mAddress;
+ uint16_t mPort;
+ nsCString mCertFingerprint;
+};
+
+NS_IMPL_ISUPPORTS(TCPDeviceInfo, nsITCPDeviceInfo)
+
+// nsITCPDeviceInfo
+NS_IMETHODIMP
+TCPDeviceInfo::GetId(nsACString& aId) {
+ aId = mId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetAddress(nsACString& aAddress) {
+ aAddress = mAddress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetPort(uint16_t* aPort) {
+ *aPort = mPort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetCertFingerprint(nsACString& aCertFingerprint) {
+ aCertFingerprint = mCertFingerprint;
+ return NS_OK;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif /* !__TCPDeviceInfo_h__ */
diff --git a/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
new file mode 100644
index 0000000000..83f8965b03
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
@@ -0,0 +1,150 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* eslint-env mozilla/frame-script */
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const manager = Cc["@mozilla.org/presentation-device/manager;1"].getService(
+ Ci.nsIPresentationDeviceManager
+);
+
+var testProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDeviceProvider"]),
+ forceDiscovery() {
+ sendAsyncMessage("force-discovery");
+ },
+ listener: null,
+};
+
+var testDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ establishControlChannel() {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+ id: null,
+ name: null,
+ type: null,
+ listener: null,
+};
+
+var testDevice1 = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+var testDevice2 = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+var mockedDeviceWithoutSupportedURL = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return false;
+ },
+};
+
+var mockedDeviceSupportHttpsURL = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ if (requestedUrl.includes("https://")) {
+ return true;
+ }
+ return false;
+ },
+};
+
+addMessageListener("setup", function() {
+ manager.addDeviceProvider(testProvider);
+
+ sendAsyncMessage("setup-complete");
+});
+
+addMessageListener("trigger-device-add", function(device) {
+ testDevice.id = device.id;
+ testDevice.name = device.name;
+ testDevice.type = device.type;
+ manager.addDevice(testDevice);
+});
+
+addMessageListener("trigger-add-unsupport-url-device", function() {
+ manager.addDevice(mockedDeviceWithoutSupportedURL);
+});
+
+addMessageListener("trigger-add-multiple-devices", function() {
+ manager.addDevice(testDevice1);
+ manager.addDevice(testDevice2);
+});
+
+addMessageListener("trigger-add-https-devices", function() {
+ manager.addDevice(mockedDeviceSupportHttpsURL);
+});
+
+addMessageListener("trigger-device-update", function(device) {
+ testDevice.id = device.id;
+ testDevice.name = device.name;
+ testDevice.type = device.type;
+ manager.updateDevice(testDevice);
+});
+
+addMessageListener("trigger-device-remove", function() {
+ manager.removeDevice(testDevice);
+});
+
+addMessageListener("trigger-remove-unsupported-device", function() {
+ manager.removeDevice(mockedDeviceWithoutSupportedURL);
+});
+
+addMessageListener("trigger-remove-multiple-devices", function() {
+ manager.removeDevice(testDevice1);
+ manager.removeDevice(testDevice2);
+});
+
+addMessageListener("trigger-remove-https-devices", function() {
+ manager.removeDevice(mockedDeviceSupportHttpsURL);
+});
+
+addMessageListener("teardown", function() {
+ manager.removeDeviceProvider(testProvider);
+});
diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
new file mode 100644
index 0000000000..5e5041a870
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -0,0 +1,553 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* eslint-env mozilla/frame-script */
+
+const Cm = Components.manager;
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+function registerMockedFactory(contractId, mockedClassId, mockedFactory) {
+ var originalClassId, originalFactory;
+
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ if (!registrar.isCIDRegistered(mockedClassId)) {
+ try {
+ originalClassId = registrar.contractIDToCID(contractId);
+ originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory);
+ } catch (ex) {
+ originalClassId = "";
+ originalFactory = null;
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return {
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory,
+ };
+}
+
+function registerOriginalFactory(
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory
+) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ // Passing null for the factory remaps the original CID to the
+ // contract ID.
+ registrar.registerFactory(originalClassId, "", contractId, null);
+ }
+}
+
+var sessionId = "test-session-id-" + uuidGenerator.generateUUID().toString();
+
+const address = Cc["@mozilla.org/supports-cstring;1"].createInstance(
+ Ci.nsISupportsCString
+);
+address.data = "127.0.0.1";
+const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+addresses.appendElement(address);
+
+const mockedChannelDescription = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+ get type() {
+ if (
+ Services.prefs.getBoolPref(
+ "dom.presentation.session_transport.data_channel.enable"
+ )
+ ) {
+ return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL;
+ }
+ return Ci.nsIPresentationChannelDescription.TYPE_TCP;
+ },
+ tcpAddress: addresses,
+ tcpPort: 1234,
+};
+
+const mockedServerSocket = {
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocket", "nsIFactory"]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ get port() {
+ return this._port;
+ },
+ set listener(listener) {
+ this._listener = listener;
+ },
+ init(port, loopbackOnly, backLog) {
+ if (port != -1) {
+ this._port = port;
+ } else {
+ this._port = 5678;
+ }
+ },
+ asyncListen(listener) {
+ this._listener = listener;
+ },
+ close() {
+ this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED);
+ },
+ simulateOnSocketAccepted(serverSocket, socketTransport) {
+ this._listener.onSocketAccepted(serverSocket, socketTransport);
+ },
+};
+
+const mockedSocketTransport = Cc[
+ "@mozilla.org/presentation/mockedsockettransport;1"
+].createInstance(Ci.nsISocketTransport);
+
+const mockedControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ set listener(listener) {
+ this._listener = listener;
+ },
+ get listener() {
+ return this._listener;
+ },
+ sendOffer(offer) {
+ sendAsyncMessage("offer-sent", this._isValidSDP(offer));
+ },
+ sendAnswer(answer) {
+ sendAsyncMessage("answer-sent", this._isValidSDP(answer));
+
+ if (answer.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ }
+ },
+ _isValidSDP(aSDP) {
+ var isValid = false;
+ if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ try {
+ var sdpAddresses = aSDP.tcpAddress;
+ if (sdpAddresses.length > 0) {
+ for (var i = 0; i < sdpAddresses.length; i++) {
+ // Ensure CString addresses are used. Otherwise, an error will be thrown.
+ sdpAddresses.queryElementAt(i, Ci.nsISupportsCString);
+ }
+
+ isValid = true;
+ }
+ } catch (e) {
+ isValid = false;
+ }
+ } else if (
+ aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL
+ ) {
+ isValid = aSDP.dataChannelSDP == "test-sdp";
+ }
+ return isValid;
+ },
+ launch(presentationId, url) {
+ sessionId = presentationId;
+ },
+ terminate(presentationId) {
+ sendAsyncMessage("sender-terminate", presentationId);
+ },
+ reconnect(presentationId, url) {
+ sendAsyncMessage("start-reconnect", url);
+ },
+ notifyReconnected() {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ disconnect(reason) {
+ sendAsyncMessage("control-channel-closed", reason);
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ },
+ simulateReceiverReady() {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReceiverReady();
+ },
+ simulateOnOffer() {
+ sendAsyncMessage("offer-received");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onOffer(mockedChannelDescription);
+ },
+ simulateOnAnswer() {
+ sendAsyncMessage("answer-received");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onAnswer(mockedChannelDescription);
+ },
+ simulateNotifyConnected() {
+ sendAsyncMessage("control-channel-opened");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+};
+
+const mockedDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ sendAsyncMessage("control-channel-established");
+ return mockedControlChannel;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+const mockedDevicePrompt = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDevicePrompt",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection(request) {
+ this._request = request;
+ sendAsyncMessage("device-prompt");
+ },
+ simulateSelect() {
+ this._request.select(mockedDevice);
+ },
+ simulateCancel(result) {
+ this._request.cancel(result);
+ },
+};
+
+const mockedSessionTransport = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransport",
+ "nsIPresentationSessionTransportBuilder",
+ "nsIPresentationTCPSessionTransportBuilder",
+ "nsIPresentationDataChannelSessionTransportBuilder",
+ "nsIPresentationControlChannelListener",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set callback(callback) {
+ this._callback = callback;
+ },
+ get callback() {
+ return this._callback;
+ },
+ get selfAddress() {
+ return this._selfAddress;
+ },
+ buildTCPSenderTransport(transport, listener) {
+ this._listener = listener;
+ this._role = Ci.nsIPresentationService.ROLE_CONTROLLER;
+ this._listener.onSessionTransport(this);
+ this._listener = null;
+ sendAsyncMessage("data-transport-initialized");
+
+ setTimeout(() => {
+ this.simulateTransportReady();
+ }, 0);
+ },
+ buildTCPReceiverTransport(description, listener) {
+ this._listener = listener;
+ this._role = Ci.nsIPresentationService.ROLE_RECEIVER;
+
+ var tcpAddresses = description.QueryInterface(
+ Ci.nsIPresentationChannelDescription
+ ).tcpAddress;
+ this._selfAddress = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetAddr"]),
+ address:
+ tcpAddresses.length > 0
+ ? tcpAddresses.queryElementAt(0, Ci.nsISupportsCString).data
+ : "",
+ port: description.QueryInterface(Ci.nsIPresentationChannelDescription)
+ .tcpPort,
+ };
+
+ setTimeout(() => {
+ this._listener.onSessionTransport(this);
+ this._listener = null;
+ }, 0);
+ },
+ // in-process case
+ buildDataChannelTransport(role, window, listener) {
+ this._listener = listener;
+ this._role = role;
+
+ var hasNavigator = window ? typeof window.navigator != "undefined" : false;
+ sendAsyncMessage("check-navigator", hasNavigator);
+
+ setTimeout(() => {
+ this._listener.onSessionTransport(this);
+ this._listener = null;
+ this.simulateTransportReady();
+ }, 0);
+ },
+ enableDataNotification() {
+ sendAsyncMessage("data-transport-notification-enabled");
+ },
+ send(data) {
+ sendAsyncMessage("message-sent", data);
+ },
+ close(reason) {
+ // Don't send a message after tearDown, to avoid a leak.
+ if (this._callback) {
+ sendAsyncMessage("data-transport-closed", reason);
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportClosed(reason);
+ }
+ },
+ simulateTransportReady() {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ },
+ simulateIncomingMessage(message) {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyData(message, false);
+ },
+ onOffer(aOffer) {},
+ onAnswer(aAnswer) {},
+};
+
+const mockedNetworkInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkInfo"]),
+ getAddresses(ips, prefixLengths) {
+ ips.value = ["127.0.0.1"];
+ prefixLengths.value = [0];
+ return 1;
+ },
+};
+
+const mockedNetworkManager = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkManager", "nsIFactory"]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ get activeNetworkInfo() {
+ return mockedNetworkInfo;
+ },
+};
+
+var requestPromise = null;
+
+const mockedRequestUIGlue = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationRequestUIGlue",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest(aUrl, aSessionId) {
+ sendAsyncMessage("receiver-launching", aSessionId);
+ return requestPromise;
+ },
+};
+
+// Register mocked factories.
+const originalFactoryData = [];
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/presentation-device/prompt;1",
+ uuidGenerator.generateUUID(),
+ mockedDevicePrompt
+ )
+);
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/network/server-socket;1",
+ uuidGenerator.generateUUID(),
+ mockedServerSocket
+ )
+);
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/presentation/presentationtcpsessiontransport;1",
+ uuidGenerator.generateUUID(),
+ mockedSessionTransport
+ )
+);
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/presentation/datachanneltransportbuilder;1",
+ uuidGenerator.generateUUID(),
+ mockedSessionTransport
+ )
+);
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/network/manager;1",
+ uuidGenerator.generateUUID(),
+ mockedNetworkManager
+ )
+);
+originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/presentation/requestuiglue;1",
+ uuidGenerator.generateUUID(),
+ mockedRequestUIGlue
+ )
+);
+
+function tearDown() {
+ requestPromise = null;
+ mockedServerSocket.listener = null;
+ mockedControlChannel.listener = null;
+ mockedDevice.listener = null;
+ mockedDevicePrompt.request = null;
+ mockedSessionTransport.callback = null;
+
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .removeDevice(mockedDevice);
+
+ // Register original factories.
+ for (var data of originalFactoryData) {
+ registerOriginalFactory(
+ data.contractId,
+ data.mockedClassId,
+ data.mockedFactory,
+ data.originalClassId,
+ data.originalFactory
+ );
+ }
+
+ sendAsyncMessage("teardown-complete");
+}
+
+addMessageListener("trigger-device-add", function() {
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .addDevice(mockedDevice);
+});
+
+addMessageListener("trigger-device-prompt-select", function() {
+ mockedDevicePrompt.simulateSelect();
+});
+
+addMessageListener("trigger-device-prompt-cancel", function(result) {
+ mockedDevicePrompt.simulateCancel(result);
+});
+
+addMessageListener("trigger-incoming-session-request", function(url) {
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onSessionRequest(mockedDevice, url, sessionId, mockedControlChannel);
+});
+
+addMessageListener("trigger-incoming-terminate-request", function() {
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onTerminateRequest(mockedDevice, sessionId, mockedControlChannel, true);
+});
+
+addMessageListener("trigger-reconnected-acked", function(url) {
+ mockedControlChannel.notifyReconnected();
+});
+
+addMessageListener("trigger-incoming-offer", function() {
+ mockedControlChannel.simulateOnOffer();
+});
+
+addMessageListener("trigger-incoming-answer", function() {
+ mockedControlChannel.simulateOnAnswer();
+});
+
+addMessageListener("trigger-incoming-transport", function() {
+ mockedServerSocket.simulateOnSocketAccepted(
+ mockedServerSocket,
+ mockedSocketTransport
+ );
+});
+
+addMessageListener("trigger-control-channel-open", function(reason) {
+ mockedControlChannel.simulateNotifyConnected();
+});
+
+addMessageListener("trigger-control-channel-close", function(reason) {
+ mockedControlChannel.disconnect(reason);
+});
+
+addMessageListener("trigger-data-transport-close", function(reason) {
+ mockedSessionTransport.close(reason);
+});
+
+addMessageListener("trigger-incoming-message", function(message) {
+ mockedSessionTransport.simulateIncomingMessage(message);
+});
+
+addMessageListener("teardown", function() {
+ tearDown();
+});
+
+var controlChannelListener;
+addMessageListener("save-control-channel-listener", function() {
+ controlChannelListener = mockedControlChannel.listener;
+});
+
+addMessageListener("restore-control-channel-listener", function(message) {
+ mockedControlChannel.listener = controlChannelListener;
+ controlChannelListener = null;
+});
+
+Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+
+ requestPromise = aSubject;
+}, "setup-request-promise");
diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
new file mode 100644
index 0000000000..fe5bb6d6be
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -0,0 +1,412 @@
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint-env mozilla/frame-script */
+
+const Cm = Components.manager;
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+function debug(str) {
+ // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n');
+}
+
+const originalFactoryData = [];
+var sessionId; // Store the uuid generated by PresentationRequest.
+var triggerControlChannelError = false; // For simulating error during control channel establishment.
+
+// control channel of sender
+const mockControlChannelOfSender = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ set listener(listener) {
+ // PresentationControllingInfo::SetControlChannel
+ if (listener) {
+ debug("set listener for mockControlChannelOfSender without null");
+ } else {
+ debug("set listener for mockControlChannelOfSender with null");
+ }
+ this._listener = listener;
+ },
+ get listener() {
+ return this._listener;
+ },
+ notifyConnected() {
+ // send offer after notifyConnected immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ notifyReconnected() {
+ // send offer after notifyOpened immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ sendOffer(offer) {
+ Services.tm.dispatchToMainThread(() => {
+ mockControlChannelOfReceiver.onOffer(offer);
+ });
+ },
+ onAnswer(answer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onAnswer(answer);
+ },
+ launch(presentationId, url) {
+ sessionId = presentationId;
+ sendAsyncMessage("sender-launch", url);
+ },
+ disconnect(reason) {
+ if (!this._listener) {
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ mockControlChannelOfReceiver.disconnect();
+ },
+ terminate(presentationId) {
+ sendAsyncMessage("sender-terminate");
+ },
+ reconnect(presentationId, url) {
+ sendAsyncMessage("start-reconnect", url);
+ },
+ sendIceCandidate(candidate) {
+ mockControlChannelOfReceiver.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+// control channel of receiver
+const mockControlChannelOfReceiver = {
+ QueryInterface: ChromeUtils.generateQI(["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.notifyConnected();
+ }
+ },
+ get listener() {
+ return this._listener;
+ },
+ notifyConnected() {
+ // do nothing
+ if (!this._listener) {
+ this._pendingOpened = true;
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ onOffer(offer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onOffer(offer);
+ },
+ sendAnswer(answer) {
+ Services.tm.dispatchToMainThread(() => {
+ mockControlChannelOfSender.onAnswer(answer);
+ });
+ },
+ disconnect(reason) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ sendAsyncMessage("control-channel-receiver-closed", reason);
+ },
+ terminate(presentaionId) {},
+ sendIceCandidate(candidate) {
+ mockControlChannelOfSender.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+const mockDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ if (triggerControlChannelError) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ sendAsyncMessage("control-channel-established");
+ return mockControlChannelOfSender;
+ },
+ disconnect() {
+ sendAsyncMessage("device-disconnected");
+ },
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+const mockDevicePrompt = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDevicePrompt",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection(request) {
+ this._request = request;
+ sendAsyncMessage("device-prompt");
+ },
+ simulateSelect() {
+ this._request.select(mockDevice);
+ },
+ simulateCancel() {
+ this._request.cancel();
+ },
+};
+
+const mockRequestUIGlue = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationRequestUIGlue",
+ "nsIFactory",
+ ]),
+ set promise(aPromise) {
+ this._promise = aPromise;
+ },
+ get promise() {
+ return this._promise;
+ },
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest(aUrl, aSessionId) {
+ return this.promise;
+ },
+};
+
+function initMockAndListener() {
+ function registerMockFactory(contractId, mockClassId, mockFactory) {
+ var originalClassId, originalFactory;
+
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ if (!registrar.isCIDRegistered(mockClassId)) {
+ try {
+ originalClassId = registrar.contractIDToCID(contractId);
+ originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory);
+ } catch (ex) {
+ originalClassId = "";
+ originalFactory = null;
+ }
+ if (originalFactory) {
+ registrar.unregisterFactory(originalClassId, originalFactory);
+ }
+ registrar.registerFactory(mockClassId, "", contractId, mockFactory);
+ }
+
+ return {
+ contractId,
+ mockClassId,
+ mockFactory,
+ originalClassId,
+ originalFactory,
+ };
+ }
+ // Register mock factories.
+ originalFactoryData.push(
+ registerMockFactory(
+ "@mozilla.org/presentation-device/prompt;1",
+ uuidGenerator.generateUUID(),
+ mockDevicePrompt
+ )
+ );
+ originalFactoryData.push(
+ registerMockFactory(
+ "@mozilla.org/presentation/requestuiglue;1",
+ uuidGenerator.generateUUID(),
+ mockRequestUIGlue
+ )
+ );
+
+ addMessageListener("trigger-device-add", function() {
+ debug("Got message: trigger-device-add");
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .addDevice(mockDevice);
+ });
+
+ addMessageListener("trigger-device-prompt-select", function() {
+ debug("Got message: trigger-device-prompt-select");
+ mockDevicePrompt.simulateSelect();
+ });
+
+ addMessageListener("trigger-on-session-request", function(url) {
+ debug("Got message: trigger-on-session-request");
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onSessionRequest(
+ mockDevice,
+ url,
+ sessionId,
+ mockControlChannelOfReceiver
+ );
+ });
+
+ addMessageListener("trigger-on-terminate-request", function() {
+ debug("Got message: trigger-on-terminate-request");
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onTerminateRequest(
+ mockDevice,
+ sessionId,
+ mockControlChannelOfReceiver,
+ false
+ );
+ });
+
+ addMessageListener("trigger-control-channel-open", function(reason) {
+ debug("Got message: trigger-control-channel-open");
+ mockControlChannelOfSender.notifyConnected();
+ mockControlChannelOfReceiver.notifyConnected();
+ });
+
+ addMessageListener("trigger-control-channel-error", function(reason) {
+ debug("Got message: trigger-control-channel-open");
+ triggerControlChannelError = true;
+ });
+
+ addMessageListener("trigger-reconnected-acked", function(url) {
+ debug("Got message: trigger-reconnected-acked");
+ mockControlChannelOfSender.notifyReconnected();
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onReconnectRequest(
+ mockDevice,
+ url,
+ sessionId,
+ mockControlChannelOfReceiver
+ );
+ });
+
+ // Used to call sendAsyncMessage in chrome script from receiver.
+ addMessageListener("forward-command", function(command_data) {
+ let command = JSON.parse(command_data);
+ sendAsyncMessage(command.name, command.data);
+ });
+
+ addMessageListener("teardown", teardown);
+
+ Services.obs.addObserver(function setupRequestPromiseHandler(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ debug("Got observer: setup-request-promise");
+ Services.obs.removeObserver(setupRequestPromiseHandler, aTopic);
+ mockRequestUIGlue.promise = aSubject;
+ sendAsyncMessage("promise-setup-ready");
+ },
+ "setup-request-promise");
+}
+
+function teardown() {
+ function registerOriginalFactory(
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory
+ ) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ registrar.registerFactory(
+ originalClassId,
+ "",
+ contractId,
+ originalFactory
+ );
+ }
+ }
+
+ mockRequestUIGlue.promise = null;
+ mockControlChannelOfSender.listener = null;
+ mockControlChannelOfReceiver.listener = null;
+ mockDevicePrompt.request = null;
+
+ var deviceManager = Cc[
+ "@mozilla.org/presentation-device/manager;1"
+ ].getService(Ci.nsIPresentationDeviceManager);
+ deviceManager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .removeDevice(mockDevice);
+ // Register original factories.
+ for (var data of originalFactoryData) {
+ registerOriginalFactory(
+ data.contractId,
+ data.mockClassId,
+ data.mockFactory,
+ data.originalClassId,
+ data.originalFactory
+ );
+ }
+ sendAsyncMessage("teardown-complete");
+}
+
+initMockAndListener();
diff --git a/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js
new file mode 100644
index 0000000000..5385e5d9b9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/frame-script */
+
+function loadPrivilegedScriptTest() {
+ /**
+ * The script is loaded as
+ * (a) a privileged script in content process for dc_sender.html
+ * (b) a frame script in the remote iframe process for dc_receiver_oop.html
+ * |type port == "undefined"| indicates the script is load by
+ * |loadPrivilegedScript| which is the first case.
+ */
+ function sendMessage(type, data) {
+ if (typeof port == "undefined") {
+ sendAsyncMessage(type, { data });
+ } else {
+ port.postMessage({ type, data });
+ }
+ }
+
+ if (typeof port != "undefined") {
+ /**
+ * When the script is loaded by |loadPrivilegedScript|, these APIs
+ * are exposed to this script.
+ */
+ port.onmessage = e => {
+ var type = e.data.type;
+ if (!handlers.hasOwnProperty(type)) {
+ return;
+ }
+ var args = [e];
+ handlers[type].forEach(handler => handler.apply(null, args));
+ };
+ var handlers = {};
+ /* eslint-disable-next-line no-global-assign */
+ addMessageListener = function(message, handler) {
+ if (handlers.hasOwnProperty(message)) {
+ handlers[message].push(handler);
+ } else {
+ handlers[message] = [handler];
+ }
+ };
+ /* eslint-disable-next-line no-global-assign */
+ removeMessageListener = function(message, handler) {
+ if (!handler || !handlers.hasOwnProperty(message)) {
+ return;
+ }
+ var index = handlers[message].indexOf(handler);
+ if (index != -1) {
+ handlers[message].splice(index, 1);
+ }
+ };
+ }
+
+ const Cm = Components.manager;
+
+ const mockedChannelDescription = {
+ /* eslint-disable-next-line mozilla/use-chromeutils-generateqi */
+ QueryInterface(iid) {
+ const interfaces = [Ci.nsIPresentationChannelDescription];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ return this;
+ },
+ get type() {
+ /* global Services */
+ if (
+ Services.prefs.getBoolPref(
+ "dom.presentation.session_transport.data_channel.enable"
+ )
+ ) {
+ return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL;
+ }
+ return Ci.nsIPresentationChannelDescription.TYPE_TCP;
+ },
+ get dataChannelSDP() {
+ return "test-sdp";
+ },
+ };
+
+ function setTimeout(callback, delay) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ { notify: callback },
+ delay,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return timer;
+ }
+
+ const mockedSessionTransport = {
+ /* eslint-disable-next-line mozilla/use-chromeutils-generateqi */
+ QueryInterface(iid) {
+ const interfaces = [
+ Ci.nsIPresentationSessionTransport,
+ Ci.nsIPresentationDataChannelSessionTransportBuilder,
+ Ci.nsIFactory,
+ ];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ return this;
+ },
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set callback(callback) {
+ this._callback = callback;
+ },
+ get callback() {
+ return this._callback;
+ },
+ /* OOP case */
+ buildDataChannelTransport(role, window, listener) {
+ dump("PresentationSessionFrameScript: build data channel transport\n");
+ this._listener = listener;
+ this._role = role;
+
+ var hasNavigator = window
+ ? typeof window.navigator != "undefined"
+ : false;
+ sendMessage("check-navigator", hasNavigator);
+
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ this._listener.sendOffer(mockedChannelDescription);
+ }
+ },
+
+ enableDataNotification() {
+ sendMessage("data-transport-notification-enabled");
+ },
+ send(data) {
+ sendMessage("message-sent", data);
+ },
+ close(reason) {
+ sendMessage("data-transport-closed", reason);
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportClosed(reason);
+ this._callback = null;
+ },
+ simulateTransportReady() {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ },
+ simulateIncomingMessage(message) {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyData(message, false);
+ },
+ onOffer(aOffer) {
+ this._listener.sendAnswer(mockedChannelDescription);
+ this._onSessionTransport();
+ },
+ onAnswer(aAnswer) {
+ this._onSessionTransport();
+ },
+ _onSessionTransport() {
+ setTimeout(() => {
+ this._listener.onSessionTransport(this);
+ this.simulateTransportReady();
+ this._listener = null;
+ }, 0);
+ },
+ };
+
+ function tearDown() {
+ mockedSessionTransport.callback = null;
+
+ /* Register original factories. */
+ for (var data of originalFactoryData) {
+ registerOriginalFactory(
+ data.contractId,
+ data.mockedClassId,
+ data.mockedFactory,
+ data.originalClassId,
+ data.originalFactory
+ );
+ }
+ sendMessage("teardown-complete");
+ }
+
+ function registerMockedFactory(contractId, mockedClassId, mockedFactory) {
+ var originalClassId, originalFactory;
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+ if (!registrar.isCIDRegistered(mockedClassId)) {
+ try {
+ originalClassId = registrar.contractIDToCID(contractId);
+ originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory);
+ } catch (ex) {
+ originalClassId = "";
+ originalFactory = null;
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return {
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory,
+ };
+ }
+
+ function registerOriginalFactory(
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory
+ ) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ registrar.registerFactory(originalClassId, "", contractId, null);
+ }
+ }
+
+ /* Register mocked factories. */
+ const originalFactoryData = [];
+ const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+ );
+ originalFactoryData.push(
+ registerMockedFactory(
+ "@mozilla.org/presentation/datachanneltransportbuilder;1",
+ uuidGenerator.generateUUID(),
+ mockedSessionTransport
+ )
+ );
+
+ addMessageListener("trigger-incoming-message", function(event) {
+ mockedSessionTransport.simulateIncomingMessage(event.data.data);
+ });
+ addMessageListener("teardown", () => tearDown());
+}
+
+// Exposed to the caller of |loadPrivilegedScript|
+var contentScript = {
+ handlers: {},
+ addMessageListener(message, handler) {
+ if (this.handlers.hasOwnProperty(message)) {
+ this.handlers[message].push(handler);
+ } else {
+ this.handlers[message] = [handler];
+ }
+ },
+ removeMessageListener(message, handler) {
+ if (!handler || !this.handlers.hasOwnProperty(message)) {
+ return;
+ }
+ var index = this.handlers[message].indexOf(handler);
+ if (index != -1) {
+ this.handlers[message].splice(index, 1);
+ }
+ },
+ sendAsyncMessage(message, data) {
+ port.postMessage({ type: message, data });
+ },
+};
+
+if (!SpecialPowers.isMainProcess()) {
+ var port;
+ try {
+ port = SpecialPowers.loadPrivilegedScript(
+ loadPrivilegedScriptTest.toString()
+ );
+ } catch (e) {
+ ok(false, "loadPrivilegedScript should not throw" + e);
+ }
+
+ port.onmessage = e => {
+ var type = e.data.type;
+ if (!contentScript.handlers.hasOwnProperty(type)) {
+ return;
+ }
+ var args = [e.data.data];
+ contentScript.handlers[type].forEach(handler => handler.apply(null, args));
+ };
+}
diff --git a/dom/presentation/tests/mochitest/chrome.ini b/dom/presentation/tests/mochitest/chrome.ini
new file mode 100644
index 0000000000..22ecc244ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/chrome.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ PresentationDeviceInfoChromeScript.js
+ PresentationSessionChromeScript.js
+
+[test_presentation_datachannel_sessiontransport.html]
+skip-if = os == 'android'
+[test_presentation_sender_startWithDevice.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_tcp_sender.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_tcp_sender_default_request.html]
+skip-if = toolkit == 'android' # Bug 1129785
diff --git a/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
new file mode 100644
index 0000000000..3d98d12a2b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -0,0 +1,216 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0];
+const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length);
+const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER);
+TYPED_DATA_ARRAY.set(DATA_ARRAY);
+
+function is_same_buffer(recv_data, expect_data) {
+ let recv_dataview = new Uint8Array(recv_data);
+ let expected_dataview = new Uint8Array(expect_data);
+
+ if (recv_dataview.length !== expected_dataview.length) {
+ return false;
+ }
+
+ for (let i = 0; i < recv_dataview.length; i++) {
+ if (recv_dataview[i] != expected_dataview[i]) {
+ info("discover byte differenct at " + i);
+ return false;
+ }
+ }
+ return true;
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+ is(navigator.presentation.defaultRequest, null, "Receiver: navigator.presentation.defaultRequest should be null.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingMessage ---");
+ connection.addEventListener("message", function(evt) {
+ let msg = evt.data;
+ is(msg, "msg-sender-to-receiver", "Receiver: Receiver should receive message from sender.");
+ command("forward-command", JSON.stringify({ name: "message-from-sender-received" }));
+ aResolve();
+ }, {once: true});
+ command("forward-command", JSON.stringify({ name: "trigger-message-from-sender" }));
+ });
+}
+
+function testSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "trigger-message-from-receiver") {
+ info("Receiver: --- testSendMessage ---");
+ connection.send("msg-receiver-to-sender");
+ }
+ if (message.type === "message-from-receiver-received") {
+ window.removeEventListener("hashchange", hashchangeHandler);
+ aResolve();
+ }
+ });
+ });
+}
+
+function testIncomingBlobMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingBlobMessage ---");
+ connection.send("testIncomingBlobMessage");
+ connection.addEventListener("message", function(evt) {
+ let recvData = String.fromCharCode.apply(null, new Uint8Array(evt.data));
+ is(recvData, "Hello World", "expected same string data");
+ aResolve();
+ }, {once: true});
+ });
+}
+
+function testConnectionClosed() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionClosed ---");
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Receiver: Connection should be closed.");
+ command("forward-command", JSON.stringify({ name: "receiver-closed" }));
+ aResolve();
+ };
+ command("forward-command", JSON.stringify({ name: "ready-to-close" }));
+ });
+}
+
+function testReconnectConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testReconnectConnection ---");
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "prepare-for-reconnect") {
+ command("forward-command", JSON.stringify({ name: "ready-to-reconnect" }));
+ }
+ });
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(true, "The connection is reconnected.");
+ aResolve();
+ };
+ });
+}
+
+function testIncomingArrayBuffer() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingArrayBuffer ---");
+ connection.binaryType = "blob";
+ connection.send("testIncomingArrayBuffer");
+ connection.addEventListener("message", function(evt) {
+ var fileReader = new FileReader();
+ fileReader.onload = function() {
+ ok(is_same_buffer(DATA_ARRAY_BUFFER, this.result), "expected same buffer data");
+ aResolve();
+ };
+ fileReader.readAsArrayBuffer(evt.data);
+ }, {once: true});
+ });
+}
+
+function testIncomingArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingArrayBufferView ---");
+ connection.binaryType = "arraybuffer";
+ connection.send("testIncomingArrayBufferView");
+ connection.addEventListener("message", function(evt) {
+ ok(is_same_buffer(evt.data, TYPED_DATA_ARRAY), "expected same buffer data");
+ aResolve();
+ }, {once: true});
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testIncomingMessage)
+ .then(testSendMessage)
+ .then(testIncomingBlobMessage)
+ .then(testConnectionClosed)
+ .then(testReconnectConnection)
+ .then(testIncomingArrayBuffer)
+ .then(testIncomingArrayBufferView)
+ .then(testConnectionClosed);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html b/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html
new file mode 100644
index 0000000000..5198e76be8
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionWentaway() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionWentaway ---\n");
+ command("forward-command", JSON.stringify({ name: "ready-to-remove-receiverFrame" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionWentaway);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html b/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html
new file mode 100644
index 0000000000..5fd1967331
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+function testReceiver() {
+ alert(!!navigator.presentation.receiver);
+}
+
+testReceiver();
+window.addEventListener("hashchange", testReceiver);
+</script>
diff --git a/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html
new file mode 100644
index 0000000000..d31b3578b2
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html
@@ -0,0 +1,158 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+function testGetAvailability() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ var request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.start().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.reconnect("dummyId").then(
+ function(aConnection) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForAboutBlank() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("about:blank");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForAboutSrcdoc() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("about:srcdoc");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForDataURL() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("data:text/html,1");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function runTest() {
+ testGetAvailability()
+ .then(testStartRequest)
+ .then(testReconnectRequest)
+ .then(testGetAvailabilityForAboutBlank)
+ .then(testGetAvailabilityForAboutSrcdoc)
+ .then(testGetAvailabilityForDataURL)
+ .then(finish);
+}
+
+window.addEventListener("message", function(evt) {
+ if (evt.data === "start") {
+ runTest();
+ }
+}, {once: true});
+
+window.setTimeout(function() {
+ command("ready-to-start");
+}, 3000);
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_non_receiver.html b/dom/presentation/tests/mochitest/file_presentation_non_receiver.html
new file mode 100644
index 0000000000..75ffe6b4ad
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver on a non-receiver page at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver, null, "navigator.presentation.receiver shouldn't be available in non-receiving pages.");
+ aResolve();
+ });
+}
+
+testConnectionAvailable().
+then(finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html b/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html
new file mode 100644
index 0000000000..c1b5c10610
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver on a non-receiver inner iframe of the receiver page at receiver side</title>
+</head>
+<body onload="testConnectionAvailable()">
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver, null, "navigator.presentation.receiver shouldn't be available in inner iframes with different origins from receiving pages.");
+ aResolve();
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver.html b/dom/presentation/tests/mochitest/file_presentation_receiver.html
new file mode 100644
index 0000000000..b864fb7c56
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(msg) {
+ alert("COMMAND " + JSON.stringify(msg));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available in receiving pages.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in receiving pages.");
+
+ navigator.presentation.receiver.connectionList.then(
+ function(aList) {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ finish();
+ aReject();
+ }
+ );
+ command({ name: "trigger-incoming-offer" });
+ });
+}
+
+function testDefaultRequestIsUndefined() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.defaultRequest, undefined, "navigator.presentation.defaultRequest should not be available in receiving UA");
+ aResolve();
+ });
+}
+
+function testConnectionAvailableSameOriginInnerIframe() {
+ return new Promise(function(aResolve, aReject) {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "./file_presentation_receiver_inner_iframe.html");
+ document.body.appendChild(iframe);
+
+ aResolve();
+ });
+}
+
+function testConnectionUnavailableDiffOriginInnerIframe() {
+ return new Promise(function(aResolve, aReject) {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "http://example.com/tests/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html");
+ document.body.appendChild(iframe);
+
+ aResolve();
+ });
+}
+
+function testConnectionListSameObject() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver.connectionList, navigator.presentation.receiver.connectionList, "The promise should be the same object.");
+ navigator.presentation.receiver.connectionList.then(
+ function(aList) {
+ is(connection, aList.connections[0], "The connection from list and the one from |connectionavailable| event should be the same.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ finish();
+ aReject();
+ }
+ );
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ command({ name: "trigger-incoming-message",
+ data: incomingMessage });
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+testConnectionAvailable().
+then(testDefaultRequestIsUndefined).
+then(testConnectionAvailableSameOriginInnerIframe).
+then(testConnectionUnavailableDiffOriginInnerIframe).
+then(testConnectionListSameObject).
+then(testIncomingMessage).
+then(testCloseConnection).
+then(finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html b/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html
new file mode 100644
index 0000000000..6bd3da1f47
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for sandboxed auxiliary navigation flag in receiver page</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(msg) {
+ alert("COMMAND " + JSON.stringify(msg));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available in OOP receiving pages.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in receiving pages.");
+
+ aResolve();
+ });
+}
+
+function testOpenWindow() {
+ return new Promise(function(aResolve, aReject) {
+ try {
+ window.open("http://example.com");
+ ok(false, "receiver page should not be able to open a new window.");
+ } catch (e) {
+ ok(true, "receiver page should not be able to open a new window.");
+ aResolve();
+ }
+ });
+}
+
+testConnectionAvailable().
+then(testOpenWindow).
+then(finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html b/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html
new file mode 100644
index 0000000000..f6dd10797e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available.");
+ aResolve();
+ });
+}
+
+function testUnexpectedControlChannelClose() {
+ // Trigger the control channel to be closed with error code.
+ command({ name: "trigger-control-channel-close", data: 0x80004004 /* NS_ERROR_ABORT */ });
+
+ return new Promise(function(aResolve, aReject) {
+ return Promise.race([
+ navigator.presentation.receiver.connectionList.then(
+ (aList) => {
+ ok(false, "Should not get a connection list.");
+ aReject();
+ },
+ (aError) => {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ aReject();
+ }
+ ),
+ new Promise(
+ () => {
+ setTimeout(() => {
+ ok(true, "Not getting a conenction list.");
+ aResolve();
+ }, 3000);
+ }
+ ),
+ ]);
+ });
+}
+
+testConnectionAvailable().
+then(testUnexpectedControlChannelClose).
+then(finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html b/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html
new file mode 100644
index 0000000000..dac1abac14
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver in an inner iframe of the receiver page at receiver side</title>
+</head>
+<body onload="testConnectionAvailable()">
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in same-origin inner iframes of receiving pages.");
+ aResolve();
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_reconnect.html b/dom/presentation/tests/mochitest/file_presentation_reconnect.html
new file mode 100644
index 0000000000..31d6e0aa1c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_reconnect.html
@@ -0,0 +1,100 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+var request;
+var connection;
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ request = new PresentationRequest("http://example1.com");
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ command({ name: "notify-connection-closed", id: connection.id });
+ };
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ if (connection.state === "closed") {
+ aResolve();
+ return;
+ }
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "The connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+window.addEventListener("message", function onMessage(evt) {
+ if (evt.data === "startConnection") {
+ testStartRequest().then(
+ function() {
+ command({ name: "connection-connected", id: connection.id });
+ }
+ );
+ } else if (evt.data === "closeConnection") {
+ testCloseConnection().then(
+ function() {
+ command({ name: "connection-closed", id: connection.id });
+ }
+ );
+ }
+});
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html
new file mode 100644
index 0000000000..162e4209e7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html
@@ -0,0 +1,113 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+function testGetAvailability() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ var request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.start().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testDefaultRequest() {
+ return new Promise(function(aResolve, aReject) {
+ navigator.presentation.defaultRequest = new PresentationRequest("http://example.com");
+ is(navigator.presentation.defaultRequest, null, "DefaultRequest shoud be null.");
+ aResolve();
+ });
+}
+
+function testReconnectRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.reconnect("dummyId").then(
+ function(aConnection) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function runTest() {
+ testGetAvailability()
+ .then(testStartRequest)
+ .then(testDefaultRequest)
+ .then(testReconnectRequest)
+ .then(finish);
+}
+
+window.addEventListener("message", function(evt) {
+ if (evt.data === "start") {
+ runTest();
+ }
+}, {once: true});
+
+window.setTimeout(function() {
+ command("ready-to-start");
+}, 3000);
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_terminate.html b/dom/presentation/tests/mochitest/file_presentation_terminate.html
new file mode 100644
index 0000000000..db5972f8d1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id='content'></div>
+<script type='application/javascript'>
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionTerminate ---");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ // Using window.alert at this stage will cause window.close() fail.
+ // Only trigger it if verdict fail.
+ if (connection.state !== "terminated") {
+ is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+ }
+ aResolve();
+ };
+ command("forward-command", JSON.stringify({ name: "ready-to-terminate" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionTerminate);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html
new file mode 100644
index 0000000000..3dfb692e88
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id='content'></div>
+<script type='application/javascript'>
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one connection.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionTerminate ---");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ // Using window.alert at this stage will cause window.close() fail.
+ // Only trigger it if verdict fail.
+ if (connection.state !== "terminated") {
+ is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+ }
+ aResolve();
+ };
+
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "ready-to-terminate") {
+ info("Receiver: --- ready-to-terminate ---");
+ connection.terminate();
+ }
+ });
+
+
+ command("forward-command", JSON.stringify({ name: "prepare-for-terminate" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionTerminate);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test
@@ -0,0 +1 @@
+
diff --git a/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^
new file mode 100644
index 0000000000..fc044e3c49
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^
@@ -0,0 +1 @@
+Content-Type: application/unknown
diff --git a/dom/presentation/tests/mochitest/mochitest.ini b/dom/presentation/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..3ff134cc4c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -0,0 +1,81 @@
+[DEFAULT]
+support-files =
+ PresentationDeviceInfoChromeScript.js
+ PresentationSessionChromeScript.js
+ PresentationSessionFrameScript.js
+ PresentationSessionChromeScript1UA.js
+ file_presentation_1ua_receiver.html
+ test_presentation_1ua_sender_and_receiver.js
+ file_presentation_non_receiver_inner_iframe.html
+ file_presentation_non_receiver.html
+ file_presentation_receiver.html
+ file_presentation_receiver_establish_connection_error.html
+ file_presentation_receiver_inner_iframe.html
+ file_presentation_1ua_wentaway.html
+ test_presentation_1ua_connection_wentaway.js
+ file_presentation_receiver_auxiliary_navigation.html
+ test_presentation_receiver_auxiliary_navigation.js
+ file_presentation_sandboxed_presentation.html
+ file_presentation_terminate.html
+ test_presentation_terminate.js
+ file_presentation_terminate_establish_connection_error.html
+ test_presentation_terminate_establish_connection_error.js
+ file_presentation_reconnect.html
+ file_presentation_unknown_content_type.test
+ file_presentation_unknown_content_type.test^headers^
+ test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
+ file_presentation_mixed_security_contexts.html
+
+[test_presentation_dc_sender.html]
+skip-if = e10s # Bug 1656033
+[test_presentation_dc_receiver.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_dc_receiver_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_inproc.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_connection_wentaway_inproc.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_connection_wentaway_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_sender_disconnect.html]
+skip-if = os == 'android'
+[test_presentation_tcp_sender_establish_connection_error.html]
+skip-if = os == 'android'
+[test_presentation_tcp_receiver_establish_connection_error.html]
+skip-if = (e10s || os == 'mac' || os == 'win') # Bug 1129785, Bug 1204709
+[test_presentation_tcp_receiver_establish_connection_timeout.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html]
+skip-if = e10s
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html]
+skip-if = e10s
+[test_presentation_tcp_receiver.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_receiver_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_receiver_auxiliary_navigation_inproc.html]
+skip-if = e10s
+[test_presentation_receiver_auxiliary_navigation_oop.html]
+skip-if = e10s
+[test_presentation_terminate_inproc.html]
+skip-if = e10s
+[test_presentation_terminate_oop.html]
+skip-if = e10s
+[test_presentation_terminate_establish_connection_error_inproc.html]
+skip-if = e10s
+[test_presentation_terminate_establish_connection_error_oop.html]
+skip-if = e10s
+[test_presentation_sender_on_terminate_request.html]
+skip-if = os == 'android'
+[test_presentation_sandboxed_presentation.html]
+skip-if = true # bug 1315867
+[test_presentation_reconnect.html]
+[test_presentation_mixed_security_contexts.html]
+[test_presentation_availability.html]
+support-files = test_presentation_availability_iframe.html
+[test_presentation_fingerprinting_resistance.html]
+skip-if = e10s
+support-files = file_presentation_fingerprinting_resistance_receiver.html
diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
new file mode 100644
index 0000000000..cff2280b72
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -0,0 +1,241 @@
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+
+function debug(str) {
+ // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_1ua_wentaway.html"
+);
+var request;
+var connection;
+var receiverIframe;
+
+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() {
+ 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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ receiverIframe.removeEventListener(
+ "mozbrowsershowmodalprompt",
+ receiverListener
+ );
+ teardown();
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ gScript.addMessageListener(
+ "promise-setup-ready",
+ function promiseSetupReadyHandler() {
+ debug("Got message: promise-setup-ready");
+ gScript.removeMessageListener(
+ "promise-setup-ready",
+ promiseSetupReadyHandler
+ );
+ gScript.sendAsyncMessage("trigger-on-session-request", receiverUrl);
+ }
+ );
+
+ return Promise.resolve();
+}
+
+function testCreateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testCreateRequest ---");
+ request = new PresentationRequest(receiverUrl);
+ request
+ .getAvailability()
+ .then(aAvailability => {
+ is(
+ aAvailability.value,
+ false,
+ "Sender: should have no available device after setup"
+ );
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Sender: Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when getting availability: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ request
+ .start()
+ .then(aConnection => {
+ connection = aConnection;
+ ok(connection, "Sender: Connection should be available.");
+ ok(connection.id, "Sender: Connection ID should be set.");
+ is(
+ connection.state,
+ "connecting",
+ "Sender: The initial state should be connecting."
+ );
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when establishing a connection: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testConnectionWentaway() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testConnectionWentaway ---");
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Sender: Connection should be closed.");
+ receiverIframe.addEventListener(
+ "mozbrowserclose",
+ function closeHandler() {
+ ok(false, "wentaway should not trigger receiver close");
+ aResolve();
+ }
+ );
+ setTimeout(aResolve, 3000);
+ };
+ gScript.addMessageListener(
+ "ready-to-remove-receiverFrame",
+ function onReadyToRemove() {
+ gScript.removeMessageListener(
+ "ready-to-remove-receiverFrame",
+ onReadyToRemove
+ );
+ receiverIframe.src = "http://example.com";
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ debug("Got message: teardown-complete");
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup()
+ .then(testCreateRequest)
+ .then(testStartConnection)
+ .then(testConnectionWentaway)
+ .then(teardown);
+}
+
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ () => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.ipc.tabs.disabled", false],
+ ["dom.presentation.test.stage", 0],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html
new file mode 100644
index 0000000000..a75507e6d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1258600">
+ Test for PresentationConnectionCloseEvent with wentaway reason</a>
+ <script type="application/javascript" src="test_presentation_1ua_connection_wentaway.js">
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html
new file mode 100644
index 0000000000..a75507e6d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1258600">
+ Test for PresentationConnectionCloseEvent with wentaway reason</a>
+ <script type="application/javascript" src="test_presentation_1ua_connection_wentaway.js">
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js
new file mode 100644
index 0000000000..73764e597b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js
@@ -0,0 +1,522 @@
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function debug(str) {
+ // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_1ua_receiver.html"
+);
+var request;
+var connection;
+var receiverIframe;
+var presentationId;
+const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0];
+const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length);
+const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER);
+TYPED_DATA_ARRAY.set(DATA_ARRAY);
+
+function postMessageToIframe(aType) {
+ receiverIframe.src =
+ receiverUrl + "#" + encodeURIComponent(JSON.stringify({ type: aType }));
+}
+
+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() {
+ 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);
+ var oop = !location.pathname.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ // 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)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ receiverIframe.removeEventListener(
+ "mozbrowsershowmodalprompt",
+ receiverListener
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ gScript.addMessageListener(
+ "promise-setup-ready",
+ function promiseSetupReadyHandler() {
+ debug("Got message: promise-setup-ready");
+ gScript.removeMessageListener(
+ "promise-setup-ready",
+ promiseSetupReadyHandler
+ );
+ gScript.sendAsyncMessage("trigger-on-session-request", receiverUrl);
+ }
+ );
+
+ return Promise.resolve();
+}
+
+function testCreateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testCreateRequest ---");
+ request = new PresentationRequest("file_presentation_1ua_receiver.html");
+ request
+ .getAvailability()
+ .then(aAvailability => {
+ is(
+ aAvailability.value,
+ false,
+ "Sender: should have no available device after setup"
+ );
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Sender: Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when getting availability: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ request
+ .start()
+ .then(aConnection => {
+ connection = aConnection;
+ ok(connection, "Sender: Connection should be available.");
+ ok(connection.id, "Sender: Connection ID should be set.");
+ is(
+ connection.state,
+ "connecting",
+ "The initial state should be connecting."
+ );
+ is(
+ connection.url,
+ receiverUrl,
+ "request URL should be expanded to absolute URL"
+ );
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ presentationId = connection.id;
+ aResolve();
+ };
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when establishing a connection: " + aError
+ );
+ teardown();
+ aReject();
+ });
+
+ let request2 = new PresentationRequest("/");
+ request2
+ .start()
+ .then(() => {
+ ok(
+ false,
+ "Sender: session start should fail while there is an unsettled promise."
+ );
+ })
+ .catch(aError => {
+ is(aError.name, "OperationError", "Expect to get OperationError.");
+ });
+ });
+}
+
+function testSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendMessage ---");
+ gScript.addMessageListener(
+ "trigger-message-from-sender",
+ function triggerMessageFromSenderHandler() {
+ debug("Got message: trigger-message-from-sender");
+ gScript.removeMessageListener(
+ "trigger-message-from-sender",
+ triggerMessageFromSenderHandler
+ );
+ info("Send message to receiver");
+ connection.send("msg-sender-to-receiver");
+ }
+ );
+
+ gScript.addMessageListener(
+ "message-from-sender-received",
+ function messageFromSenderReceivedHandler() {
+ debug("Got message: message-from-sender-received");
+ gScript.removeMessageListener(
+ "message-from-sender-received",
+ messageFromSenderReceivedHandler
+ );
+ aResolve();
+ }
+ );
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testIncomingMessage ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "msg-receiver-to-sender",
+ "Sender: Sender should receive message from Receiver"
+ );
+ postMessageToIframe("message-from-receiver-received");
+ aResolve();
+ },
+ { once: true }
+ );
+ postMessageToIframe("trigger-message-from-receiver");
+ });
+}
+
+function testSendBlobMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendBlobMessage ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "testIncomingBlobMessage",
+ "Sender: Sender should receive message from Receiver"
+ );
+ let blob = new Blob(["Hello World"], { type: "text/plain" });
+ connection.send(blob);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function testSendArrayBuffer() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendArrayBuffer ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "testIncomingArrayBuffer",
+ "Sender: Sender should receive message from Receiver"
+ );
+ connection.send(DATA_ARRAY_BUFFER);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function testSendArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendArrayBufferView ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "testIncomingArrayBufferView",
+ "Sender: Sender should receive message from Receiver"
+ );
+ connection.send(TYPED_DATA_ARRAY);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function testCloseConnection() {
+ info("Sender: --- testCloseConnection ---");
+ // Test terminate immediate after close.
+ function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener(
+ "control-channel-established",
+ controlChannelEstablishedHandler
+ );
+ ok(false, "terminate after close should do nothing");
+ }
+ gScript.addMessageListener("ready-to-close", function onReadyToClose() {
+ gScript.removeMessageListener("ready-to-close", onReadyToClose);
+ connection.close();
+
+ gScript.addMessageListener(
+ "control-channel-established",
+ controlChannelEstablishedHandler
+ );
+ connection.terminate();
+ });
+
+ return Promise.all([
+ new Promise(function(aResolve, aReject) {
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Sender: Connection should be closed.");
+ gScript.removeMessageListener(
+ "control-channel-established",
+ controlChannelEstablishedHandler
+ );
+ aResolve();
+ };
+ }),
+ new Promise(function(aResolve, aReject) {
+ let timeout = setTimeout(function() {
+ gScript.removeMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ ok(true, "terminate after close should not trigger device.disconnect");
+ aResolve();
+ }, 3000);
+
+ function deviceDisconnectedHandler() {
+ gScript.removeMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ ok(false, "terminate after close should not trigger device.disconnect");
+ clearTimeout(timeout);
+ aResolve();
+ }
+
+ gScript.addMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ }),
+ new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener(
+ "receiver-closed",
+ function onReceiverClosed() {
+ gScript.removeMessageListener("receiver-closed", onReceiverClosed);
+ gScript.removeMessageListener(
+ "control-channel-established",
+ controlChannelEstablishedHandler
+ );
+ aResolve();
+ }
+ );
+ }),
+ ]);
+}
+
+function testTerminateAfterClose() {
+ info("Sender: --- testTerminateAfterClose ---");
+ return Promise.race([
+ new Promise(function(aResolve, aReject) {
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ ok(false, "terminate after close should do nothing");
+ aResolve();
+ };
+ connection.terminate();
+ }),
+ new Promise(function(aResolve, aReject) {
+ setTimeout(function() {
+ is(connection.state, "closed", "Sender: Connection should be closed.");
+ aResolve();
+ }, 3000);
+ }),
+ ]);
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testReconnect ---");
+ gScript.addMessageListener(
+ "control-channel-established",
+ function controlChannelEstablished() {
+ gScript.removeMessageListener(
+ "control-channel-established",
+ controlChannelEstablished
+ );
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ }
+ );
+
+ gScript.addMessageListener(
+ "start-reconnect",
+ function startReconnectHandler(url) {
+ debug("Got message: start-reconnect");
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, receiverUrl, "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ }
+ );
+
+ gScript.addMessageListener(
+ "ready-to-reconnect",
+ function onReadyToReconnect() {
+ gScript.removeMessageListener("ready-to-reconnect", onReadyToReconnect);
+ request
+ .reconnect(presentationId)
+ .then(aConnection => {
+ connection = aConnection;
+ ok(connection, "Sender: Connection should be available.");
+ is(
+ connection.id,
+ presentationId,
+ "The presentationId should be the same."
+ );
+ is(
+ connection.state,
+ "connecting",
+ "The initial state should be connecting."
+ );
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(
+ connection.state,
+ "connected",
+ "Connection should be connected."
+ );
+ aResolve();
+ };
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when establishing a connection: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ }
+ );
+
+ postMessageToIframe("prepare-for-reconnect");
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ debug("Got message: teardown-complete");
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup()
+ .then(testCreateRequest)
+ .then(testStartConnection)
+ .then(testSendMessage)
+ .then(testIncomingMessage)
+ .then(testSendBlobMessage)
+ .then(testCloseConnection)
+ .then(testReconnect)
+ .then(testSendArrayBuffer)
+ .then(testSendArrayBufferView)
+ .then(testCloseConnection)
+ .then(testTerminateAfterClose)
+ .then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ () => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ /* Mocked TCP session transport builder in the test */
+ ["dom.presentation.session_transport.data_channel.enable", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.presentation.test.stage", 0],
+ ["media.navigator.permission.disabled", true],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html
new file mode 100644
index 0000000000..b57573fdd6
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1234492">
+ Test for B2G Presentation API when sender and receiver at the same side</a>
+ <script type="application/javascript" src="test_presentation_1ua_sender_and_receiver.js">
+ </script>
+ </body>
+</html>
diff --git 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
new file mode 100644
index 0000000000..28004487d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side (OOP ver.)</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1234492">
+ Test for B2G Presentation API when sender and receiver at the same side (OOP ver.)</a>
+ <script type="application/javascript" src="test_presentation_1ua_sender_and_receiver.js">
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_availability.html b/dom/presentation/tests/mochitest/test_presentation_availability.html
new file mode 100644
index 0000000000..7d7a348353
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_availability.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for PresentationAvailability</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1228508">Test PresentationAvailability</a>
+<script type="application/javascript">
+// This test loads in an iframe, to ensure that the navigator instance is
+// loaded with the correct value of the preference.
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ () => {
+ let iframe = document.createElement("iframe");
+ iframe.src = "test_presentation_availability_iframe.html";
+ document.body.appendChild(iframe);
+ }
+ );
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html b/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html
new file mode 100644
index 0000000000..135b3b92bc
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html
@@ -0,0 +1,227 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for PresentationAvailability</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1228508">Test PresentationAvailability</a>
+<script type="application/javascript">
+let ok = window.parent.ok;
+let is = window.parent.is;
+let isnot = window.parent.isnot;
+let SimpleTest = window.parent.SimpleTest;
+let SpecialPowers = window.parent.SpecialPowers;
+
+"use strict";
+
+var testDevice = {
+ id: "id",
+ name: "name",
+ type: "type",
+};
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationDeviceInfoChromeScript.js"));
+var request;
+var availability;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("setup-complete", function() {
+ aResolve();
+ });
+ gScript.sendAsyncMessage("setup");
+ });
+}
+
+function testInitialUnavailable() {
+ request = new PresentationRequest("https://example.com");
+
+ return request.getAvailability().then(async function(aAvailability) {
+ is(aAvailability.value, false, "Should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ };
+ availability = aAvailability;
+ await gScript.sendQuery("trigger-device-add", testDevice);
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testInitialAvailable() {
+ let anotherRequest = new PresentationRequest("https://example.net");
+ return anotherRequest.getAvailability().then(function(aAvailability) {
+ is(aAvailability.value, true, "Should have available device initially");
+ isnot(aAvailability, availability, "Should get different availability object for different request URL");
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testSameObject() {
+ let sameUrlRequest = new PresentationRequest("https://example.com");
+ return sameUrlRequest.getAvailability().then(function(aAvailability) {
+ is(aAvailability, availability, "Should get same availability object for same request URL");
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testOnChangeEvent() {
+ return new Promise(function(aResolve, aReject) {
+ availability.onchange = function() {
+ availability.onchange = null;
+ is(availability.value, false, "Should have no available device after device removed");
+ aResolve();
+ };
+ gScript.sendAsyncMessage("trigger-device-remove");
+ });
+}
+
+function testConsecutiveGetAvailability() {
+ let presRequest = new PresentationRequest("https://example.org");
+ let firstAvailabilityResolved = false;
+ return Promise.all([
+ presRequest.getAvailability().then(function() {
+ firstAvailabilityResolved = true;
+ }),
+ presRequest.getAvailability().then(function() {
+ ok(firstAvailabilityResolved, "getAvailability() should be resolved in sequence");
+ }),
+ ]).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testUnsupportedDeviceAvailability() {
+ return Promise.race([
+ new Promise(function(aResolve, aReject) {
+ let presRequest = new PresentationRequest("https://test.com");
+ presRequest.getAvailability().then(function(aAvailability) {
+ availability = aAvailability;
+ aAvailability.onchange = function() {
+ availability.onchange = null;
+ ok(false, "Should not get onchange event.");
+ teardown();
+ };
+ });
+ gScript.sendAsyncMessage("trigger-add-unsupport-url-device");
+ }),
+ new Promise(function(aResolve, aReject) {
+ setTimeout(function() {
+ ok(true, "Should not get onchange event.");
+ availability.onchange = null;
+ gScript.sendAsyncMessage("trigger-remove-unsupported-device");
+ aResolve();
+ }, 3000);
+ }),
+ ]);
+}
+
+function testMultipleAvailabilityURLs() {
+ let request1 = new PresentationRequest(["https://example.com",
+ "https://example1.com"]);
+ let request2 = new PresentationRequest(["https://example1.com",
+ "https://example2.com"]);
+ return Promise.all([
+ request1.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ request2.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-add-multiple-devices");
+ aResolve();
+ }),
+ ]).then(new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-remove-multiple-devices");
+ aResolve();
+ }));
+}
+
+function testPartialSupportedDeviceAvailability() {
+ let request1 = new PresentationRequest(["https://supportedUrl.com"]);
+ let request2 = new PresentationRequest(["http://notSupportedUrl.com"]);
+
+ return Promise.all([
+ request1.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ Promise.race([
+ request2.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(false, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ new Promise(function(aResolve) {
+ setTimeout(function() {
+ ok(true, "Should not get onchange event.");
+ availability.onchange = null;
+ aResolve();
+ }, 3000);
+ }),
+ ]),
+ new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-add-https-devices");
+ aResolve();
+ }),
+ ]).then(new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-remove-https-devices");
+ aResolve();
+ }));
+}
+
+function teardown() {
+ request = null;
+ availability = null;
+ gScript.sendAsyncMessage("teardown");
+ gScript.destroy();
+ SimpleTest.finish();
+}
+
+ok(navigator.presentation, "navigator.presentation should be available.");
+testSetup().then(testInitialUnavailable)
+ .then(testInitialAvailable)
+ .then(testSameObject)
+ .then(testOnChangeEvent)
+ .then(testConsecutiveGetAvailability)
+ .then(testMultipleAvailabilityURLs)
+ .then(testUnsupportedDeviceAvailability)
+ .then(testPartialSupportedDeviceAvailability)
+ .then(teardown);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
new file mode 100644
index 0000000000..6c0fa8f4c7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
@@ -0,0 +1,243 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for data channel as session transport in Presentation API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for data channel as session transport in Presentation API</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const loadingTimeoutPref = "presentation.receiver.loading.timeout";
+
+var clientBuilder;
+var serverBuilder;
+var clientTransport;
+var serverTransport;
+
+const clientMessage = "Client Message";
+const serverMessage = "Server Message";
+
+const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+var gResolve;
+var gReject;
+
+const clientCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportCallback"]),
+ notifyTransportReady() {
+ info("Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ info("Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData(aData) {
+ is(aData, serverMessage, "Client transport receives data.");
+ gResolve();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportCallback"]),
+ notifyTransportReady() {
+ info("Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ info("Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData(aData) {
+ is(aData, clientMessage, "Server transport receives data.");
+ gResolve();
+ },
+};
+
+const clientListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportBuilderListener"]),
+ onSessionTransport(aTransport) {
+ info("Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+ },
+ onError(aError) {
+ ok(false, "client's builder reports error " + aError);
+ },
+ sendOffer(aOffer) {
+ setTimeout(() => this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer(aAnswer) {
+ setTimeout(() => this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate(aCandidate) {
+ setTimeout(() => this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect(aReason) {
+ setTimeout(() => this._localBuilder.notifyDisconnected(aReason), 0);
+ setTimeout(() => this._remoteBuilder.notifyDisconnected(aReason), 0);
+ },
+ set remoteBuilder(aRemoteBuilder) {
+ this._remoteBuilder = aRemoteBuilder;
+ },
+ set localBuilder(aLocalBuilder) {
+ this._localBuilder = aLocalBuilder;
+ },
+};
+
+const serverListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportBuilderListener"]),
+ onSessionTransport(aTransport) {
+ info("Server Transport is built.");
+ serverTransport = aTransport;
+ serverTransport.callback = serverCallback;
+ serverTransport.enableDataNotification();
+ },
+ onError(aError) {
+ ok(false, "server's builder reports error " + aError);
+ },
+ sendOffer(aOffer) {
+ setTimeout(() => this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer(aAnswer) {
+ setTimeout(() => this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate(aCandidate) {
+ setTimeout(() => this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect(aReason) {
+ setTimeout(() => this._localBuilder.notifyDisconnected(aReason), 0);
+ setTimeout(() => this._remoteBuilder.notifyDisconnected(aReason), 0);
+ },
+ set remoteBuilder(aRemoteBuilder) {
+ this._remoteBuilder = aRemoteBuilder;
+ },
+ set localBuilder(aLocalBuilder) {
+ this._localBuilder = aLocalBuilder;
+ },
+};
+
+function testBuilder() {
+ return new Promise(function(aResolve, aReject) {
+ gResolve = aResolve;
+ gReject = aReject;
+
+ clientBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+ .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+ serverBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+ .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+
+ clientListener.localBuilder = clientBuilder;
+ clientListener.remoteBuilder = serverBuilder;
+ serverListener.localBuilder = serverBuilder;
+ serverListener.remoteBuilder = clientBuilder;
+
+ clientBuilder
+ .buildDataChannelTransport(Ci.nsIPresentationService.ROLE_CONTROLLER,
+ window,
+ clientListener);
+
+ serverBuilder
+ .buildDataChannelTransport(Ci.nsIPresentationService.ROLE_RECEIVER,
+ window,
+ serverListener);
+ });
+}
+
+function testClientSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("client sends message");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ clientTransport.send(clientMessage);
+ });
+}
+
+function testServerSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("server sends message");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ serverTransport.send(serverMessage);
+ setTimeout(() => clientTransport.enableDataNotification(), 0);
+ });
+}
+
+function testCloseSessionTransport() {
+ return new Promise(function(aResolve, aReject) {
+ info("close session transport");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ serverTransport.close(Cr.NS_OK);
+ });
+}
+
+function finish() {
+ info("test finished, teardown");
+ Services.prefs.clearUserPref(loadingTimeoutPref);
+
+ SimpleTest.finish();
+}
+
+function error(aError) {
+ ok(false, "report Error " + aError.name + ":" + aError.message);
+ gReject();
+}
+
+function runTests() {
+ Services.prefs.setIntPref(loadingTimeoutPref, 30000);
+
+ testBuilder()
+ .then(testClientSendMessage)
+ .then(testServerSendMessage)
+ .then(testCloseSessionTransport)
+ .then(finish)
+ .catch(error);
+}
+
+window.addEventListener("load", function() {
+ runTests();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html
new file mode 100644
index 0000000000..6ea64b2843
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for B2G PresentationConnection API at receiver side</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", receiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ iframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) {
+ var message = evt.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, message.replace(/^OK /, ""));
+ } else if (/^KO /.exec(message)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ iframe.removeEventListener("mozbrowsershowmodalprompt",
+ receiverListener);
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aInnerResolve, aInnerReject) {
+ document.body.appendChild(iframe);
+
+ aInnerResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ gScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+
+ aResolve();
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ ok(navigator.presentation, "navigator.presentation should be available in in-process pages.");
+ is(navigator.presentation.receiver, null, "Non-receiving in-process pages shouldn't get a presentation receiver instance.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup().
+ then(testIncomingSessionRequest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
new file mode 100644
index 0000000000..f828fc44f3
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
@@ -0,0 +1,209 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side (OOP)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test B2G PresentationConnection API at receiver side (OOP)</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+var nonReceiverUrl = SimpleTest.getTestFileURL("file_presentation_non_receiver.html");
+
+var isReceiverFinished = false;
+var isNonReceiverFinished = false;
+
+var obs = SpecialPowers.Services.obs;
+var receiverIframe;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ // Create a receiver OOP iframe.
+ receiverIframe = document.createElement("iframe");
+ receiverIframe.setAttribute("remote", "true");
+ receiverIframe.setAttribute("mozbrowser", "true");
+ receiverIframe.setAttribute("mozpresentation", receiverUrl);
+ receiverIframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(aEvent) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ if (command.name == "trigger-incoming-message") {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.sendAsyncMessage("trigger-incoming-message", {"data": command.data});
+ } else {
+ gScript.sendAsyncMessage(command.name, command.data);
+ }
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener);
+
+ isReceiverFinished = true;
+
+ if (isNonReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ var promise = new Promise(function(aInnerResolve, aInnerReject) {
+ document.body.appendChild(receiverIframe);
+ receiverIframe.addEventListener("mozbrowserloadstart", function() {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.loadFrameScript("data:,(" + loadPrivilegedScriptTest.toString() + ")();", false);
+ }, {once: true});
+
+ aInnerResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ // Create a non-receiver OOP iframe.
+ var nonReceiverIframe = document.createElement("iframe");
+ nonReceiverIframe.setAttribute("remote", "true");
+ nonReceiverIframe.setAttribute("mozbrowser", "true");
+ nonReceiverIframe.setAttribute("src", nonReceiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ nonReceiverIframe.addEventListener("mozbrowsershowmodalprompt", function nonReceiverListener(aEvent) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ nonReceiverIframe.removeEventListener("mozbrowsershowmodalprompt", nonReceiverListener);
+
+ isNonReceiverFinished = true;
+
+ if (isReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ document.body.appendChild(nonReceiverIframe);
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ mm.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(SpecialPowers.wrap(aSuccess).data.data, "buildDataChannel get correct window object");
+ });
+
+ mm.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ mm.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ mm.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ mm.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(SpecialPowers.wrap(aReason).data.data, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+
+ aResolve();
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+var mmTeardownComplete = false;
+var gScriptTeardownComplete = false;
+function teardown() {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ mm.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ mmTeardownComplete = true;
+ if (gScriptTeardownComplete) {
+ SimpleTest.finish();
+ }
+ });
+
+ mm.sendAsyncMessage("teardown");
+
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ gScriptTeardownComplete = true;
+ if (mmTeardownComplete) {
+ SimpleTest.finish();
+ }
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup().
+ then(testIncomingSessionRequest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true],
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["presentation.receiver.loading.timeout", 5000000]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_sender.html b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
new file mode 100644
index 0000000000..40f8b337df
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
@@ -0,0 +1,289 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var frameScript = SpecialPowers.isMainProcess() ? gScript : contentScript;
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com/");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ frameScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ frameScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ });
+
+ frameScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ frameScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testSend() {
+ return new Promise(function(aResolve, aReject) {
+ const outgoingMessage = "test outgoing message";
+
+ frameScript.addMessageListener("message-sent", function messageSentHandler(aMessage) {
+ frameScript.removeMessageListener("message-sent", messageSentHandler);
+ is(aMessage, outgoingMessage, "The message is sent out.");
+ aResolve();
+ });
+
+ connection.send(outgoingMessage);
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ frameScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ frameScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ frameScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnect ---");
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablished() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablished);
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "http://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ frameScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ info("teardown-complete");
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function testConstructRequestError() {
+ return Promise.all([
+ // XXX: Bug 1305204 - uncomment when bug 1275746 is fixed again.
+ // new Promise(function(aResolve, aReject) {
+ // try {
+ // request = new PresentationRequest("\\\\\\");
+ // }
+ // catch(e) {
+ // is(e.name, "SyntaxError", "Expect to get SyntaxError.");
+ // aResolve();
+ // }
+ // }),
+ new Promise(function(aResolve, aReject) {
+ try {
+ request = new PresentationRequest([]);
+ } catch (e) {
+ is(e.name, "NotSupportedError", "Expect to get NotSupportedError.");
+ aResolve();
+ }
+ }),
+ ]);
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testSend).
+ then(testIncomingMessage).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(testConstructRequestError).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html b/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html
new file mode 100644
index 0000000000..28788b1252
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+/* global SimpleTest SpecialPowers */
+
+const gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js"));
+const kReceiverFile = "file_presentation_fingerprinting_resistance_receiver.html";
+const kReceiverUrl = SimpleTest.getTestFileURL(kReceiverFile);
+
+let runTests = async () => {
+ await setup();
+ let request = await createRequest();
+ let iframe = await testRequestAndReceiver(request);
+ await enableResistFingerprinting();
+ await testRequestResistFingerprinting(request);
+ await testReceiverResistFingerprinting(iframe);
+ teardown();
+};
+
+let setup = async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.presentation.test.stage", 0],
+ ],
+ });
+
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("promise-setup-ready", function promiseSetupReadyHandler() {
+ gScript.removeMessageListener("promise-setup-ready", promiseSetupReadyHandler);
+ gScript.sendAsyncMessage("trigger-on-session-request", kReceiverUrl);
+ });
+};
+
+let createRequest = () => new Promise((resolve, reject) => {
+ let request = new PresentationRequest(kReceiverFile);
+ request.getAvailability().then((availability) => {
+ SimpleTest.ok(availability, "PresentationRequest.getAvailability");
+ availability.onchange = () => {
+ availability.onchange = null;
+ resolve(request);
+ };
+ gScript.sendAsyncMessage("trigger-device-add");
+ }).catch((error) => {
+ SimpleTest.ok(false, "PresentationRequest.getAvailability: " + error);
+ teardown();
+ reject(error);
+ });
+});
+
+let testRequestAndReceiver = (request) => new Promise((resolve, reject) => {
+ gScript.addMessageListener("sender-launch", function senderLaunchHandler(url) {
+ // SimpleTest.is(url, kReceiverUrl, 'sender-launch');
+ gScript.removeMessageListener("sender-launch", senderLaunchHandler);
+
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", kReceiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", kReceiverUrl);
+ iframe.setAttribute("remote", "false");
+ iframe.addEventListener("mozbrowsershowmodalprompt", (event) => {
+ SimpleTest.is(event.detail.message, "true", "navigator.presentation.receiver");
+ resolve(iframe);
+ }, {once: true});
+
+ let promise = new Promise((aInnerResolve) => {
+ document.body.appendChild(iframe);
+ aInnerResolve(iframe);
+ });
+
+ let obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ request.start().then((connection) => {
+ SimpleTest.ok(connection, "PresentationRequest.start");
+ }).catch((error) => {
+ SimpleTest.ok(false, "PresentationRequest.start: " + error);
+ teardown();
+ reject(error);
+ });
+});
+
+let enableResistFingerprinting = () => {
+ const kPref = "privacy.resistFingerprinting";
+ SimpleTest.info(kPref + " = true");
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ [kPref, true],
+ ],
+ });
+};
+
+let testRequestResistFingerprinting = (request) => {
+ return request.getAvailability()
+ .then(() => SimpleTest.ok(false, "PresentationRequest.getAvailability"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.getAvailability"))
+ .then(() => request.start())
+ .then(() => SimpleTest.ok(false, "PresentationRequest.start"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.start"))
+ .then(() => request.reconnect(kReceiverUrl))
+ .then(() => SimpleTest.ok(false, "PresentationRequest.reconnect"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.reconnect"));
+};
+
+let testReceiverResistFingerprinting = (iframe) => new Promise((resolve) => {
+ iframe.addEventListener("mozbrowsershowmodalprompt", (event) => {
+ SimpleTest.is(event.detail.message, "false", "navigator.presentation.receiver");
+ resolve();
+ }, {once: true});
+ iframe.setAttribute("src", kReceiverUrl + "#privacy.resistFingerprinting");
+});
+
+let teardown = () => {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+};
+
+SimpleTest.waitForExplicitFinish();
+document.addEventListener("DOMContentLoaded", () => {
+ SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+ ], runTests);
+});
+</script>
diff --git a/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
new file mode 100644
index 0000000000..4ad4aa99ad
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268758">Test allow-presentation sandboxing flag</a>
+<iframe id="iframe" src="https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var readyToStart = false;
+var testSetuped = false;
+
+function setup() {
+ SpecialPowers.addPermission("presentation",
+ true, { url: "https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html",
+ originAttributes: {
+ inIsolatedMozBrowser: false }});
+
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ if (/^OK /.exec(message)) {
+ ok(true, message.replace(/^OK /, ""));
+ } else if (/^KO /.exec(message)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ if (command === "ready-to-start") {
+ readyToStart = true;
+ startTest();
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ testSetuped = true;
+ aResolve();
+ });
+}
+
+iframe.onload = startTest();
+
+function startTest() {
+ if (!(testSetuped && readyToStart)) {
+ return;
+ }
+ iframe.contentWindow.postMessage("start", "*");
+}
+
+function runTests() {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ setup().then(startTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
new file mode 100644
index 0000000000..c6a23881e2
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
@@ -0,0 +1,91 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_receiver_auxiliary_navigation.html"
+);
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+ var oop = !location.pathname.includes("_inproc");
+ iframe.setAttribute("remote", oop);
+ iframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "postMessage".
+ iframe.addEventListener("mozbrowsershowmodalprompt", function listener(
+ aEvent
+ ) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ iframe.removeEventListener("mozbrowsershowmodalprompt", listener);
+
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ function() {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html
new file mode 100644
index 0000000000..31be5be5ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268810">
+ Test for receiver page with sandboxed auxiliary navigation browsing context flag.</a>
+ <script type="application/javascript" src="test_presentation_receiver_auxiliary_navigation.js">
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html
new file mode 100644
index 0000000000..31be5be5ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268810">
+ Test for receiver page with sandboxed auxiliary navigation browsing context flag.</a>
+ <script type="application/javascript" src="test_presentation_receiver_auxiliary_navigation.js">
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_reconnect.html b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
new file mode 100644
index 0000000000..6febdce57b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
@@ -0,0 +1,378 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197690">Test for Presentation API at sender side</a>
+<iframe id="iframe" src="file_presentation_reconnect.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var frameScript = SpecialPowers.isMainProcess() ? gScript : contentScript;
+var request;
+var connection;
+var commandHandler = {};
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ if (/^OK /.exec(message)) {
+ ok(true, message.replace(/^OK /, ""));
+ } else if (/^KO /.exec(message)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ if (command.name in commandHandler) {
+ commandHandler[command.name](command);
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ request = new PresentationRequest("http://example.com/");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ info("The control channel is closed. " + aReason);
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ info("Data transport channel is initialized.");
+ });
+
+ frameScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ frameScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ frameScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnectAConnectedConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectAConnectedConnection ---");
+ is(connection.state, "connected", "Make sure the state is connected.");
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ is(aConnection.id, connection.id, "Connection ID should be the same.");
+ is(aConnection.state, "connected", "The state should be connected.");
+ is(aConnection, connection, "The connection should be the same.");
+
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testReconnectInvalidID() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectInvalidID ---");
+
+ request.reconnect("dummyID").then(
+ function(aConnection) {
+ ok(false, "Unexpected success.");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Should get NotFoundError.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectInvalidURL() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectInvalidURL ---");
+
+ var request1 = new PresentationRequest("http://invalidURL");
+ request1.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(false, "Unexpected success.");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Should get NotFoundError.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectIframeConnectedConnection() {
+ info("--- testReconnectIframeConnectedConnection ---");
+ gScript.sendAsyncMessage("save-control-channel-listener");
+ return Promise.all([
+ new Promise(function(aResolve, aReject) {
+ commandHandler["connection-connected"] = function(command) {
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ var request1 = new PresentationRequest("http://example1.com");
+ request1.reconnect(command.id).then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The state should be connecting.");
+ aConnection.onclose = function() {
+ delete commandHandler["connection-connected"];
+ gScript.sendAsyncMessage("restore-control-channel-listener");
+ aResolve();
+ };
+ aConnection.close();
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ };
+ iframe.contentWindow.postMessage("startConnection", "*");
+ }),
+ new Promise(function(aResolve, aReject) {
+ commandHandler["notify-connection-closed"] = function(command) {
+ delete commandHandler["notify-connection-closed"];
+ aResolve();
+ };
+ }),
+ ]);
+}
+
+function testReconnectIframeClosedConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectIframeClosedConnection ---");
+ gScript.sendAsyncMessage("save-control-channel-listener");
+ commandHandler["connection-closed"] = function(command) {
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ var request1 = new PresentationRequest("http://example1.com");
+ request1.reconnect(command.id).then(
+ function(aConnection) {
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "The connection should be connected.");
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ ok(true, "The connection is closed.");
+ delete commandHandler["connection-closed"];
+ aResolve();
+ };
+ aConnection.close();
+ gScript.sendAsyncMessage("restore-control-channel-listener");
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ };
+ iframe.contentWindow.postMessage("closeConnection", "*");
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnect ---");
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "http://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+
+ const incomingMessage = "test incoming message";
+ aConnection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ frameScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ info("teardown-complete");
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testReconnectInvalidID).
+ then(testReconnectInvalidURL).
+ then(testReconnectAConnectedConnection).
+ then(testReconnectIframeConnectedConnection).
+ then(testReconnectIframeClosedConnection).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html b/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html
new file mode 100644
index 0000000000..d5902d6223
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268758">Test allow-presentation sandboxing flag</a>
+<iframe sandbox="allow-popups allow-scripts allow-same-origin" id="iframe" src="file_presentation_sandboxed_presentation.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var readyToStart = false;
+var testSetuped = false;
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ if (/^OK /.exec(message)) {
+ ok(true, message.replace(/^OK /, ""));
+ } else if (/^KO /.exec(message)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ if (command === "ready-to-start") {
+ readyToStart = true;
+ startTest();
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ testSetuped = true;
+ aResolve();
+ });
+}
+
+iframe.onload = startTest();
+
+function startTest() {
+ if (!(testSetuped && readyToStart)) {
+ return;
+ }
+ iframe.contentWindow.postMessage("start", "*");
+}
+
+function runTests() {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ setup().then(startTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
new file mode 100644
index 0000000000..7da672a080
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test onTerminateRequest at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276378">Test onTerminateRequest at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testOnTerminateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ is(connection.state, "terminated", "Connection should be closed.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-incoming-terminate-request");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testOnTerminateRequest).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html b/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html
new file mode 100644
index 0000000000..6bcf379058
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html
@@ -0,0 +1,173 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test startWithDevice for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1239242">Test startWithDevice for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("https://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionWithDevice() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ ok(false, "Device prompt should not be triggered.");
+ teardown();
+ aReject();
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.startWithDevice("id").then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionWithDeviceNotFoundError() {
+ return new Promise(function(aResolve, aReject) {
+ request.startWithDevice("").then(
+ function(aConnection) {
+ ok(false, "Should not establish connection to an unknown device");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Expect NotFoundError occurred when establishing a connection");
+ aResolve();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnectionWithDevice).
+ then(testStartConnectionWithDeviceNotFoundError).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.presentation.test.stage", 0]]},
+ runTests);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
new file mode 100644
index 0000000000..52196d9913
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for B2G PresentationConnection API at receiver side</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+ iframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "postMessage".
+ iframe.addEventListener("mozbrowsershowmodalprompt", function listener(aEvent) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ iframe.removeEventListener("mozbrowsershowmodalprompt", listener);
+
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ ok(navigator.presentation, "navigator.presentation should be available in in-process pages.");
+ is(navigator.presentation.receiver, null, "Non-receiving in-process pages shouldn't get a presentation receiver instance.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
new file mode 100644
index 0000000000..1c553f2fa9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing errors of B2G Presentation API at receiver side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver_establish_connection_error.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", receiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ iframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) {
+ var message = evt.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, message.replace(/^OK /, ""));
+ } else if (/^KO /.exec(message)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ iframe.removeEventListener("mozbrowsershowmodalprompt",
+ receiverListener);
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, 0x80004004 /* NS_ERROR_ABORT */, "The control channel is closed abnormally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
new file mode 100644
index 0000000000..067f9691d1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing timeout of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing timeout of B2G Presentation API at receiver side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var promise = new Promise(function(aResolve, aReject) {
+ // In order to trigger timeout, do not resolve the promise.
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+function testIncomingSessionRequestReceiverLaunchTimeout() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, 0x80530017 /* NS_ERROR_DOM_TIMEOUT_ERR */, "The control channel is closed due to timeout.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", "http://example.com");
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequestReceiverLaunchTimeout().
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["presentation.receiver.loading.timeout", 10]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
new file mode 100644
index 0000000000..6521846a10
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
@@ -0,0 +1,116 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_unknown_content_type.test"
+);
+
+var obs = SpecialPowers.Services.obs;
+
+var receiverIframe;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ receiverIframe = document.createElement("iframe");
+ receiverIframe.setAttribute("mozbrowser", "true");
+ receiverIframe.setAttribute("mozpresentation", receiverUrl);
+ receiverIframe.setAttribute("src", receiverUrl);
+ var oop = !location.pathname.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+function testIncomingSessionRequestReceiverLaunchUnknownContentType() {
+ let promise = Promise.all([
+ new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener(
+ "receiver-launching",
+ function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener(
+ "receiver-launching",
+ launchReceiverHandler
+ );
+ info("Trying to launch receiver page.");
+
+ receiverIframe.addEventListener("mozbrowserclose", function() {
+ ok(true, "observe receiver window closed");
+ aResolve();
+ });
+ }
+ );
+ }),
+ new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener(
+ "control-channel-closed",
+ function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener(
+ "control-channel-closed",
+ controlChannelClosedHandler
+ );
+ is(
+ aReason,
+ 0x80530020 /* NS_ERROR_DOM_OPERATION_ERR */,
+ "The control channel is closed due to load failure."
+ );
+ aResolve();
+ }
+ );
+ }),
+ ]);
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ return promise;
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+
+ testIncomingSessionRequestReceiverLaunchUnknownContentType().then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ function() {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.ipc.tabs.disabled", false],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html
new file mode 100644
index 0000000000..acf800e609
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for unknown content type of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287717">Test for unknown content type of B2G Presentation API at receiver side</a>
+ <script type="application/javascript" src="test_presentation_tcp_receiver_establish_connection_unknown_content_type.js">
+ </script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html
new file mode 100644
index 0000000000..c78b48b39d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for unknown content type of B2G Presentation API at receiver side (OOP)</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287717">Test for unknown content type of B2G Presentation API at receiver side (OOP)</a>
+ <script type="application/javascript" src="test_presentation_tcp_receiver_establish_connection_unknown_content_type.js">
+ </script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
new file mode 100644
index 0000000000..cc6d73745b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side (OOP)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test B2G PresentationConnection API at receiver side (OOP)</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+var nonReceiverUrl = SimpleTest.getTestFileURL("file_presentation_non_receiver.html");
+
+var isReceiverFinished = false;
+var isNonReceiverFinished = false;
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ // Create a receiver OOP iframe.
+ var receiverIframe = document.createElement("iframe");
+ receiverIframe.setAttribute("remote", "true");
+ receiverIframe.setAttribute("mozbrowser", "true");
+ receiverIframe.setAttribute("mozpresentation", receiverUrl);
+ receiverIframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(aEvent) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener);
+
+ isReceiverFinished = true;
+
+ if (isNonReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ // Create a non-receiver OOP iframe.
+ var nonReceiverIframe = document.createElement("iframe");
+ nonReceiverIframe.setAttribute("remote", "true");
+ nonReceiverIframe.setAttribute("mozbrowser", "true");
+ nonReceiverIframe.setAttribute("src", nonReceiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ nonReceiverIframe.addEventListener("mozbrowsershowmodalprompt", function nonReceiverListener(aEvent) {
+ var message = aEvent.detail.message;
+ if (/^OK /.exec(message)) {
+ ok(true, "Message from iframe: " + message);
+ } else if (/^KO /.exec(message)) {
+ ok(false, "Message from iframe: " + message);
+ } else if (/^INFO /.exec(message)) {
+ info("Message from iframe: " + message);
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ nonReceiverIframe.removeEventListener("mozbrowsershowmodalprompt", nonReceiverListener);
+
+ isNonReceiverFinished = true;
+
+ if (isReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ document.body.appendChild(nonReceiverIframe);
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.ipc.browser_frames.oop_by_default", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
new file mode 100644
index 0000000000..e03c43c54c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
@@ -0,0 +1,258 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("https://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testSend() {
+ return new Promise(function(aResolve, aReject) {
+ const outgoingMessage = "test outgoing message";
+
+ gScript.addMessageListener("message-sent", function messageSentHandler(aMessage) {
+ gScript.removeMessageListener("message-sent", messageSentHandler);
+ is(aMessage, outgoingMessage, "The message is sent out.");
+ aResolve();
+ });
+
+ connection.send(outgoingMessage);
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ gScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnect ---");
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablished() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablished);
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "https://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler() {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testSend).
+ then(testIncomingMessage).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html
new file mode 100644
index 0000000000..a4b4b1ffc7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html
@@ -0,0 +1,151 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test default request for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ navigator.presentation.defaultRequest = new PresentationRequest("https://example.com");
+
+ navigator.presentation.defaultRequest.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler() {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ info("An offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ is(navigator.presentation.receiver, undefined, "Sender shouldn't get a presentation receiver instance.");
+
+ navigator.presentation.defaultRequest.onconnectionavailable = function(aEvent) {
+ navigator.presentation.defaultRequest.onconnectionavailable = null;
+ connection = aEvent.connection;
+ ok(connection, "|connectionavailable| event is fired with a connection.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ };
+
+ // Simulate the UA triggers |start()| of the default request.
+ navigator.presentation.defaultRequest.start();
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+ ok(navigator.presentation, "navigator.presentation should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html
new file mode 100644
index 0000000000..d08e376999
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for disconnection of B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for disconnection of B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testDisconnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-data-transport-close", SpecialPowers.Cr.NS_ERROR_FAILURE);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testDisconnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html
new file mode 100644
index 0000000000..af3fa93024
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html
@@ -0,0 +1,514 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing errors of B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionCancelPrompt() {
+ info("--- testStartConnectionCancelPrompt ---");
+ return Promise.all([
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-cancel", SpecialPowers.Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ resolve();
+ });
+ }),
+ request.start().then(
+ function(aConnection) {
+ ok(false, "|start| shouldn't succeed in this case.");
+ },
+ function(aError) {
+ is(aError.name, "NotAllowedError", "NotAllowedError is expected when the prompt is canceled.");
+ }
+ ),
+ ]);
+}
+
+function testStartConnectionNoDevice() {
+ info("--- testStartConnectionNoDevice ---");
+ return Promise.all([
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-cancel", SpecialPowers.Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ resolve();
+ });
+ }),
+ request.start().then(
+ function(aConnection) {
+ ok(false, "|start| shouldn't succeed in this case.");
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "NotFoundError is expected when no available device.");
+ }
+ ),
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_ERROR_FAILURE, "The control channel is closed with NS_ERROR_FAILURE");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_ERROR_FAILURE);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed with NS_OK");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_OK);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_ERROR_ABORT, "The control channel is closed with NS_ERROR_ABORT");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_ERROR_ABORT);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady -- ");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed with NS_OK");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_OK);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedDataTransportClose() {
+ info("--- testStartConnectionUnexpectedDataTransportClose ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ info("recv offer-sent.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-data-transport-close", SpecialPowers.Cr.NS_ERROR_UNEXPECTED);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ setup().
+ then(testStartConnectionCancelPrompt).
+ then(testStartConnectionNoDevice).
+ then(testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit).
+ then(testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit).
+ then(testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady).
+ then(testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady).
+ then(testStartConnectionUnexpectedDataTransportClose).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate.js b/dom/presentation/tests/mochitest/test_presentation_terminate.js
new file mode 100644
index 0000000000..ed43986534
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,325 @@
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+
+function debug(str) {
+ // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_terminate.html");
+var request;
+var connection;
+var receiverIframe;
+
+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() {
+ 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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ receiverIframe.removeEventListener(
+ "mozbrowsershowmodalprompt",
+ receiverListener
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ gScript.addMessageListener(
+ "promise-setup-ready",
+ function promiseSetupReadyHandler() {
+ debug("Got message: promise-setup-ready");
+ gScript.removeMessageListener(
+ "promise-setup-ready",
+ promiseSetupReadyHandler
+ );
+ gScript.sendAsyncMessage("trigger-on-session-request", receiverUrl);
+ }
+ );
+
+ return Promise.resolve();
+}
+
+function testCreateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testCreateRequest ---");
+ request = new PresentationRequest(receiverUrl);
+ request
+ .getAvailability()
+ .then(aAvailability => {
+ is(
+ aAvailability.value,
+ false,
+ "Sender: should have no available device after setup"
+ );
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Sender: Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when getting availability: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ request
+ .start()
+ .then(aConnection => {
+ connection = aConnection;
+ ok(connection, "Sender: Connection should be available.");
+ ok(connection.id, "Sender: Connection ID should be set.");
+ is(
+ connection.state,
+ "connecting",
+ "Sender: The initial state should be connecting."
+ );
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+
+ info("Sender: test terminate at connecting state");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ ok(false, "Should not be able to terminate at connecting state");
+ aReject();
+ };
+ connection.terminate();
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when establishing a connection: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testConnectionTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testConnectionTerminate---");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ is(
+ connection.state,
+ "terminated",
+ "Sender: Connection should be terminated."
+ );
+ };
+ gScript.addMessageListener(
+ "control-channel-established",
+ function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener(
+ "control-channel-established",
+ controlChannelEstablishedHandler
+ );
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ }
+ );
+ gScript.addMessageListener(
+ "sender-terminate",
+ function senderTerminateHandler() {
+ gScript.removeMessageListener(
+ "sender-terminate",
+ senderTerminateHandler
+ );
+
+ Promise.all([
+ new Promise(resolve => {
+ gScript.addMessageListener(
+ "device-disconnected",
+ function deviceDisconnectedHandler() {
+ gScript.removeMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ ok(true, "observe device disconnect");
+ resolve();
+ }
+ );
+ }),
+ new Promise(resolve => {
+ receiverIframe.addEventListener("mozbrowserclose", function() {
+ ok(true, "observe receiver page closing");
+ resolve();
+ });
+ }),
+ ]).then(aResolve);
+
+ gScript.sendAsyncMessage("trigger-on-terminate-request");
+ }
+ );
+ gScript.addMessageListener(
+ "ready-to-terminate",
+ function onReadyToTerminate() {
+ gScript.removeMessageListener("ready-to-terminate", onReadyToTerminate);
+ connection.terminate();
+
+ // test unexpected close right after terminate
+ connection.onclose = function() {
+ ok(false, "close after terminate should do nothing");
+ };
+ connection.close();
+ }
+ );
+ });
+}
+
+function testSendAfterTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ try {
+ connection.send("something");
+ ok(false, "PresentationConnection.send should be failed");
+ } catch (e) {
+ is(e.name, "InvalidStateError", "Must throw InvalidStateError");
+ }
+ aResolve();
+ });
+}
+
+function testCloseAfterTerminate() {
+ return Promise.race([
+ new Promise(function(aResolve, aReject) {
+ connection.onclose = function() {
+ connection.onclose = null;
+ ok(false, "close at terminated state should do nothing");
+ aResolve();
+ };
+ connection.close();
+ }),
+ new Promise(function(aResolve, aReject) {
+ setTimeout(function() {
+ is(
+ connection.state,
+ "terminated",
+ "Sender: Connection should be terminated."
+ );
+ aResolve();
+ }, 3000);
+ }),
+ ]);
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ debug("Got message: teardown-complete");
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup()
+ .then(testCreateRequest)
+ .then(testStartConnection)
+ .then(testConnectionTerminate)
+ .then(testSendAfterTerminate)
+ .then(testCloseAfterTerminate)
+ .then(teardown);
+}
+
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ () => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.ipc.tabs.disabled", false],
+ ["dom.presentation.test.stage", 0],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
new file mode 100644
index 0000000000..7a541911f3
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
@@ -0,0 +1,266 @@
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+
+function debug(str) {
+ // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_terminate_establish_connection_error.html"
+);
+var request;
+var connection;
+var receiverIframe;
+
+function postMessageToIframe(aType) {
+ receiverIframe.src =
+ receiverUrl + "#" + encodeURIComponent(JSON.stringify({ type: aType }));
+}
+
+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() {
+ 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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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)) {
+ ok(false, message.replace(/^KO /, ""));
+ } else if (/^INFO /.exec(message)) {
+ info(message.replace(/^INFO /, ""));
+ } else if (/^COMMAND /.exec(message)) {
+ var command = JSON.parse(message.replace(/^COMMAND /, ""));
+ gScript.sendAsyncMessage(command.name, command.data);
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ receiverIframe.removeEventListener(
+ "mozbrowsershowmodalprompt",
+ receiverListener
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ gScript.addMessageListener(
+ "promise-setup-ready",
+ function promiseSetupReadyHandler() {
+ debug("Got message: promise-setup-ready");
+ gScript.removeMessageListener(
+ "promise-setup-ready",
+ promiseSetupReadyHandler
+ );
+ gScript.sendAsyncMessage("trigger-on-session-request", receiverUrl);
+ }
+ );
+
+ return Promise.resolve();
+}
+
+function testCreateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testCreateRequest ---");
+ request = new PresentationRequest(receiverUrl);
+ request
+ .getAvailability()
+ .then(aAvailability => {
+ is(
+ aAvailability.value,
+ false,
+ "Sender: should have no available device after setup"
+ );
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Sender: Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when getting availability: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ request
+ .start()
+ .then(aConnection => {
+ connection = aConnection;
+ ok(connection, "Sender: Connection should be available.");
+ ok(connection.id, "Sender: Connection ID should be set.");
+ is(
+ connection.state,
+ "connecting",
+ "Sender: The initial state should be connecting."
+ );
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ })
+ .catch(aError => {
+ ok(
+ false,
+ "Sender: Error occurred when establishing a connection: " + aError
+ );
+ teardown();
+ aReject();
+ });
+ });
+}
+
+function testConnectionTerminate() {
+ info("Sender: --- testConnectionTerminate---");
+ let promise = Promise.all([
+ new Promise(function(aResolve, aReject) {
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Sender: Connection should be closed.");
+ aResolve();
+ };
+ }),
+ new Promise(function(aResolve, aReject) {
+ function deviceDisconnectedHandler() {
+ gScript.removeMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ ok(true, "should not receive device disconnect");
+ aResolve();
+ }
+
+ gScript.addMessageListener(
+ "device-disconnected",
+ deviceDisconnectedHandler
+ );
+ }),
+ new Promise(function(aResolve, aReject) {
+ receiverIframe.addEventListener("mozbrowserclose", function() {
+ ok(true, "observe receiver page closing");
+ aResolve();
+ });
+ }),
+ ]);
+
+ gScript.addMessageListener(
+ "prepare-for-terminate",
+ function prepareForTerminateHandler() {
+ debug("Got message: prepare-for-terminate");
+ gScript.removeMessageListener(
+ "prepare-for-terminate",
+ prepareForTerminateHandler
+ );
+ gScript.sendAsyncMessage("trigger-control-channel-error");
+ postMessageToIframe("ready-to-terminate");
+ }
+ );
+
+ return promise;
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ debug("Got message: teardown-complete");
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup()
+ .then(testCreateRequest)
+ .then(testStartConnection)
+ .then(testConnectionTerminate)
+ .then(teardown);
+}
+
+SpecialPowers.pushPermissions(
+ [
+ { type: "presentation-device-manage", allow: false, context: document },
+ { type: "browser", allow: true, context: document },
+ ],
+ () => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.ipc.tabs.disabled", false],
+ ["dom.presentation.test.stage", 0],
+ ],
+ },
+ runTests
+ );
+ }
+);
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
new file mode 100644
index 0000000000..9d702c2e1e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+ Test for constrol channel establish error during PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate_establish_connection_error.js'>
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
new file mode 100644
index 0000000000..9d702c2e1e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+ Test for constrol channel establish error during PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate_establish_connection_error.js'>
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
new file mode 100644
index 0000000000..24e6f1dc99
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+ Test for PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate.js'>
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
new file mode 100644
index 0000000000..24e6f1dc99
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+ Test for PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate.js'>
+ </script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
new file mode 100644
index 0000000000..4ccc0a54ef
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
@@ -0,0 +1,1465 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cm = Components.manager;
+
+const { Promise } = ChromeUtils.import("resource://gre/modules/Promise.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const INFO_CONTRACT_ID =
+ "@mozilla.org/toolkit/components/mdnsresponder/dns-info;1";
+const PROVIDER_CONTRACT_ID =
+ "@mozilla.org/presentation-device/multicastdns-provider;1";
+const SD_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-sd;1";
+const UUID_CONTRACT_ID = "@mozilla.org/uuid-generator;1";
+const SERVER_CONTRACT_ID = "@mozilla.org/presentation/control-service;1";
+
+const PREF_DISCOVERY = "dom.presentation.discovery.enabled";
+const PREF_DISCOVERABLE = "dom.presentation.discoverable";
+const PREF_DEVICENAME = "dom.presentation.device.name";
+
+const LATEST_VERSION = 1;
+const SERVICE_TYPE = "_presentation-ctrl._tcp";
+const versionAttr = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+);
+versionAttr.setPropertyAsUint32("version", LATEST_VERSION);
+
+var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+function sleep(aMs) {
+ return new Promise(resolve => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ timer.initWithCallback(
+ {
+ notify() {
+ resolve();
+ },
+ },
+ aMs,
+ timer.TYPE_ONE_SHOT
+ );
+ });
+}
+
+function MockFactory(aClass) {
+ this._cls = aClass;
+}
+MockFactory.prototype = {
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ switch (typeof this._cls) {
+ case "function":
+ return new this._cls().QueryInterface(aIID);
+ case "object":
+ return this._cls.QueryInterface(aIID);
+ default:
+ return null;
+ }
+ },
+ lockFactory(aLock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+function ContractHook(aContractID, aClass) {
+ this._contractID = aContractID;
+ this.classID = Cc[UUID_CONTRACT_ID].getService(
+ Ci.nsIUUIDGenerator
+ ).generateUUID();
+ this._newFactory = new MockFactory(aClass);
+
+ if (!this.hookedMap.has(this._contractID)) {
+ this.hookedMap.set(this._contractID, []);
+ }
+
+ this.init();
+}
+
+ContractHook.prototype = {
+ hookedMap: new Map(), // remember only the most original factory.
+
+ init() {
+ this.reset();
+
+ let oldContract = this.unregister();
+ this.hookedMap.get(this._contractID).push(oldContract);
+ registrar.registerFactory(
+ this.classID,
+ "",
+ this._contractID,
+ this._newFactory
+ );
+
+ registerCleanupFunction(() => {
+ this.cleanup.apply(this);
+ });
+ },
+
+ reset() {},
+
+ cleanup() {
+ this.reset();
+
+ this.unregister();
+ let prevContract = this.hookedMap.get(this._contractID).pop();
+
+ if (prevContract.classID) {
+ registrar.registerFactory(
+ prevContract.classID,
+ "",
+ this._contractID,
+ prevContract.factory
+ );
+ }
+ },
+
+ unregister() {
+ var classID, factory;
+
+ try {
+ classID = registrar.contractIDToCID(this._contractID);
+ factory = Cm.getClassObject(Cc[this._contractID], Ci.nsIFactory);
+ } catch (ex) {
+ classID = "";
+ factory = null;
+ }
+
+ if (factory) {
+ try {
+ registrar.unregisterFactory(classID, factory);
+ } catch (e) {
+ factory = null;
+ }
+ }
+
+ return { classID, factory };
+ },
+};
+
+function MockDNSServiceInfo() {}
+MockDNSServiceInfo.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceInfo"]),
+
+ set host(aHost) {
+ this._host = aHost;
+ },
+
+ get host() {
+ return this._host;
+ },
+
+ set address(aAddress) {
+ this._address = aAddress;
+ },
+
+ get address() {
+ return this._address;
+ },
+
+ set port(aPort) {
+ this._port = aPort;
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ set serviceName(aServiceName) {
+ this._serviceName = aServiceName;
+ },
+
+ get serviceName() {
+ return this._serviceName;
+ },
+
+ set serviceType(aServiceType) {
+ this._serviceType = aServiceType;
+ },
+
+ get serviceType() {
+ return this._serviceType;
+ },
+
+ set domainName(aDomainName) {
+ this._domainName = aDomainName;
+ },
+
+ get domainName() {
+ return this._domainName;
+ },
+
+ set attributes(aAttributes) {
+ this._attributes = aAttributes;
+ },
+
+ get attributes() {
+ return this._attributes;
+ },
+};
+
+function TestPresentationDeviceListener() {
+ this.devices = {};
+}
+TestPresentationDeviceListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ addDevice(device) {
+ this.devices[device.id] = device;
+ },
+ removeDevice(device) {
+ delete this.devices[device.id];
+ },
+ updateDevice(device) {
+ this.devices[device.id] = device;
+ },
+ onSessionRequest(device, url, presentationId, controlChannel) {},
+
+ count() {
+ var size = 0,
+ key;
+ for (key in this.devices) {
+ if (this.devices.hasOwnProperty(key)) {
+ ++size;
+ }
+ }
+ return size;
+ },
+};
+
+function createDevice(
+ host,
+ port,
+ serviceName,
+ serviceType,
+ domainName,
+ attributes
+) {
+ let device = new MockDNSServiceInfo();
+ device.host = host || "";
+ device.port = port || 0;
+ device.address = host || "";
+ device.serviceName = serviceName || "";
+ device.serviceType = serviceType || "";
+ device.domainName = domainName || "";
+ device.attributes = attributes || versionAttr;
+ return device;
+}
+
+function registerService() {
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let deferred = Promise.defer();
+
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0,
+ };
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+
+ Assert.equal(mockObj.serviceRegistered, 0);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+
+ // Register
+ provider.listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ };
+
+ deferred.promise.then(function() {
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+
+ // Unregister
+ provider.listener = null;
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 1);
+
+ run_next_test();
+ });
+}
+
+function noRegisterService() {
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+ let deferred = Promise.defer();
+
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ Assert.ok(false, "should not register service if not discoverable");
+ },
+ resolveService(serviceInfo, listener) {},
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+
+ // Try register
+ provider.listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ };
+
+ let race = Promise.race([deferred.promise, sleep(1000)]);
+
+ race.then(() => {
+ provider.listener = null;
+
+ run_next_test();
+ });
+}
+
+function registerServiceDynamically() {
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+ let deferred = Promise.defer();
+
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0,
+ };
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+
+ Assert.equal(mockObj.serviceRegistered, 0);
+ Assert.equal(mockObj.serviceRegistered, 0);
+
+ // Try Register
+ provider.listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ };
+
+ Assert.equal(mockObj.serviceRegistered, 0);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+
+ // Enable registration
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ deferred.promise.then(function() {
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+
+ // Disable registration
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 1);
+
+ // Try unregister
+ provider.listener = null;
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 1);
+
+ run_next_test();
+ });
+}
+
+function addDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = new TestPresentationDeviceListener();
+ Assert.equal(listener.count(), 0);
+
+ // Start discovery
+ provider.listener = listener;
+ Assert.equal(listener.count(), 1);
+
+ // Force discovery again
+ provider.forceDiscovery();
+ Assert.equal(listener.count(), 1);
+
+ provider.listener = null;
+ Assert.equal(listener.count(), 1);
+
+ run_next_test();
+}
+
+function filterDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ let tests = [
+ {
+ requestedUrl: "app://fling-player.gaiamobile.org/index.html",
+ supported: true,
+ },
+ {
+ requestedUrl: "app://notification-receiver.gaiamobile.org/index.html",
+ supported: true,
+ },
+ { requestedUrl: "http://example.com", supported: true },
+ { requestedUrl: "https://example.com", supported: true },
+ { requestedUrl: "ftp://example.com", supported: false },
+ { requestedUrl: "app://unknown-app-id", supported: false },
+ { requestedUrl: "unknowSchem://example.com", supported: false },
+ ];
+
+ for (let test of tests) {
+ Assert.equal(
+ device.isRequestedUrlSupported(test.requestedUrl),
+ test.supported
+ );
+ }
+
+ provider.listener = null;
+ provider = null;
+ run_next_test();
+ },
+ updateDevice() {},
+ removeDevice() {},
+ onSessionRequest() {},
+ };
+
+ provider.listener = listener;
+}
+
+function handleSessionRequest() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+ const testDeviceName = "test-device-name";
+
+ Services.prefs.setCharPref(PREF_DEVICENAME, testDeviceName);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ connect(deviceInfo) {
+ this.request = {
+ deviceInfo,
+ };
+ return {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannel",
+ ]),
+ };
+ },
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return this.version === version;
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ this.device = device;
+ },
+ };
+
+ provider.listener = listener;
+
+ 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.id, testDeviceName);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function handleOnSessionRequest() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {},
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ onSessionRequest(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url,
+ presentationId,
+ };
+ },
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ id: mockDevice.host,
+ address: mockDevice.host,
+ port: 54321,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ };
+ provider
+ .QueryInterface(Ci.nsIPresentationControlServerListener)
+ .onSessionRequest(
+ deviceInfo,
+ testUrl,
+ testPresentationId,
+ testControlChannel
+ );
+
+ Assert.equal(listener.request.deviceId, deviceInfo.id);
+ Assert.equal(listener.request.url, testUrl);
+ Assert.equal(listener.request.presentationId, testPresentationId);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function handleOnSessionRequestFromUnknownDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {},
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {},
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ Assert.ok(false, "shouldn't create any new device");
+ },
+ removeDevice(device) {
+ Assert.ok(false, "shouldn't remote any device");
+ },
+ updateDevice(device) {
+ Assert.ok(false, "shouldn't update any device");
+ },
+ onSessionRequest(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url,
+ presentationId,
+ };
+ },
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ id: "unknown-device.local",
+ address: "unknown-device.local",
+ port: 12345,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ };
+ provider
+ .QueryInterface(Ci.nsIPresentationControlServerListener)
+ .onSessionRequest(
+ deviceInfo,
+ testUrl,
+ testPresentationId,
+ testControlChannel
+ );
+
+ Assert.equal(listener.request.deviceId, deviceInfo.id);
+ Assert.equal(listener.request.url, testUrl);
+ Assert.equal(listener.request.presentationId, testPresentationId);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function noAddDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ Assert.ok(false, "shouldn't perform any device discovery");
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {},
+ };
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ };
+ provider.listener = listener;
+ provider.forceDiscovery();
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function ignoreIncompatibleDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+
+ let deferred = Promise.defer();
+
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(
+ createDevice("", 54321, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return false;
+ },
+ port: 54321,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = new TestPresentationDeviceListener();
+
+ // Register service
+ provider.listener = listener;
+
+ deferred.promise.then(function() {
+ Assert.equal(mockServerObj.id, mockDevice.host);
+
+ // Start discovery
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Assert.equal(listener.count(), 0);
+
+ provider.listener = null;
+ provider = null;
+
+ run_next_test();
+ });
+}
+
+function ignoreSelfDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+
+ let deferred = Promise.defer();
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return this.version === version;
+ },
+ port: 54321,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = new TestPresentationDeviceListener();
+
+ // Register service
+ provider.listener = listener;
+ deferred.promise.then(() => {
+ Assert.equal(mockServerObj.id, mockDevice.host);
+
+ // Start discovery
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Assert.equal(listener.count(), 0);
+
+ provider.listener = null;
+ provider = null;
+
+ run_next_test();
+ });
+}
+
+function addDeviceDynamically() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = new TestPresentationDeviceListener();
+ provider.listener = listener;
+ Assert.equal(listener.count(), 0);
+
+ // Enable discovery
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Assert.equal(listener.count(), 1);
+
+ // Try discovery again
+ provider.forceDiscovery();
+ Assert.equal(listener.count(), 1);
+
+ // Try discovery once more
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ provider.forceDiscovery();
+ Assert.equal(listener.count(), 1);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function updateDevice() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
+ let mockDevice2 = createDevice("A.local", 23456, "N2", SERVICE_TYPE);
+
+ let mockObj = {
+ discovered: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+
+ if (!this.discovered) {
+ listener.onServiceFound(mockDevice1);
+ } else {
+ listener.onServiceFound(mockDevice2);
+ }
+ this.discovered = true;
+
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {
+ listener.onDiscoveryStopped(serviceType);
+ },
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
+ if (serviceInfo.serviceName == "N1") {
+ listener.onServiceResolved(mockDevice1);
+ } else if (serviceInfo.serviceName == "N2") {
+ listener.onServiceResolved(mockDevice2);
+ } else {
+ Assert.ok(false);
+ }
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ addDevice(device) {
+ Assert.ok(!this.isDeviceAdded);
+ Assert.equal(device.id, mockDevice1.host);
+ Assert.equal(device.name, mockDevice1.serviceName);
+ this.isDeviceAdded = true;
+ },
+ removeDevice(device) {
+ Assert.ok(false);
+ },
+ updateDevice(device) {
+ Assert.ok(!this.isDeviceUpdated);
+ Assert.equal(device.id, mockDevice2.host);
+ Assert.equal(device.name, mockDevice2.serviceName);
+ this.isDeviceUpdated = true;
+ },
+
+ isDeviceAdded: false,
+ isDeviceUpdated: false,
+ };
+ Assert.equal(listener.isDeviceAdded, false);
+ Assert.equal(listener.isDeviceUpdated, false);
+
+ // Start discovery
+ provider.listener = listener; // discover: N1
+
+ Assert.equal(listener.isDeviceAdded, true);
+ Assert.equal(listener.isDeviceUpdated, false);
+
+ // temporarily disable to stop discovery and re-enable
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ provider.forceDiscovery(); // discover: N2
+
+ Assert.equal(listener.isDeviceAdded, true);
+ Assert.equal(listener.isDeviceUpdated, true);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function diffDiscovery() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
+ let mockDevice2 = createDevice("B.local", 23456, "N2", SERVICE_TYPE);
+ let mockDevice3 = createDevice("C.local", 45678, "N3", SERVICE_TYPE);
+
+ let mockObj = {
+ discovered: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+
+ if (!this.discovered) {
+ listener.onServiceFound(mockDevice1);
+ listener.onServiceFound(mockDevice2);
+ } else {
+ listener.onServiceFound(mockDevice1);
+ listener.onServiceFound(mockDevice3);
+ }
+ this.discovered = true;
+
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {
+ listener.onDiscoveryStopped(serviceType);
+ },
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
+ if (serviceInfo.serviceName == "N1") {
+ listener.onServiceResolved(mockDevice1);
+ } else if (serviceInfo.serviceName == "N2") {
+ listener.onServiceResolved(mockDevice2);
+ } else if (serviceInfo.serviceName == "N3") {
+ listener.onServiceResolved(mockDevice3);
+ } else {
+ Assert.ok(false);
+ }
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = new TestPresentationDeviceListener();
+ Assert.equal(listener.count(), 0);
+
+ // Start discovery
+ provider.listener = listener; // discover: N1, N2
+ Assert.equal(listener.count(), 2);
+ Assert.equal(listener.devices["A.local"].name, mockDevice1.serviceName);
+ Assert.equal(listener.devices["B.local"].name, mockDevice2.serviceName);
+ Assert.ok(!listener.devices["C.local"]);
+
+ // temporarily disable to stop discovery and re-enable
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ provider.forceDiscovery(); // discover: N1, N3, going to remove: N2
+ Assert.equal(listener.count(), 3);
+ Assert.equal(listener.devices["A.local"].name, mockDevice1.serviceName);
+ Assert.equal(listener.devices["B.local"].name, mockDevice2.serviceName);
+ Assert.equal(listener.devices["C.local"].name, mockDevice3.serviceName);
+
+ // temporarily disable to stop discovery and re-enable
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ provider.forceDiscovery(); // discover: N1, N3, remove: N2
+ Assert.equal(listener.count(), 2);
+ Assert.equal(listener.devices["A.local"].name, mockDevice1.serviceName);
+ Assert.ok(!listener.devices["B.local"]);
+ Assert.equal(listener.devices["C.local"].name, mockDevice3.serviceName);
+
+ provider.listener = null;
+
+ run_next_test();
+}
+
+function serverClosed() {
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+
+ let mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(serviceInfo, listener) {
+ Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+ Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ serviceRegistered: 0,
+ serviceUnregistered: 0,
+ };
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+
+ Assert.equal(mockObj.serviceRegistered, 0);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+
+ // Register
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ this.devices.push(device);
+ },
+ removeDevice(device) {},
+ updateDevice(device) {},
+ devices: [],
+ };
+ Assert.equal(listener.devices.length, 0);
+
+ provider.listener = listener;
+ Assert.equal(mockObj.serviceRegistered, 1);
+ Assert.equal(mockObj.serviceUnregistered, 0);
+ Assert.equal(listener.devices.length, 1);
+
+ let serverListener = provider.QueryInterface(
+ Ci.nsIPresentationControlServerListener
+ );
+ let randomPort = 9527;
+ serverListener.onServerReady(randomPort, "");
+
+ Assert.equal(mockObj.serviceRegistered, 2);
+ Assert.equal(mockObj.serviceUnregistered, 1);
+ Assert.equal(listener.devices.length, 1);
+
+ // Unregister
+ provider.listener = null;
+ Assert.equal(mockObj.serviceRegistered, 2);
+ Assert.equal(mockObj.serviceUnregistered, 2);
+ Assert.equal(listener.devices.length, 1);
+
+ run_next_test();
+}
+
+function serverRetry() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+ let isRetrying = false;
+
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ Assert.ok(isRetrying, "register service after retrying startServer");
+ provider.listener = null;
+ run_next_test();
+ },
+ resolveService(serviceInfo, listener) {},
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer(encrypted, port) {
+ if (!isRetrying) {
+ isRetrying = true;
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerStopped(Cr.NS_ERROR_FAILURE);
+ });
+ } else {
+ this.port = 54321;
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ }
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ onSessionRequest(device, url, presentationId, controlChannel) {},
+ };
+
+ provider.listener = listener;
+}
+
+function run_test() {
+ // Need profile dir to store the key / cert
+ do_get_profile();
+ // Ensure PSM is initialized
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+ new ContractHook(INFO_CONTRACT_ID, MockDNSServiceInfo);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_DISCOVERY);
+ Services.prefs.clearUserPref(PREF_DISCOVERABLE);
+ });
+
+ add_test(registerService);
+ add_test(noRegisterService);
+ add_test(registerServiceDynamically);
+ add_test(addDevice);
+ add_test(filterDevice);
+ add_test(handleSessionRequest);
+ add_test(handleOnSessionRequest);
+ add_test(handleOnSessionRequestFromUnknownDevice);
+ add_test(noAddDevice);
+ add_test(ignoreIncompatibleDevice);
+ add_test(ignoreSelfDevice);
+ add_test(addDeviceDynamically);
+ add_test(updateDevice);
+ add_test(diffDiscovery);
+ add_test(serverClosed);
+ add_test(serverRetry);
+
+ run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
new file mode 100644
index 0000000000..e49a794ebc
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -0,0 +1,288 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const manager = Cc["@mozilla.org/presentation-device/manager;1"].getService(
+ Ci.nsIPresentationDeviceManager
+);
+
+function TestPresentationDevice() {}
+
+function TestPresentationControlChannel() {}
+
+TestPresentationControlChannel.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ sendOffer(offer) {},
+ sendAnswer(answer) {},
+ disconnect() {},
+ launch() {},
+ terminate() {},
+ reconnect() {},
+ set listener(listener) {},
+ get listener() {},
+};
+
+var testProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDeviceProvider"]),
+
+ forceDiscovery() {},
+ set listener(listener) {},
+ get listener() {},
+};
+
+const forbiddenRequestedUrl = "http://example.com";
+var testDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return forbiddenRequestedUrl !== requestedUrl;
+ },
+};
+
+function addProvider() {
+ Object.defineProperty(testProvider, "listener", {
+ configurable: true,
+ set(listener) {
+ Assert.strictEqual(
+ listener,
+ manager,
+ "listener setter is invoked by PresentationDeviceManager"
+ );
+ delete testProvider.listener;
+ run_next_test();
+ },
+ });
+ manager.addDeviceProvider(testProvider);
+}
+
+function forceDiscovery() {
+ testProvider.forceDiscovery = function() {
+ testProvider.forceDiscovery = function() {};
+ Assert.ok(true, "forceDiscovery is invoked by PresentationDeviceManager");
+ run_next_test();
+ };
+ manager.forceDiscovery();
+}
+
+function addDevice() {
+ 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");
+ Assert.equal(data, "add", "expected update type");
+
+ Assert.ok(manager.deviceAvailable, "device is available");
+
+ let devices = manager.getAvailableDevices();
+ Assert.equal(devices.length, 1, "expect 1 available device");
+
+ let device = devices.queryElementAt(0, Ci.nsIPresentationDevice);
+ Assert.equal(device.id, testDevice.id, "expected device id");
+ Assert.equal(device.name, testDevice.name, "expected device name");
+ Assert.equal(device.type, testDevice.type, "expected device type");
+
+ run_next_test();
+ }, "presentation-device-change");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .addDevice(testDevice);
+}
+
+function updateDevice() {
+ 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");
+ Assert.equal(data, "update", "expected update type");
+
+ Assert.ok(manager.deviceAvailable, "device is available");
+
+ let devices = manager.getAvailableDevices();
+ Assert.equal(devices.length, 1, "expect 1 available device");
+
+ let device = devices.queryElementAt(0, Ci.nsIPresentationDevice);
+ Assert.equal(device.id, testDevice.id, "expected device id");
+ Assert.equal(
+ device.name,
+ testDevice.name,
+ "expected name after device update"
+ );
+ Assert.equal(device.type, testDevice.type, "expected device type");
+
+ run_next_test();
+ }, "presentation-device-change");
+ testDevice.name = "updated-name";
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .updateDevice(testDevice);
+}
+
+function filterDevice() {
+ let presentationUrls = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ let url = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ url.data = forbiddenRequestedUrl;
+ presentationUrls.appendElement(url);
+ let devices = manager.getAvailableDevices(presentationUrls);
+ Assert.equal(devices.length, 0, "expect 0 available device for example.com");
+ run_next_test();
+}
+
+function sessionRequest() {
+ let testUrl = "http://www.example.org/";
+ let testPresentationId = "test-presentation-id";
+ let testControlChannel = new TestPresentationControlChannel();
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, topic);
+
+ let request = subject.QueryInterface(Ci.nsIPresentationSessionRequest);
+
+ Assert.equal(request.device.id, testDevice.id, "expected device");
+ Assert.equal(request.url, testUrl, "expected requesting URL");
+ Assert.equal(
+ request.presentationId,
+ testPresentationId,
+ "expected presentation Id"
+ );
+
+ run_next_test();
+ }, "presentation-session-request");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onSessionRequest(
+ testDevice,
+ testUrl,
+ testPresentationId,
+ testControlChannel
+ );
+}
+
+function terminateRequest() {
+ 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");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onTerminateRequest(
+ testDevice,
+ testPresentationId,
+ testControlChannel,
+ testIsFromReceiver
+ );
+}
+
+function reconnectRequest() {
+ let testUrl = "http://www.example.org/";
+ let testPresentationId = "test-presentation-id";
+ let testControlChannel = new TestPresentationControlChannel();
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, topic);
+
+ let request = subject.QueryInterface(Ci.nsIPresentationSessionRequest);
+
+ Assert.equal(request.device.id, testDevice.id, "expected device");
+ Assert.equal(request.url, testUrl, "expected requesting URL");
+ Assert.equal(
+ request.presentationId,
+ testPresentationId,
+ "expected presentation Id"
+ );
+
+ run_next_test();
+ }, "presentation-reconnect-request");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onReconnectRequest(
+ testDevice,
+ testUrl,
+ testPresentationId,
+ testControlChannel
+ );
+}
+
+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");
+ Assert.equal(data, "remove", "expected update type");
+
+ Assert.ok(!manager.deviceAvailable, "device is not available");
+
+ let devices = manager.getAvailableDevices();
+ Assert.equal(devices.length, 0, "expect 0 available device");
+
+ run_next_test();
+ }, "presentation-device-change");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .removeDevice(testDevice);
+}
+
+function removeProvider() {
+ Object.defineProperty(testProvider, "listener", {
+ configurable: true,
+ set(listener) {
+ Assert.strictEqual(
+ listener,
+ null,
+ "unsetListener is invoked by PresentationDeviceManager"
+ );
+ delete testProvider.listener;
+ run_next_test();
+ },
+ });
+ manager.removeDeviceProvider(testProvider);
+}
+
+add_test(addProvider);
+add_test(forceDiscovery);
+add_test(addDevice);
+add_test(updateDevice);
+add_test(filterDevice);
+add_test(sessionRequest);
+add_test(terminateRequest);
+add_test(reconnectRequest);
+add_test(removeDevice);
+add_test(removeProvider);
diff --git a/dom/presentation/tests/xpcshell/test_presentation_session_transport.js b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
new file mode 100644
index 0000000000..6f5f267c4c
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
@@ -0,0 +1,225 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CC = Components.Constructor;
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+var testServer = null;
+var clientTransport = null;
+var serverTransport = null;
+
+var clientBuilder = null;
+var serverBuilder = null;
+
+const clientMessage = "Client Message";
+const serverMessage = "Server Message";
+
+const address = Cc["@mozilla.org/supports-cstring;1"].createInstance(
+ Ci.nsISupportsCString
+);
+address.data = "127.0.0.1";
+const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+addresses.appendElement(address);
+
+const serverChannelDescription = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+ type: 1,
+ tcpAddress: addresses,
+};
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+const clientCallback = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportCallback",
+ ]),
+ notifyTransportReady() {
+ Assert.ok(true, "Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ Assert.ok(true, "Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData(aData) {
+ Assert.equal(aData, serverMessage, "Client transport receives data.");
+ run_next_test();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportCallback",
+ ]),
+ notifyTransportReady() {
+ Assert.ok(true, "Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ Assert.ok(true, "Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData(aData) {
+ Assert.equal(aData, clientMessage, "Server transport receives data.");
+ run_next_test();
+ },
+};
+
+const clientListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportBuilderListener",
+ ]),
+ onSessionTransport(aTransport) {
+ Assert.ok(true, "Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+
+ if (serverTransport) {
+ run_next_test();
+ }
+ },
+};
+
+const serverListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportBuilderListener",
+ ]),
+ onSessionTransport(aTransport) {
+ Assert.ok(true, "Server Transport is built.");
+ serverTransport = aTransport;
+ serverTransport.callback = serverCallback;
+ serverTransport.enableDataNotification();
+
+ if (clientTransport) {
+ run_next_test();
+ }
+ },
+};
+
+function TestServer() {
+ this.serverSocket = ServerSocket(-1, true, -1);
+ this.serverSocket.asyncListen(this);
+}
+
+TestServer.prototype = {
+ onSocketAccepted(aSocket, aTransport) {
+ print("Test server gets a client connection.");
+ serverBuilder = Cc[
+ "@mozilla.org/presentation/presentationtcpsessiontransport;1"
+ ].createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+ serverBuilder.buildTCPSenderTransport(aTransport, serverListener);
+ },
+ onStopListening(aSocket) {
+ print("Test server stops listening.");
+ },
+ close() {
+ if (this.serverSocket) {
+ this.serverSocket.close();
+ this.serverSocket = null;
+ }
+ },
+};
+
+// Set up the transport connection and ensure |notifyTransportReady| triggered
+// at both sides.
+function setup() {
+ clientBuilder = Cc[
+ "@mozilla.org/presentation/presentationtcpsessiontransport;1"
+ ].createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+ clientBuilder.buildTCPReceiverTransport(
+ serverChannelDescription,
+ clientListener
+ );
+}
+
+// Test |selfAddress| attribute of |nsIPresentationSessionTransport|.
+function selfAddress() {
+ var serverSelfAddress = serverTransport.selfAddress;
+ Assert.equal(
+ serverSelfAddress.address,
+ address.data,
+ "The self address of server transport should be set."
+ );
+ Assert.equal(
+ serverSelfAddress.port,
+ testServer.serverSocket.port,
+ "The port of server transport should be set."
+ );
+
+ var clientSelfAddress = clientTransport.selfAddress;
+ Assert.ok(
+ clientSelfAddress.address,
+ "The self address of client transport should be set."
+ );
+ Assert.ok(
+ clientSelfAddress.port,
+ "The port of client transport should be set."
+ );
+
+ run_next_test();
+}
+
+// Test the client sends a message and then a corresponding notification gets
+// triggered at the server side.
+function clientSendMessage() {
+ clientTransport.send(clientMessage);
+}
+
+// Test the server sends a message an then a corresponding notification gets
+// triggered at the client side.
+function serverSendMessage() {
+ serverTransport.send(serverMessage);
+ // The client enables data notification even after the incoming message has
+ // been sent, and should still be able to consume it.
+ clientTransport.enableDataNotification();
+}
+
+function transportClose() {
+ clientTransport.close(Cr.NS_OK);
+}
+
+function shutdown() {
+ testServer.close();
+ run_next_test();
+}
+
+add_test(setup);
+add_test(selfAddress);
+add_test(clientSendMessage);
+add_test(serverSendMessage);
+add_test(transportClose);
+add_test(shutdown);
+
+function run_test() {
+ testServer = new TestServer();
+ // Get the port of the test server.
+ serverChannelDescription.tcpPort = testServer.serverSocket.port;
+
+ 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
new file mode 100644
index 0000000000..05726ab4b1
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -0,0 +1,376 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ControllerStateMachine } = ChromeUtils.import(
+ "resource://gre/modules/presentation/ControllerStateMachine.jsm"
+);
+const { ReceiverStateMachine } = ChromeUtils.import(
+ "resource://gre/modules/presentation/ReceiverStateMachine.jsm"
+);
+const { State } = ChromeUtils.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) {
+ executeSoon(function() {
+ receiverState.onCommand(command);
+ });
+};
+
+mockReceiverChannel.sendCommand = function(command) {
+ executeSoon(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(presId) {
+ Assert.equal(
+ controllerState.state,
+ State.CONNECTED,
+ "controller in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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(presId) {
+ Assert.equal(
+ controllerState.state,
+ State.CONNECTED,
+ "controller in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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(presId) {
+ Assert.equal(
+ receiverState.state,
+ State.CONNECTED,
+ "receiver in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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);
+ 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(
+ controllerCandidate
+ ) {
+ Assert.equal(
+ controllerCandidate,
+ 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.notifyDisconnected = 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.notifyDisconnected = function(disconnectReason) {
+ Assert.equal(disconnectReason, 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.notifyDisconnected = 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.notifyDisconnected = function(disconnectReason) {
+ Assert.equal(disconnectReason, 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.notifyDisconnected = 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.notifyDisconnected = function(disconnectReason) {
+ Assert.equal(
+ disconnectReason,
+ 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(terminateByController);
+add_test(terminateByReceiver);
+add_test(exchangeSDP);
+add_test(disconnect);
+add_test(receiverDisconnect);
+add_test(abnormalDisconnect);
diff --git a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
new file mode 100644
index 0000000000..ee0b3aa074
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -0,0 +1,523 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var pcs;
+
+// Call |run_next_test| if all functions in |names| are called
+function makeJointSuccess(names) {
+ let funcs = {},
+ successCount = 0;
+ names.forEach(function(name) {
+ funcs[name] = function() {
+ info("got expected: " + name);
+ if (++successCount === names.length) {
+ run_next_test();
+ }
+ };
+ });
+ return funcs;
+}
+
+function TestDescription(aType, aTcpAddress, aTcpPort) {
+ this.type = aType;
+ this.tcpAddress = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ for (let address of aTcpAddress) {
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"].createInstance(
+ Ci.nsISupportsCString
+ );
+ wrapper.data = address;
+ this.tcpAddress.appendElement(wrapper);
+ }
+ this.tcpPort = aTcpPort;
+}
+
+TestDescription.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+};
+
+const CONTROLLER_CONTROL_CHANNEL_PORT = 36777;
+const PRESENTER_CONTROL_CHANNEL_PORT = 36888;
+
+var CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_OK;
+var candidate;
+
+// presenter's presentation channel description
+const OFFER_ADDRESS = "192.168.123.123";
+const OFFER_PORT = 123;
+
+// controller's presentation channel description
+const ANSWER_ADDRESS = "192.168.321.321";
+const ANSWER_PORT = 321;
+
+function loopOfferAnser() {
+ pcs = Cc["@mozilla.org/presentation/control-service;1"].createInstance(
+ Ci.nsIPresentationControlService
+ );
+ pcs.id = "controllerID";
+ pcs.listener = {
+ onServerReady() {
+ testPresentationServer();
+ },
+ };
+
+ // First run with TLS enabled.
+ pcs.startServer(true, PRESENTER_CONTROL_CHANNEL_PORT);
+}
+
+function testPresentationServer() {
+ let yayFuncs = makeJointSuccess([
+ "controllerControlChannelClose",
+ "presenterControlChannelClose",
+ "controllerControlChannelReconnect",
+ "presenterControlChannelReconnect",
+ ]);
+ let presenterControlChannel;
+
+ pcs.listener = {
+ onSessionRequest(deviceInfo, url, presentationId, controlChannel) {
+ presenterControlChannel = controlChannel;
+ Assert.equal(deviceInfo.id, pcs.id, "expected device id");
+ Assert.equal(deviceInfo.address, "127.0.0.1", "expected device address");
+ Assert.equal(url, "http://example.com", "expected url");
+ Assert.equal(
+ presentationId,
+ "testPresentationId",
+ "expected presentation id"
+ );
+
+ presenterControlChannel.listener = {
+ status: "created",
+ onOffer(aOffer) {
+ Assert.equal(
+ this.status,
+ "opened",
+ "1. presenterControlChannel: get offer, send answer"
+ );
+ this.status = "onOffer";
+
+ let offer = aOffer.QueryInterface(
+ Ci.nsIPresentationChannelDescription
+ );
+ Assert.strictEqual(
+ offer.tcpAddress.queryElementAt(0, Ci.nsISupportsCString).data,
+ OFFER_ADDRESS,
+ "expected offer address array"
+ );
+ Assert.equal(offer.tcpPort, OFFER_PORT, "expected offer port");
+ try {
+ let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+ let answer = new TestDescription(
+ tcpType,
+ [ANSWER_ADDRESS],
+ ANSWER_PORT
+ );
+ presenterControlChannel.sendAnswer(answer);
+ } catch (e) {
+ Assert.ok(false, "sending answer fails" + e);
+ }
+ },
+ onAnswer(aAnswer) {
+ Assert.ok(false, "get answer");
+ },
+ onIceCandidate(aCandidate) {
+ Assert.ok(
+ true,
+ "3. presenterControlChannel: 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."
+ );
+ }
+ }
+ presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+ },
+ notifyConnected() {
+ Assert.equal(
+ this.status,
+ "created",
+ "0. presenterControlChannel: opened"
+ );
+ this.status = "opened";
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ this.status,
+ "onOffer",
+ "4. presenterControlChannel: closed"
+ );
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "presenterControlChannel notify closed"
+ );
+ this.status = "closed";
+ yayFuncs.controllerControlChannelClose();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ onReconnectRequest(deviceInfo, url, presentationId, controlChannel) {
+ Assert.equal(url, "http://example.com", "expected url");
+ Assert.equal(
+ presentationId,
+ "testPresentationId",
+ "expected presentation id"
+ );
+ yayFuncs.presenterControlChannelReconnect();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let controllerControlChannel = pcs.connect(presenterDeviceInfo);
+
+ controllerControlChannel.listener = {
+ status: "created",
+ onOffer(offer) {
+ Assert.ok(false, "get offer");
+ },
+ onAnswer(aAnswer) {
+ Assert.equal(
+ this.status,
+ "opened",
+ "2. controllerControlChannel: get answer, send ICE candidate"
+ );
+
+ let answer = aAnswer.QueryInterface(Ci.nsIPresentationChannelDescription);
+ Assert.strictEqual(
+ answer.tcpAddress.queryElementAt(0, Ci.nsISupportsCString).data,
+ ANSWER_ADDRESS,
+ "expected answer address array"
+ );
+ Assert.equal(answer.tcpPort, ANSWER_PORT, "expected answer port");
+ candidate = {
+ candidate: "1 1 UDP 1 127.0.0.1 34567 type host",
+ sdpMid: "helloworld",
+ sdpMLineIndex: 1,
+ };
+ controllerControlChannel.sendIceCandidate(JSON.stringify(candidate));
+ },
+ onIceCandidate(aCandidate) {
+ Assert.ok(false, "get ICE candidate");
+ },
+ notifyConnected() {
+ Assert.equal(
+ this.status,
+ "created",
+ "0. controllerControlChannel: opened, send offer"
+ );
+ controllerControlChannel.launch(
+ "testPresentationId",
+ "http://example.com"
+ );
+ this.status = "opened";
+ try {
+ let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+ let offer = new TestDescription(tcpType, [OFFER_ADDRESS], OFFER_PORT);
+ controllerControlChannel.sendOffer(offer);
+ } catch (e) {
+ Assert.ok(false, "sending offer fails:" + e);
+ }
+ },
+ notifyDisconnected(aReason) {
+ this.status = "closed";
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "4. controllerControlChannel notify closed"
+ );
+ yayFuncs.presenterControlChannelClose();
+
+ let reconnectControllerControlChannel = pcs.connect(presenterDeviceInfo);
+ reconnectControllerControlChannel.listener = {
+ notifyConnected() {
+ reconnectControllerControlChannel.reconnect(
+ "testPresentationId",
+ "http://example.com"
+ );
+ },
+ notifyReconnected() {
+ yayFuncs.controllerControlChannelReconnect();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function terminateRequest() {
+ let yayFuncs = makeJointSuccess([
+ "controllerControlChannelConnected",
+ "controllerControlChannelDisconnected",
+ "presenterControlChannelDisconnected",
+ "terminatedByController",
+ "terminatedByReceiver",
+ ]);
+ let controllerControlChannel;
+ let terminatePhase = "controller";
+
+ pcs.listener = {
+ onTerminateRequest(
+ 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() {
+ Assert.ok(true, "control channel notify connected");
+ yayFuncs.controllerControlChannelConnected();
+
+ terminatePhase = "receiver";
+ controllerControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "controllerControlChannel notify disconncted"
+ );
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "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: ChromeUtils.generateQI([
+ "nsITCPPresentationServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected() {
+ presenterControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "4. presenterControlChannel notify disconnected"
+ );
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function terminateRequestAbnormal() {
+ let yayFuncs = makeJointSuccess([
+ "controllerControlChannelConnected",
+ "controllerControlChannelDisconnected",
+ "presenterControlChannelDisconnected",
+ ]);
+ let controllerControlChannel;
+
+ pcs.listener = {
+ onTerminateRequest(
+ 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() {
+ Assert.ok(true, "control channel notify connected");
+ yayFuncs.controllerControlChannelConnected();
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ Cr.NS_ERROR_FAILURE,
+ "controllerControlChannel notify disconncted with error"
+ );
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsITCPPresentationServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected() {
+ presenterControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ Cr.NS_ERROR_FAILURE,
+ "4. presenterControlChannel notify disconnected with error"
+ );
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function setOffline() {
+ pcs.listener = {
+ onServerReady(aPort, aCertFingerprint) {
+ Assert.notEqual(
+ aPort,
+ 0,
+ "TCPPresentationServer port changed and the port should be valid"
+ );
+ pcs.close();
+ run_next_test();
+ },
+ };
+
+ // Let the server socket restart automatically.
+ Services.io.offline = true;
+ Services.io.offline = false;
+}
+
+function oneMoreLoop() {
+ try {
+ pcs.listener = {
+ onServerReady() {
+ testPresentationServer();
+ },
+ };
+
+ // Second run with TLS disabled.
+ pcs.startServer(false, PRESENTER_CONTROL_CHANNEL_PORT);
+ } catch (e) {
+ Assert.ok(false, "TCP presentation init fail:" + e);
+ run_next_test();
+ }
+}
+
+function shutdown() {
+ pcs.listener = {
+ onServerReady(aPort, aCertFingerprint) {
+ Assert.ok(false, "TCPPresentationServer port changed");
+ },
+ };
+ pcs.close();
+ Assert.equal(pcs.port, 0, "TCPPresentationServer closed");
+ run_next_test();
+}
+
+// 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() {
+ // Need profile dir to store the key / cert
+ do_get_profile();
+ // Ensure PSM is initialized
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+ Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.presentation.tcp_server.debug");
+ });
+
+ run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/xpcshell.ini b/dom/presentation/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..635cabe5cd
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+
+[test_multicast_dns_device_provider.js]
+[test_presentation_device_manager.js]
+[test_presentation_session_transport.js]
+[test_tcp_control_channel.js]
+skip-if = os == "android" || tsan || socketprocess_networking # Bugs 1422582, 1580136, 1450502, 1584360, 1606813
+[test_presentation_state_machine.js]