summaryrefslogtreecommitdiffstats
path: root/dom/presentation
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/presentation
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/presentation')
-rw-r--r--dom/presentation/AvailabilityCollection.cpp99
-rw-r--r--dom/presentation/AvailabilityCollection.h45
-rw-r--r--dom/presentation/ControllerConnectionCollection.cpp116
-rw-r--r--dom/presentation/ControllerConnectionCollection.h49
-rw-r--r--dom/presentation/DCPresentationChannelDescription.cpp46
-rw-r--r--dom/presentation/DCPresentationChannelDescription.h37
-rw-r--r--dom/presentation/Presentation.cpp182
-rw-r--r--dom/presentation/Presentation.h70
-rw-r--r--dom/presentation/PresentationAvailability.cpp206
-rw-r--r--dom/presentation/PresentationAvailability.h74
-rw-r--r--dom/presentation/PresentationCallbacks.cpp282
-rw-r--r--dom/presentation/PresentationCallbacks.h85
-rw-r--r--dom/presentation/PresentationConnection.cpp763
-rw-r--r--dom/presentation/PresentationConnection.h128
-rw-r--r--dom/presentation/PresentationConnectionList.cpp125
-rw-r--r--dom/presentation/PresentationConnectionList.h57
-rw-r--r--dom/presentation/PresentationDataChannelSessionTransport.js384
-rw-r--r--dom/presentation/PresentationDataChannelSessionTransport.manifest6
-rw-r--r--dom/presentation/PresentationDeviceInfoManager.js119
-rw-r--r--dom/presentation/PresentationDeviceInfoManager.jsm104
-rw-r--r--dom/presentation/PresentationDeviceInfoManager.manifest3
-rw-r--r--dom/presentation/PresentationDeviceManager.cpp336
-rw-r--r--dom/presentation/PresentationDeviceManager.h54
-rw-r--r--dom/presentation/PresentationLog.h26
-rw-r--r--dom/presentation/PresentationNetworkHelper.js28
-rw-r--r--dom/presentation/PresentationNetworkHelper.manifest3
-rw-r--r--dom/presentation/PresentationReceiver.cpp179
-rw-r--r--dom/presentation/PresentationReceiver.h71
-rw-r--r--dom/presentation/PresentationRequest.cpp563
-rw-r--r--dom/presentation/PresentationRequest.h84
-rw-r--r--dom/presentation/PresentationService.cpp1188
-rw-r--r--dom/presentation/PresentationService.h68
-rw-r--r--dom/presentation/PresentationServiceBase.h401
-rw-r--r--dom/presentation/PresentationSessionInfo.cpp1664
-rw-r--r--dom/presentation/PresentationSessionInfo.h304
-rw-r--r--dom/presentation/PresentationSessionRequest.cpp72
-rw-r--r--dom/presentation/PresentationSessionRequest.h41
-rw-r--r--dom/presentation/PresentationTCPSessionTransport.cpp589
-rw-r--r--dom/presentation/PresentationTCPSessionTransport.h110
-rw-r--r--dom/presentation/PresentationTerminateRequest.cpp73
-rw-r--r--dom/presentation/PresentationTerminateRequest.h41
-rw-r--r--dom/presentation/PresentationTransportBuilderConstructor.cpp85
-rw-r--r--dom/presentation/PresentationTransportBuilderConstructor.h48
-rw-r--r--dom/presentation/interfaces/moz.build30
-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.idl58
-rw-r--r--dom/presentation/interfaces/nsIPresentationDeviceProvider.idl75
-rw-r--r--dom/presentation/interfaces/nsIPresentationListener.idl50
-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.idl275
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionRequest.idl35
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionTransport.idl69
-rw-r--r--dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl80
-rw-r--r--dom/presentation/interfaces/nsIPresentationTerminateRequest.idl33
-rw-r--r--dom/presentation/ipc/PPresentation.ipdl112
-rw-r--r--dom/presentation/ipc/PPresentationBuilder.ipdl34
-rw-r--r--dom/presentation/ipc/PPresentationRequest.ipdl22
-rw-r--r--dom/presentation/ipc/PresentationBuilderChild.cpp184
-rw-r--r--dom/presentation/ipc/PresentationBuilderChild.h48
-rw-r--r--dom/presentation/ipc/PresentationBuilderParent.cpp267
-rw-r--r--dom/presentation/ipc/PresentationBuilderParent.h52
-rw-r--r--dom/presentation/ipc/PresentationChild.cpp198
-rw-r--r--dom/presentation/ipc/PresentationChild.h101
-rw-r--r--dom/presentation/ipc/PresentationContentSessionInfo.cpp109
-rw-r--r--dom/presentation/ipc/PresentationContentSessionInfo.h62
-rw-r--r--dom/presentation/ipc/PresentationIPCService.cpp538
-rw-r--r--dom/presentation/ipc/PresentationIPCService.h75
-rw-r--r--dom/presentation/ipc/PresentationParent.cpp553
-rw-r--r--dom/presentation/ipc/PresentationParent.h137
-rw-r--r--dom/presentation/moz.build89
-rw-r--r--dom/presentation/provider/AndroidCastDeviceProvider.js461
-rw-r--r--dom/presentation/provider/AndroidCastDeviceProvider.manifest4
-rw-r--r--dom/presentation/provider/BuiltinProviders.manifest2
-rw-r--r--dom/presentation/provider/ControllerStateMachine.jsm240
-rw-r--r--dom/presentation/provider/DeviceProviderHelpers.cpp57
-rw-r--r--dom/presentation/provider/DeviceProviderHelpers.h30
-rw-r--r--dom/presentation/provider/DisplayDeviceProvider.cpp580
-rw-r--r--dom/presentation/provider/DisplayDeviceProvider.h136
-rw-r--r--dom/presentation/provider/LegacyMDNSDeviceProvider.cpp774
-rw-r--r--dom/presentation/provider/LegacyMDNSDeviceProvider.h191
-rw-r--r--dom/presentation/provider/LegacyPresentationControlService.js488
-rw-r--r--dom/presentation/provider/LegacyProviders.manifest2
-rw-r--r--dom/presentation/provider/MulticastDNSDeviceProvider.cpp1249
-rw-r--r--dom/presentation/provider/MulticastDNSDeviceProvider.h225
-rw-r--r--dom/presentation/provider/PresentationControlService.js961
-rw-r--r--dom/presentation/provider/PresentationDeviceProviderModule.cpp90
-rw-r--r--dom/presentation/provider/ReceiverStateMachine.jsm238
-rw-r--r--dom/presentation/provider/StateMachineHelper.jsm39
-rw-r--r--dom/presentation/provider/moz.build40
-rw-r--r--dom/presentation/provider/nsTCPDeviceInfo.h77
-rw-r--r--dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js150
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript.js470
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js366
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionFrameScript.js258
-rw-r--r--dom/presentation/tests/mochitest/chrome.ini14
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html220
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html95
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html159
-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.html140
-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.html102
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html114
-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.ini77
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js175
-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.js370
-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.html236
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html245
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver.html141
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html213
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_sender.html291
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_device_info.html144
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_device_info_permission.html35
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html81
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js77
-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.html379
-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.html137
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html110
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html81
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js88
-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.html178
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender.html260
-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.js243
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js197
-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.js1318
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_device_manager.js244
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_session_transport.js198
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_state_machine.js236
-rw-r--r--dom/presentation/tests/xpcshell/test_tcp_control_channel.js398
-rw-r--r--dom/presentation/tests/xpcshell/xpcshell.ini9
160 files changed, 28822 insertions, 0 deletions
diff --git a/dom/presentation/AvailabilityCollection.cpp b/dom/presentation/AvailabilityCollection.cpp
new file mode 100644
index 000000000..73752c750
--- /dev/null
+++ b/dom/presentation/AvailabilityCollection.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..d2faae4c2
--- /dev/null
+++ b/dom/presentation/AvailabilityCollection.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..7d3ffe684
--- /dev/null
+++ b/dom/presentation/ControllerConnectionCollection.cpp
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..c5300fe30
--- /dev/null
+++ b/dom/presentation/ControllerConnectionCollection.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..a904dfe3f
--- /dev/null
+++ b/dom/presentation/DCPresentationChannelDescription.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..63a058f9a
--- /dev/null
+++ b/dom/presentation/DCPresentationChannelDescription.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/Presentation.cpp b/dom/presentation/Presentation.cpp
new file mode 100644
index 000000000..07ca12f26
--- /dev/null
+++ b/dom/presentation/Presentation.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/dom/PresentationBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocShell.h"
+#include "nsIPresentationService.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()
+{
+}
+
+/* virtual */ JSObject*
+Presentation::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto)
+{
+ return PresentationBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+Presentation::SetDefaultRequest(PresentationRequest* aRequest)
+{
+ nsCOMPtr<nsIDocument> 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
+{
+ RefPtr<PresentationRequest> request = mDefaultRequest;
+ return request.forget();
+}
+
+already_AddRefed<PresentationReceiver>
+Presentation::GetReceiver()
+{
+ // 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 (!Preferences::GetBool("dom.presentation.testing.simulate-receiver") &&
+ !docShell->GetIsInMozBrowserOrApp() &&
+ !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;
+ }
+
+ nsCOMPtr<nsIURI> docURI = mWindow->GetDocumentURI();
+ return NS_SUCCEEDED(securityManager->CheckSameOriginURI(presentationURI,
+ docURI,
+ false));
+}
+
+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 000000000..08d0003b3
--- /dev/null
+++ b/dom/presentation/Presentation.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 000000000..93f27dfbf
--- /dev/null
+++ b/dom/presentation/PresentationAvailability.cpp
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/Unused.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationDeviceManager.h"
+#include "nsIPresentationService.h"
+#include "nsServiceManagerUtils.h"
+#include "PresentationLog.h"
+
+using namespace mozilla;
+using namespace mozilla::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_END
+
+NS_IMPL_ADDREF_INHERITED(PresentationAvailability, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationAvailability, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(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)
+{
+ 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 PresentationAvailabilityBinding::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
+{
+ 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>(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 = Move(mPromises);
+ for (auto& promise : promises) {
+ promise->MaybeResolve(this);
+ }
+ // more promises may have been added to mPromises, at least in theory
+ } while (!mPromises.IsEmpty());
+
+ return;
+ }
+
+ if (isChanged) {
+ Unused <<
+ NS_WARN_IF(NS_FAILED(DispatchTrustedEvent(NS_LITERAL_STRING("change"))));
+ }
+}
diff --git a/dom/presentation/PresentationAvailability.h b/dom/presentation/PresentationAvailability.h
new file mode 100644
index 000000000..edfae2c02
--- /dev/null
+++ b/dom/presentation/PresentationAvailability.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "nsIPresentationListener.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+
+class PresentationAvailability final : public DOMEventTargetHelper
+ , public nsIPresentationAvailabilityListener
+ , public SupportsWeakPtr<PresentationAvailability>
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PresentationAvailability,
+ DOMEventTargetHelper)
+ NS_DECL_NSIPRESENTATIONAVAILABILITYLISTENER
+ MOZ_DECLARE_WEAKREFERENCE_TYPENAME(PresentationAvailability)
+
+ 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 000000000..fd0ffee31
--- /dev/null
+++ b/dom/presentation/PresentationCallbacks.cpp
@@ -0,0 +1,282 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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"
+
+using namespace mozilla;
+using namespace mozilla::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()
+{
+}
+
+// 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
+ */
+
+NS_IMPL_ISUPPORTS_INHERITED0(PresentationReconnectCallback,
+ PresentationRequesterCallback)
+
+PresentationReconnectCallback::PresentationReconnectCallback(
+ PresentationRequest* aRequest,
+ const nsAString& aSessionId,
+ Promise* aPromise,
+ PresentationConnection* aConnection)
+ : PresentationRequesterCallback(aRequest, aSessionId, aPromise)
+ , mConnection(aConnection)
+{
+}
+
+PresentationReconnectCallback::~PresentationReconnectCallback()
+{
+}
+
+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([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;
+ }
+
+ uint32_t busyFlags = nsIDocShell::BUSY_FLAGS_NONE;
+ nsresult rv = aDocShell->GetBusyFlags(&busyFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ 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 state)
+{
+ // Do nothing.
+ return NS_OK;
+}
diff --git a/dom/presentation/PresentationCallbacks.h b/dom/presentation/PresentationCallbacks.h
new file mode 100644
index 000000000..e493b0510
--- /dev/null
+++ b/dom/presentation/PresentationCallbacks.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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_DECL_ISUPPORTS_INHERITED
+ 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 000000000..e9c4a71ca
--- /dev/null
+++ b/dom/presentation/PresentationConnection.cpp
@@ -0,0 +1,763 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/ErrorNames.h"
+#include "mozilla/DebugOnly.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringStream.h"
+#include "PresentationConnectionList.h"
+#include "PresentationLog.h"
+
+using namespace mozilla;
+using namespace mozilla::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_INHERITED(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()
+{
+}
+
+/* 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 PresentationConnectionBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+PresentationConnection::GetId(nsAString& aId) const
+{
+ aId = mId;
+}
+
+void
+PresentationConnection::GetUrl(nsAString& aUrl) const
+{
+ aUrl = mUrl;
+}
+
+PresentationConnectionState
+PresentationConnection::State() const
+{
+ return mState;
+}
+
+PresentationConnectionBinaryType
+PresentationConnection::BinaryType() const
+{
+ return mBinaryType;
+}
+
+void
+PresentationConnection::SetBinaryType(PresentationConnectionBinaryType aType)
+{
+ mBinaryType = aType;
+}
+
+void
+PresentationConnection::Send(const nsAString& aData,
+ ErrorResult& aRv)
+{
+ // 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(
+ NS_LITERAL_STRING("Unable to send message due to an internal error."));
+ 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(
+ NS_LITERAL_STRING("Unable to send message: \"") + data +
+ NS_LITERAL_STRING("\""));
+ }
+}
+
+void
+PresentationConnection::Send(Blob& aData,
+ ErrorResult& aRv)
+{
+ 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(
+ NS_LITERAL_STRING("Unable to send message due to an internal error."));
+ return;
+ }
+
+ nsresult rv = service->SendSessionBlob(mId, mRole, &aData);
+ if(NS_WARN_IF(NS_FAILED(rv))) {
+ AsyncCloseConnectionWithErrorMsg(
+ NS_LITERAL_STRING("Unable to send binary message for Blob message."));
+ }
+}
+
+void
+PresentationConnection::Send(const ArrayBuffer& aData,
+ ErrorResult& aRv)
+{
+ 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(
+ NS_LITERAL_STRING("Unable to send message due to an internal error."));
+ return;
+ }
+
+ aData.ComputeLengthAndData();
+
+ 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(
+ NS_LITERAL_STRING("Unable to send binary message for ArrayBuffer message."));
+ }
+}
+
+void
+PresentationConnection::Send(const ArrayBufferView& aData,
+ ErrorResult& aRv)
+{
+ 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(
+ NS_LITERAL_STRING("Unable to send message due to an internal error."));
+ return;
+ }
+
+ aData.ComputeLengthAndData();
+
+ 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(
+ NS_LITERAL_STRING("Unable to send binary message for ArrayBufferView message."));
+ }
+}
+
+void
+PresentationConnection::Close(ErrorResult& aRv)
+{
+ // 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)
+{
+ // 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[%x], reason[%x], role[%d]\n",
+ NS_ConvertUTF16toUTF8(aSessionId).get(), aState,
+ 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: {
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, NS_LITERAL_STRING("connect"), false);
+ 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))) {
+ mozilla::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: {
+ // Ensure onterminate event is fired.
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
+ 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(
+ NS_LITERAL_STRING("Unable to receive a message."));
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+PresentationConnection::DoReceiveMessage(const nsACString& aData, bool aIsBinary)
+{
+ // 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(GetOwner(), aData, EmptyString());
+ MOZ_ASSERT(blob);
+
+ 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 {
+ NS_RUNTIMEABORT("Unknown binary type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ } 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 (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,
+ NS_LITERAL_STRING("close"),
+ init);
+ closedEvent->SetTrusted(true);
+
+ if (aDispatchNow) {
+ bool ignore;
+ return DOMEventTargetHelper::DispatchEvent(closedEvent, &ignore);
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, static_cast<Event*>(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,
+ NS_LITERAL_STRING("message"),
+ false, false, aData, origin,
+ EmptyString(), nullptr,
+ Sequence<OwningNonNull<MessagePort>>());
+ messageEvent->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, static_cast<Event*>(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(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<nsIDocument> 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;
+}
+
+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([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)));
+}
diff --git a/dom/presentation/PresentationConnection.h b/dom/presentation/PresentationConnection.h
new file mode 100644
index 000000000..cecf6c346
--- /dev/null
+++ b/dom/presentation/PresentationConnection.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "nsWeakReference.h"
+
+namespace mozilla {
+namespace dom {
+
+class Blob;
+class PresentationConnectionList;
+
+class PresentationConnection final : public DOMEventTargetHelper
+ , public nsIPresentationSessionListener
+ , public nsIRequest
+ , public SupportsWeakPtr<PresentationConnection>
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PresentationConnection,
+ DOMEventTargetHelper)
+ NS_DECL_NSIPRESENTATIONSESSIONLISTENER
+ NS_DECL_NSIREQUEST
+ MOZ_DECLARE_WEAKREFERENCE_TYPENAME(PresentationConnection)
+
+ 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 000000000..0e0e7696c
--- /dev/null
+++ b/dom/presentation/PresentationConnectionList.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "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_INHERITED(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 PresentationConnectionListBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+PresentationConnectionList::GetConnections(
+ nsTArray<RefPtr<PresentationConnection>>& aConnections) const
+{
+ aConnections = mConnections;
+}
+
+nsresult
+PresentationConnectionList::DispatchConnectionAvailableEvent(
+ PresentationConnection* aConnection)
+{
+ PresentationConnectionAvailableEventInit init;
+ init.mConnection = aConnection;
+
+ RefPtr<PresentationConnectionAvailableEvent> event =
+ PresentationConnectionAvailableEvent::Constructor(
+ this,
+ NS_LITERAL_STRING("connectionavailable"),
+ 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 == nsAutoString(aId)) {
+ return i;
+ }
+ }
+
+ return mConnections.NoIndex;
+}
+
+void
+PresentationConnectionList::NotifyStateChange(const nsAString& aSessionId,
+ PresentationConnection* aConnection)
+{
+ if (!aConnection) {
+ MOZ_ASSERT(false, "PresentationConnection can not be null.");
+ return;
+ }
+
+ bool connectionFound =
+ FindConnectionById(aSessionId) != mConnections.NoIndex ? true : false;
+
+ PresentationConnectionListBinding::ClearCachedConnectionsValue(this);
+ switch (aConnection->State()) {
+ case PresentationConnectionState::Connected:
+ if (!connectionFound) {
+ mConnections.AppendElement(aConnection);
+ if (mGetConnectionListPromise) {
+ 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 000000000..b430219ce
--- /dev/null
+++ b/dom/presentation/PresentationConnectionList.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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.js b/dom/presentation/PresentationDataChannelSessionTransport.js
new file mode 100644
index 000000000..461e4f2cb
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.js
@@ -0,0 +1,384 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// 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: XPCOMUtils.generateQI([Ci.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: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilder,
+ Ci.nsIPresentationDataChannelSessionTransportBuilder,
+ Ci.nsITimerCallback]),
+
+ buildDataChannelTransport: function(aRole, aWindow, aListener) {
+ if (!aRole || !aWindow || !aListener) {
+ log("buildDataChannelTransport with illegal parameters");
+ throw Cr.NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (this._window) {
+ log("buildDataChannelTransport has started.");
+ throw 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 Cr.NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // TODO bug 1228235 we should have a way to let device providers customize
+ // the time-out duration.
+ let timeout;
+ try {
+ timeout = Services.prefs.getIntPref("presentation.receiver.loading.timeout");
+ } catch (e) {
+ // This happens if the pref doesn't exist, so we have a default value.
+ 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: function() {
+ if (!this._sessionTransport) {
+ this._cleanup(Cr.NS_ERROR_NET_TIMEOUT);
+ }
+ },
+
+ _reportError: function(aError) {
+ log("report Error " + aError.name + ":" + aError.message);
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ },
+
+ _setDataChannel: function() {
+ 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: function(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: function(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: function(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: function(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: function(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: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport]),
+
+ init: function(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 NS_ERROR_NOT_AVAILABLE;
+ },
+
+ get callback() {
+ return this._callback;
+ },
+
+ set callback(aCallback) {
+ this._callback = aCallback;
+ },
+
+ send: function(aData) {
+ log("send " + aData);
+ this._dataChannel.send(aData);
+ },
+
+ sendBinaryMsg: function(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: function(aBlob) {
+ log("sendBlob");
+
+ this._dataChannel.send(aBlob);
+ },
+
+ enableDataNotification: function() {
+ log("enableDataNotification");
+ if (this._enableDataNotification) {
+ return;
+ }
+
+ if (!this._callback) {
+ throw NS_ERROR_NOT_AVAILABLE;
+ }
+
+ this._enableDataNotification = true;
+
+ this._messageQueue.forEach(aData => this._doNotifyData(aData));
+ this._messageQueue = [];
+ },
+
+ close: function(aReason) {
+ this._closeReason = aReason;
+
+ this._dataChannel.close();
+ },
+
+ _cleanup: function() {
+ this._dataChannel = null;
+
+ if (this._peerConnection) {
+ this._peerConnection.close();
+ this._peerConnection = null;
+ }
+ this._callback = null;
+ this._messageQueue = [];
+ this._window = null;
+ },
+
+ _doNotifyData: function(aData) {
+ if (!this._callback) {
+ throw 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);
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationTransportBuilder,
+ PresentationTransport]);
diff --git a/dom/presentation/PresentationDataChannelSessionTransport.manifest b/dom/presentation/PresentationDataChannelSessionTransport.manifest
new file mode 100644
index 000000000..6838f675f
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.manifest
@@ -0,0 +1,6 @@
+# PresentationDataChannelSessionTransport.js
+component {dd2bbf2f-3399-4389-8f5f-d382afb8b2d6} PresentationDataChannelSessionTransport.js
+contract @mozilla.org/presentation/datachanneltransport;1 {dd2bbf2f-3399-4389-8f5f-d382afb8b2d6}
+
+component {215b2f62-46e2-4004-a3d1-6858e56c20f3} PresentationDataChannelSessionTransport.js
+contract @mozilla.org/presentation/datachanneltransportbuilder;1 {215b2f62-46e2-4004-a3d1-6858e56c20f3}
diff --git a/dom/presentation/PresentationDeviceInfoManager.js b/dom/presentation/PresentationDeviceInfoManager.js
new file mode 100644
index 000000000..29e7d370c
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+function log(aMsg) {
+ //dump("-*- PresentationDeviceInfoManager.js : " + aMsg + "\n");
+}
+
+const PRESENTATIONDEVICEINFOMANAGER_CID = Components.ID("{1bd66bef-f643-4be3-b690-0c656353eafd}");
+const PRESENTATIONDEVICEINFOMANAGER_CONTRACTID = "@mozilla.org/presentation-device/deviceInfo;1";
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+function PresentationDeviceInfoManager() {}
+
+PresentationDeviceInfoManager.prototype = {
+ __proto__: DOMRequestIpcHelper.prototype,
+
+ classID: PRESENTATIONDEVICEINFOMANAGER_CID,
+ contractID: PRESENTATIONDEVICEINFOMANAGER_CONTRACTID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+ Ci.nsIObserver,
+ Ci.nsIDOMGlobalPropertyInitializer]),
+
+ receiveMessage: function(aMsg) {
+ if (!aMsg || !aMsg.data) {
+ return;
+ }
+
+ let data = aMsg.data;
+
+ log("receive aMsg: " + aMsg.name);
+ switch (aMsg.name) {
+ case "PresentationDeviceInfoManager:OnDeviceChange": {
+ let detail = {
+ detail: {
+ type: data.type,
+ deviceInfo: data.deviceInfo,
+ }
+ };
+ let event = new this._window.CustomEvent("devicechange", Cu.cloneInto(detail, this._window));
+ this.__DOM_IMPL__.dispatchEvent(event);
+ break;
+ }
+ case "PresentationDeviceInfoManager:GetAll:Result:Ok": {
+ let resolver = this.takePromiseResolver(data.requestId);
+
+ if (!resolver) {
+ return;
+ }
+
+ resolver.resolve(Cu.cloneInto(data.devices, this._window));
+ break;
+ }
+ case "PresentationDeviceInfoManager:GetAll:Result:Error": {
+ let resolver = this.takePromiseResolver(data.requestId);
+
+ if (!resolver) {
+ return;
+ }
+
+ resolver.reject(data.error);
+ break;
+ }
+ }
+ },
+
+ init: function(aWin) {
+ log("init");
+ this.initDOMRequestHelper(aWin, [
+ {name: "PresentationDeviceInfoManager:OnDeviceChange", weakRef: true},
+ {name: "PresentationDeviceInfoManager:GetAll:Result:Ok", weakRef: true},
+ {name: "PresentationDeviceInfoManager:GetAll:Result:Error", weakRef: true},
+ ]);
+ },
+
+ uninit: function() {
+ log("uninit");
+ let self = this;
+
+ this.forEachPromiseResolver(function(aKey) {
+ self.takePromiseResolver(aKey).reject("PresentationDeviceInfoManager got destroyed");
+ });
+ },
+
+ get ondevicechange() {
+ return this.__DOM_IMPL__.getEventHandler("ondevicechange");
+ },
+
+ set ondevicechange(aHandler) {
+ this.__DOM_IMPL__.setEventHandler("ondevicechange", aHandler);
+ },
+
+ getAll: function() {
+ log("getAll");
+ let self = this;
+ return this.createPromiseWithId(function(aResolverId) {
+ cpmm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll", {
+ requestId: aResolverId,
+ });
+ });
+ },
+
+ forceDiscovery: function() {
+ cpmm.sendAsyncMessage("PresentationDeviceInfoManager:ForceDiscovery");
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationDeviceInfoManager]);
diff --git a/dom/presentation/PresentationDeviceInfoManager.jsm b/dom/presentation/PresentationDeviceInfoManager.jsm
new file mode 100644
index 000000000..205982b9c
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.jsm
@@ -0,0 +1,104 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = ["PresentationDeviceInfoService"];
+
+function log(aMsg) {
+ //dump("PresentationDeviceInfoManager.jsm: " + aMsg + "\n");
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "presentationDeviceManager",
+ "@mozilla.org/presentation-device/manager;1",
+ "nsIPresentationDeviceManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+
+this.PresentationDeviceInfoService = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
+ Ci.nsIObserver]),
+
+ init: function() {
+ log("init");
+ ppmm.addMessageListener("PresentationDeviceInfoManager:GetAll", this);
+ ppmm.addMessageListener("PresentationDeviceInfoManager:ForceDiscovery", this);
+ Services.obs.addObserver(this, "presentation-device-change", false);
+ },
+
+ getAll: function(aData, aMm) {
+ log("getAll");
+ let deviceArray = presentationDeviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray);
+ if (!deviceArray) {
+ aData.error = "DataError";
+ aMm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll:Result:Error", aData);
+ return;
+ }
+
+ aData.devices = [];
+ for (let i = 0; i < deviceArray.length; i++) {
+ let device = deviceArray.queryElementAt(i, Ci.nsIPresentationDevice);
+ aData.devices.push({
+ id: device.id,
+ name: device.name,
+ type: device.type,
+ });
+ }
+ aMm.sendAsyncMessage("PresentationDeviceInfoManager:GetAll:Result:Ok", aData);
+ },
+
+ forceDiscovery: function() {
+ log("forceDiscovery");
+ presentationDeviceManager.forceDiscovery();
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ log("observe: " + aTopic);
+
+ let device = aSubject.QueryInterface(Ci.nsIPresentationDevice);
+ let data = {
+ type: aData,
+ deviceInfo: {
+ id: device.id,
+ name: device.name,
+ type: device.type,
+ },
+ };
+ ppmm.broadcastAsyncMessage("PresentationDeviceInfoManager:OnDeviceChange", data);
+ },
+
+ receiveMessage: function(aMessage) {
+ if (!aMessage.target.assertPermission("presentation-device-manage")) {
+ debug("receive message " + aMessage.name +
+ " from a content process with no 'presentation-device-manage' privileges.");
+ return null;
+ }
+
+ let msg = aMessage.data || {};
+ let mm = aMessage.target;
+
+ log("receiveMessage: " + aMessage.name);
+ switch (aMessage.name) {
+ case "PresentationDeviceInfoManager:GetAll": {
+ this.getAll(msg, mm);
+ break;
+ }
+ case "PresentationDeviceInfoManager:ForceDiscovery": {
+ this.forceDiscovery();
+ break;
+ }
+ }
+ },
+};
+
+this.PresentationDeviceInfoService.init();
diff --git a/dom/presentation/PresentationDeviceInfoManager.manifest b/dom/presentation/PresentationDeviceInfoManager.manifest
new file mode 100644
index 000000000..ae50b8e6a
--- /dev/null
+++ b/dom/presentation/PresentationDeviceInfoManager.manifest
@@ -0,0 +1,3 @@
+# PresentationDeviceInfoManager.js
+component {1bd66bef-f643-4be3-b690-0c656353eafd} PresentationDeviceInfoManager.js
+contract @mozilla.org/presentation-device/deviceInfo;1 {1bd66bef-f643-4be3-b690-0c656353eafd}
diff --git a/dom/presentation/PresentationDeviceManager.cpp b/dom/presentation/PresentationDeviceManager.cpp
new file mode 100644
index 000000000..7e5a4700c
--- /dev/null
+++ b/dom/presentation/PresentationDeviceManager.cpp
@@ -0,0 +1,336 @@
+/* -*- 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 "nsIMutableArray.h"
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+#include "PresentationSessionRequest.h"
+#include "PresentationTerminateRequest.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationDeviceManager,
+ nsIPresentationDeviceManager,
+ nsIPresentationDeviceListener,
+ nsIObserver,
+ nsISupportsWeakReference)
+
+PresentationDeviceManager::PresentationDeviceManager()
+{
+}
+
+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(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], false);
+ 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], false);
+ 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 000000000..f854ee883
--- /dev/null
+++ b/dom/presentation/PresentationDeviceManager.h
@@ -0,0 +1,54 @@
+/* -*- 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 000000000..96af0c124
--- /dev/null
+++ b/dom/presentation/PresentationLog.h
@@ -0,0 +1,26 @@
+/* -*- 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 {
+namespace dom {
+extern mozilla::LazyLogModule gPresentationLog;
+}
+}
+
+#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.js b/dom/presentation/PresentationNetworkHelper.js
new file mode 100644
index 000000000..9b6458daf
--- /dev/null
+++ b/dom/presentation/PresentationNetworkHelper.js
@@ -0,0 +1,28 @@
+// -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const NETWORKHELPER_CID = Components.ID("{5fb96caa-6d49-4f6b-9a4b-65dd0d51f92d}");
+
+function PresentationNetworkHelper() {}
+
+PresentationNetworkHelper.prototype = {
+ classID: NETWORKHELPER_CID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationNetworkHelper]),
+
+ getWifiIPAddress: function(aListener) {
+ Messaging.sendRequestForResult({type: "Wifi:GetIPAddress"})
+ .then(result => aListener.onGetWifiIPAddress(result),
+ err => aListener.onError(err));
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationNetworkHelper]);
diff --git a/dom/presentation/PresentationNetworkHelper.manifest b/dom/presentation/PresentationNetworkHelper.manifest
new file mode 100644
index 000000000..a061cef08
--- /dev/null
+++ b/dom/presentation/PresentationNetworkHelper.manifest
@@ -0,0 +1,3 @@
+# PresentationNetworkHelper.js
+component {5fb96caa-6d49-4f6b-9a4b-65dd0d51f92d} PresentationNetworkHelper.js
+contract @mozilla.org/presentation-device/networkHelper;1 {5fb96caa-6d49-4f6b-9a4b-65dd0d51f92d}
diff --git a/dom/presentation/PresentationReceiver.cpp b/dom/presentation/PresentationReceiver.cpp
new file mode 100644
index 000000000..bc1776b45
--- /dev/null
+++ b/dom/presentation/PresentationReceiver.cpp
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/dom/PresentationReceiverBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.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[%d]\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 PresentationReceiverBinding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMETHODIMP
+PresentationReceiver::NotifySessionConnect(uint64_t aWindowId,
+ const nsAString& aSessionId)
+{
+ PRES_DEBUG("receiver session connect:id[%s], windowId[%x]\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([self] () -> void {
+ self->CreateConnectionList();
+ }));
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+ }
+
+ RefPtr<Promise> promise = mGetConnectionListPromise;
+ 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 000000000..ee72f587b
--- /dev/null
+++ b/dom/presentation/PresentationReceiver.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/ErrorResult.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPresentationListener.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+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 000000000..221684e53
--- /dev/null
+++ b/dom/presentation/PresentationRequest.cpp
@@ -0,0 +1,563 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "AvailabilityCollection.h"
+#include "ControllerConnectionCollection.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/PresentationRequestBinding.h"
+#include "mozilla/dom/PresentationConnectionAvailableEvent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Move.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsContentSecurityManager.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGlobalWindow.h"
+#include "nsIDocument.h"
+#include "nsIPresentationService.h"
+#include "nsIURI.h"
+#include "nsIUUIDGenerator.h"
+#include "nsNetUtil.h"
+#include "nsSandboxFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "Presentation.h"
+#include "PresentationAvailability.h"
+#include "PresentationCallbacks.h"
+#include "PresentationLog.h"
+#include "PresentationTransportBuilderConstructor.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_ADDREF_INHERITED(PresentationRequest, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(PresentationRequest, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(PresentationRequest)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+static nsresult
+GetAbsoluteURL(const nsAString& aUrl,
+ nsIURI* aBaseUri,
+ nsIDocument* aDocument,
+ nsAString& aAbsoluteUrl)
+{
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri),
+ aUrl,
+ aDocument ? aDocument->GetDocumentCharacterSet().get()
+ : nullptr,
+ aBaseUri);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+
+ aAbsoluteUrl = NS_ConvertUTF8toUTF16(spec);
+
+ return NS_OK;
+}
+
+/* static */ already_AddRefed<PresentationRequest>
+PresentationRequest::Constructor(const GlobalObject& aGlobal,
+ const nsAString& aUrl,
+ ErrorResult& aRv)
+{
+ Sequence<nsString> urls;
+ urls.AppendElement(aUrl, fallible);
+ 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, Move(urls));
+ return NS_WARN_IF(!request->Init()) ? nullptr : request.forget();
+}
+
+PresentationRequest::PresentationRequest(nsPIDOMWindowInner* aWindow,
+ nsTArray<nsString>&& aUrls)
+ : DOMEventTargetHelper(aWindow)
+ , mUrls(Move(aUrls))
+{
+}
+
+PresentationRequest::~PresentationRequest()
+{
+}
+
+bool
+PresentationRequest::Init()
+{
+ return true;
+}
+
+/* virtual */ JSObject*
+PresentationRequest::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto)
+{
+ return PresentationRequestBinding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<Promise>
+PresentationRequest::Start(ErrorResult& aRv)
+{
+ return StartWithDevice(NullString(), 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<nsIDocument> 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 (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 =
+ nsGlobalWindow::Cast(GetOwner())->GetNavigator(aRv);
+ 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(buffer, 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<nsIDOMEventTarget> handler =
+ do_QueryInterface(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<nsIDocument> 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 (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>>(
+ 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<nsIDocument> 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 (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)
+{
+ PresentationConnectionAvailableEventInit init;
+ init.mConnection = aConnection;
+
+ RefPtr<PresentationConnectionAvailableEvent> event =
+ PresentationConnectionAvailableEvent::Constructor(this,
+ NS_LITERAL_STRING("connectionavailable"),
+ 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;
+ }
+
+ ErrorResult rv;
+ RefPtr<Navigator> navigator =
+ nsGlobalWindow::Cast(GetOwner())->GetNavigator(rv);
+ if (!navigator) {
+ return;
+ }
+
+ RefPtr<Presentation> presentation = navigator->GetPresentation(rv);
+
+ if (presentation) {
+ presentation->SetStartSessionUnsettled(false);
+ }
+}
+
+bool
+PresentationRequest::IsProhibitMixedSecurityContexts(nsIDocument* aDocument)
+{
+ MOZ_ASSERT(aDocument);
+
+ if (nsContentUtils::IsChromeDoc(aDocument)) {
+ return true;
+ }
+
+ nsCOMPtr<nsIDocument> doc = aDocument;
+ while (doc && !nsContentUtils::IsChromeDoc(doc)) {
+ if (nsContentUtils::HttpsStateIsModern(doc)) {
+ return true;
+ }
+
+ doc = doc->GetParentDocument();
+ }
+
+ 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;
+ }
+
+ PrincipalOriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateCodebasePrincipal(uri, attrs);
+ if (NS_WARN_IF(!principal)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIContentSecurityManager> csm =
+ do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID);
+ if (NS_WARN_IF(!csm)) {
+ return false;
+ }
+
+ bool isTrustworthyOrigin = false;
+ csm->IsOriginPotentiallyTrustworthy(principal, &isTrustworthyOrigin);
+ return isTrustworthyOrigin;
+}
+
+bool
+PresentationRequest::IsAllURLAuthenticated()
+{
+ for (const auto& url : mUrls) {
+ if (!IsPrioriAuthenticatedURL(url)) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/dom/presentation/PresentationRequest.h b/dom/presentation/PresentationRequest.h
new file mode 100644
index 000000000..ce82f2b44
--- /dev/null
+++ b/dom/presentation/PresentationRequest.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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"
+
+class nsIDocument;
+
+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(nsIDocument* 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 000000000..bc525cdb8
--- /dev/null
+++ b/dom/presentation/PresentationService.cpp
@@ -0,0 +1,1188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 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 "nsGlobalWindow.h"
+#include "nsIMutableArray.h"
+#include "nsIObserverService.h"
+#include "nsIPresentationControlChannel.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, false);
+ 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,
+ nsIDOMEventTarget* 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,
+ nsIDOMEventTarget* aEventTarget,
+ nsIPrincipal* aPrincipal,
+ nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor)
+ : mRequestUrls(aUrls)
+ , 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(nsIDOMEventTarget** aChromeEventHandler)
+{
+ nsCOMPtr<nsIDOMEventTarget> 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()
+{
+}
+
+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;
+ } else 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;
+ }
+
+ return HandleDeviceAdded(device);
+ } else if(!NS_strcmp(aData, u"remove")) {
+ return HandleDeviceRemoved();
+ }
+
+ return NS_OK;
+ } else 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);
+ } else 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);
+ } else 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);
+ } else 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);
+ }
+}
+
+nsresult
+PresentationService::HandleDeviceAdded(nsIPresentationDevice* aDevice)
+{
+ PRES_DEBUG("%s\n", __func__);
+ if (!aDevice) {
+ MOZ_ASSERT(false, "aDevice shoud no be null.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // 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()) {
+ return mAvailabilityManager.DoNotifyAvailableChange(supportedAvailabilityUrl,
+ true);
+ }
+
+ return NS_OK;
+}
+
+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()) {
+ return mAvailabilityManager.DoNotifyAvailableChange(aAvailabilityUrls,
+ false);
+ }
+
+ return mAvailabilityManager.DoNotifyAvailableChange(supportedAvailabilityUrl,
+ true);
+}
+
+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[%d], 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, 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);
+ }
+ nsCOMPtr<nsISupports> 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);
+ }
+ nsCOMPtr<Promise> realPromise = do_QueryInterface(promise);
+ static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(realPromise);
+
+ 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("handle termination:id[%s], receiver[%d]\n", __func__,
+ sessionId.get(), isFromReceiver);
+
+ return info->OnTerminate(ctrlChannel);
+}
+
+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,
+ nsIDOMEventTarget* 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, 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,
+ nsIDOMBlob* 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[%lld]\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, aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationService::UnregisterRespondingListener(uint64_t aWindowId)
+{
+ PRES_DEBUG("%s:windowId[%lld]\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[%lld], 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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(aSessionId).get(), 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([windowId]() -> void {
+ PRES_DEBUG("Attempt to close window[%d]\n", windowId);
+
+ if (auto* window = nsGlobalWindow::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)
+{
+ return UpdateWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+}
+
+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 PresentationService();
+ if (NS_WARN_IF(!static_cast<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 000000000..b2d39e691
--- /dev/null
+++ b/dom/presentation/PresentationService.h
@@ -0,0 +1,68 @@
+/* -*- 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();
+ nsresult 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 000000000..227e95430
--- /dev/null
+++ b/dom/presentation/PresentationServiceBase.h
@@ -0,0 +1,401 @@
+/* -*- 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/. */
+
+#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"
+
+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);
+ }
+
+ ~SessionIdManager()
+ {
+ MOZ_COUNT_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);
+ }
+ }
+ }
+ }
+
+ nsresult UpdateWindowId(const nsAString& aSessionId, const uint64_t aWindowId)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RemoveSessionId(aSessionId);
+ AddSessionId(aWindowId, aSessionId);
+ return NS_OK;
+ }
+
+ 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);
+ }
+
+ ~AvailabilityManager()
+ {
+ MOZ_COUNT_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);
+ }
+ }
+ }
+ }
+
+ nsresult 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)));
+ }
+ return NS_OK;
+ }
+
+ 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);
+ }
+ }
+
+ nsresult 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) {
+ return mControllerSessionIdManager.UpdateWindowId(aSessionId, aWindowId);
+ }
+
+ return 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 000000000..f93909864
--- /dev/null
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -0,0 +1,1664 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/ContentParent.h"
+#include "mozilla/dom/HTMLIFrameElementBinding.h"
+#include "mozilla/dom/TabParent.h"
+#include "mozilla/Function.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Move.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsIDocShell.h"
+#include "nsFrameLoader.h"
+#include "nsIMutableArray.h"
+#include "nsINetAddr.h"
+#include "nsISocketTransport.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "PresentationLog.h"
+#include "PresentationService.h"
+#include "PresentationSessionInfo.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "nsIPresentationNetworkHelper.h"
+#endif // MOZ_WIDGET_ANDROID
+
+#ifdef MOZ_WIDGET_GONK
+#include "nsINetworkInterface.h"
+#include "nsINetworkManager.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::services;
+
+/*
+ * 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>(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() {}
+
+ nsCString mAddress;
+ uint16_t mPort;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+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, false);
+ 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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), 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(nsIDOMBlob* 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;
+}
+
+nsresult
+PresentationSessionInfo::OnTerminate(nsIPresentationControlChannel* aControlChannel)
+{
+ mIsOnTerminating = true; // Mark for terminating transport channel
+ SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED, NS_OK);
+ SetControlChannel(aControlChannel);
+
+ return NS_OK;
+}
+
+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;
+ }
+
+ auto window = nsGlobalWindow::GetInnerWindowWithId(windowId);
+ if (!window) {
+ return nullptr;
+ }
+
+ return window->AsInner();
+}
+
+/* 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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), 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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), 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 defined(MOZ_WIDGET_GONK)
+ nsCOMPtr<nsINetworkManager> networkManager =
+ do_GetService("@mozilla.org/network/manager;1");
+ if (NS_WARN_IF(!networkManager)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsINetworkInfo> activeNetworkInfo;
+ networkManager->GetActiveNetworkInfo(getter_AddRefs(activeNetworkInfo));
+ if (NS_WARN_IF(!activeNetworkInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ char16_t** ips = nullptr;
+ uint32_t* prefixes = nullptr;
+ uint32_t count = 0;
+ activeNetworkInfo->GetAddresses(&ips, &prefixes, &count);
+ if (NS_WARN_IF(!count)) {
+ NS_Free(prefixes);
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, ips);
+ return NS_ERROR_FAILURE;
+ }
+
+ // 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.
+
+ nsAutoString ip;
+ ip.Assign(ips[0]);
+
+ // On Android platform, 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>(
+ this,
+ &PresentationControllingInfo::OnGetAddress,
+ NS_ConvertUTF16toUTF8(ip)));
+
+ NS_Free(prefixes);
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, ips);
+
+#elif 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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), 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[%x]\n",__func__, 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 char** aAddressArray,
+ uint32_t aAddressArraySize)
+{
+ if (!aAddressArraySize) {
+ return OnListNetworkAddressesFailed();
+ }
+
+ // 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.
+
+ nsAutoCString ip;
+ ip.Assign(aAddressArray[0]);
+
+ // 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>(
+ this,
+ &PresentationControllingInfo::OnGetAddress,
+ ip));
+
+ 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>(
+ 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 =
+ mReconnectCallback.forget();
+ 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)
+
+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);
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = mTimer->InitWithCallback(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[%x], role[%d]\n", __func__,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), 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);
+}
+
+// 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;
+ }
+
+ nsCOMPtr<nsIFrameLoaderOwner> owner = do_QueryInterface((nsIFrameLoaderOwner*) frame);
+ if (NS_WARN_IF(!owner)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIFrameLoader> frameLoader = owner->GetFrameLoader();
+ if (NS_WARN_IF(!frameLoader)) {
+ ReplyError(NS_ERROR_DOM_OPERATION_ERR);
+ return;
+ }
+
+ RefPtr<TabParent> tabParent = TabParent::GetFrom(frameLoader);
+ if (tabParent) {
+ // OOP frame
+ // Notify the content process that a receiver page has launched, so it can
+ // start monitoring the loading progress.
+ mContentParent = tabParent->Manager();
+ Unused << NS_WARN_IF(!static_cast<ContentParent*>(mContentParent.get())->SendNotifyPresentationReceiverLaunched(tabParent, mSessionId));
+ } else {
+ // In-process frame
+ nsCOMPtr<nsIDocShell> docShell;
+ rv = frameLoader->GetDocShell(getter_AddRefs(docShell));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ 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);
+}
diff --git a/dom/presentation/PresentationSessionInfo.h b/dom/presentation/PresentationSessionInfo.h
new file mode 100644
index 000000000..6338d3c32
--- /dev/null
+++ b/dom/presentation/PresentationSessionInfo.h
@@ -0,0 +1,304 @@
+/* -*- 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/nsIContentParent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.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(nsIDOMBlob* aBlob);
+
+ nsresult Close(nsresult aReason,
+ uint32_t aState);
+
+ nsresult 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:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPRESENTATIONCONTROLCHANNELLISTENER
+ NS_DECL_NSITIMERCALLBACK
+
+ 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.
+ nsCOMPtr<nsIContentParent> 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 000000000..219fbd6a4
--- /dev/null
+++ b/dom/presentation/PresentationSessionRequest.cpp
@@ -0,0 +1,72 @@
+/* -*- 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()
+{
+}
+
+// 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 000000000..c56502d77
--- /dev/null
+++ b/dom/presentation/PresentationSessionRequest.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_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 000000000..1ccb8b43c
--- /dev/null
+++ b/dom/presentation/PresentationTCPSessionTransport.cpp
@@ -0,0 +1,589 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "nsArrayUtils.h"
+#include "nsIAsyncStreamCopier.h"
+#include "nsIInputStreamPump.h"
+#include "nsIMultiplexInputStream.h"
+#include "nsIMutableArray.h"
+#include "nsIOutputStream.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIScriptableInputStream.h"
+#include "nsISocketTransport.h"
+#include "nsISocketTransportService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "PresentationLog.h"
+#include "PresentationTCPSessionTransport.h"
+
+#define BUFFER_SIZE 65536
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class CopierCallbacks final : public nsIRequestObserver
+{
+public:
+ explicit CopierCallbacks(PresentationTCPSessionTransport* aTransport)
+ : mOwner(aTransport)
+ {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+private:
+ ~CopierCallbacks() {}
+
+ RefPtr<PresentationTCPSessionTransport> mOwner;
+};
+
+NS_IMPL_ISUPPORTS(CopierCallbacks, nsIRequestObserver)
+
+NS_IMETHODIMP
+CopierCallbacks::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CopierCallbacks::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus)
+{
+ mOwner->NotifyCopyComplete(aStatus);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION(PresentationTCPSessionTransport, mTransport,
+ mSocketInputStream, mSocketOutputStream,
+ mInputStreamPump, mInputStreamScriptable,
+ mMultiplexStream, mMultiplexStreamCopier, 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()
+{
+}
+
+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*>(mListener,
+ &nsIPresentationSessionTransportBuilderListener::OnSessionTransport,
+ sessionTransport);
+
+ NS_DispatchToCurrentThread(onSessionTransportRunnable.forget());
+
+ nsCOMPtr<nsIRunnable> setReadyStateRunnable =
+ NewRunnableMethod<ReadyState>(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(nullptr, 0, serverHost, serverPort, nullptr,
+ getter_AddRefs(mTransport));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIThread> mainThread;
+ NS_GetMainThread(getter_AddRefs(mainThread));
+
+ mTransport->SetEventSink(this, mainThread);
+
+ 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*>(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<nsIThread> mainThread;
+ NS_GetMainThread(getter_AddRefs(mainThread));
+
+ rv = asyncStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, mainThread);
+ 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;
+ }
+
+ mMultiplexStream = do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mMultiplexStreamCopier = do_CreateInstance("@mozilla.org/network/async-stream-copier;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!sts)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIEventTarget> target = do_QueryInterface(sts);
+ rv = mMultiplexStreamCopier->Init(mMultiplexStream,
+ mSocketOutputStream,
+ target,
+ true, /* source buffered */
+ false, /* sink buffered */
+ BUFFER_SIZE,
+ false, /* close source */
+ false); /* close sink */
+ 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, -1, -1, 0, 0, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mInputStreamPump->AsyncRead(this, nullptr);
+ 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);
+}
+
+void
+PresentationTCPSessionTransport::EnsureCopying()
+{
+ if (mAsyncCopierActive) {
+ return;
+ }
+
+ mAsyncCopierActive = true;
+ RefPtr<CopierCallbacks> callbacks = new CopierCallbacks(this);
+ Unused << NS_WARN_IF(NS_FAILED(mMultiplexStreamCopier->AsyncCopy(callbacks, nullptr)));
+}
+
+void
+PresentationTCPSessionTransport::NotifyCopyComplete(nsresult aStatus)
+{
+ mAsyncCopierActive = false;
+ mMultiplexStream->RemoveStream(0);
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ if (mReadyState != ReadyState::CLOSED) {
+ mCloseStatus = aStatus;
+ SetReadyState(ReadyState::CLOSED);
+ }
+ return;
+ }
+
+ uint32_t count;
+ nsresult rv = mMultiplexStream->GetCount(&count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (count) {
+ 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;
+ }
+
+ mMultiplexStream->AppendStream(stream);
+
+ EnsureCopying();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::SendBinaryMsg(const nsACString& aData)
+{
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::SendBlob(nsIDOMBlob* aBlob)
+{
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::Close(nsresult aReason)
+{
+ PRES_DEBUG("%s:reason[%x]\n", __func__, aReason);
+
+ if (mReadyState == ReadyState::CLOSED || mReadyState == ReadyState::CLOSING) {
+ return NS_OK;
+ }
+
+ mCloseStatus = aReason;
+ SetReadyState(ReadyState::CLOSING);
+
+ uint32_t count = 0;
+ mMultiplexStream->GetCount(&count);
+ if (!count) {
+ 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[%x]\n", __func__, 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,
+ nsISupports* aContext)
+{
+ // Do nothing.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTCPSessionTransport::OnStopRequest(nsIRequest* aRequest,
+ nsISupports* aContext,
+ nsresult aStatusCode)
+{
+ PRES_DEBUG("%s:aStatusCode[%x]\n", __func__, aStatusCode);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ uint32_t count;
+ nsresult rv = mMultiplexStream->GetCount(&count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInputStreamPump = nullptr;
+
+ if (count != 0 && 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,
+ nsISupports* aContext,
+ 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);
+}
diff --git a/dom/presentation/PresentationTCPSessionTransport.h b/dom/presentation/PresentationTCPSessionTransport.h
new file mode 100644
index 000000000..f36b371a4
--- /dev/null
+++ b/dom/presentation/PresentationTCPSessionTransport.h
@@ -0,0 +1,110 @@
+/* -*- 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 "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();
+
+ void 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;
+
+ // Output stream machinery
+ nsCOMPtr<nsIMultiplexInputStream> mMultiplexStream;
+ nsCOMPtr<nsIAsyncStreamCopier> mMultiplexStreamCopier;
+
+ nsCOMPtr<nsIPresentationSessionTransportCallback> mCallback;
+ nsCOMPtr<nsIPresentationSessionTransportBuilderListener> mListener;
+};
+
+} // 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 000000000..61fd8403f
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PresentationTerminateRequest.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationDevice.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationTerminateRequest, nsIPresentationTerminateRequest)
+
+PresentationTerminateRequest::PresentationTerminateRequest(
+ nsIPresentationDevice* aDevice,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel,
+ bool aIsFromReceiver)
+ : mPresentationId(aPresentationId)
+ , mDevice(aDevice)
+ , mControlChannel(aControlChannel)
+ , mIsFromReceiver(aIsFromReceiver)
+{
+}
+
+PresentationTerminateRequest::~PresentationTerminateRequest()
+{
+}
+
+// nsIPresentationTerminateRequest
+NS_IMETHODIMP
+PresentationTerminateRequest::GetDevice(nsIPresentationDevice** aRetVal)
+{
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsCOMPtr<nsIPresentationDevice> device = mDevice;
+ device.forget(aRetVal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetPresentationId(nsAString& aRetVal)
+{
+ aRetVal = mPresentationId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetControlChannel(
+ nsIPresentationControlChannel** aRetVal)
+{
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel;
+ controlChannel.forget(aRetVal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetIsFromReceiver(bool* aRetVal)
+{
+ *aRetVal = mIsFromReceiver;
+
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/PresentationTerminateRequest.h b/dom/presentation/PresentationTerminateRequest.h
new file mode 100644
index 000000000..ca3563f8d
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PresentationTerminateRequest_h__
+#define mozilla_dom_PresentationTerminateRequest_h__
+
+#include "nsIPresentationTerminateRequest.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationTerminateRequest final : public nsIPresentationTerminateRequest
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONTERMINATEREQUEST
+
+ PresentationTerminateRequest(nsIPresentationDevice* aDevice,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel,
+ bool aIsFromReceiver);
+
+private:
+ virtual ~PresentationTerminateRequest();
+
+ nsString mPresentationId;
+ nsCOMPtr<nsIPresentationDevice> mDevice;
+ nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
+ bool mIsFromReceiver;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_PresentationTerminateRequest_h__ */
+
diff --git a/dom/presentation/PresentationTransportBuilderConstructor.cpp b/dom/presentation/PresentationTransportBuilderConstructor.cpp
new file mode 100644
index 000000000..98177958d
--- /dev/null
+++ b/dom/presentation/PresentationTransportBuilderConstructor.cpp
@@ -0,0 +1,85 @@
+/* -*- 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;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(PresentationTransportBuilderConstructor,
+ DummyPresentationTransportBuilderConstructor)
+
+/* 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 000000000..4250d61ba
--- /dev/null
+++ b/dom/presentation/PresentationTransportBuilderConstructor.h
@@ -0,0 +1,48 @@
+/* -*- 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_DECL_ISUPPORTS_INHERITED
+ 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/interfaces/moz.build b/dom/presentation/interfaces/moz.build
new file mode 100644
index 000000000..935e39000
--- /dev/null
+++ b/dom/presentation/interfaces/moz.build
@@ -0,0 +1,30 @@
+# -*- 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 000000000..669e4088e
--- /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 DOMString 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 DOMString 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 DOMString 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 DOMString presentationId, in DOMString url);
+
+ /*
+ * Terminate a presentation on remote endpoint.
+ * @param presentationId The Id for representing this session.
+ * @throws NS_ERROR_FAILURE on failure
+ */
+ void terminate(in DOMString presentationId);
+
+ /*
+ * Disconnect the control channel.
+ * @param reason The reason of disconnecting channel; NS_OK represents normal.
+ */
+ void disconnect(in nsresult reason);
+
+ /*
+ * 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 DOMString presentationId, in DOMString url);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationControlService.idl b/dom/presentation/interfaces/nsIPresentationControlService.idl
new file mode 100644
index 000000000..d4b967b00
--- /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 DOMString aUrl,
+ in DOMString aPresentationId,
+ in nsIPresentationControlChannel aControlChannel);
+
+ /**
+ * Callback while the remote host is requesting to terminate a presentation session.
+ * @param aDeviceInfo The device information related to the remote host.
+ * @param aPresentationId The Id for representing this session.
+ * @param aControlChannel The control channel for this session.
+ * @param aIsFromReceiver true if termination is initiated by receiver.
+ */
+ void onTerminateRequest(in nsITCPDeviceInfo aDeviceInfo,
+ in DOMString aPresentationId,
+ in nsIPresentationControlChannel aControlChannel,
+ in boolean aIsFromReceiver);
+
+ /**
+ * 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 DOMString url,
+ in DOMString 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 000000000..63e4a52ef
--- /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 DOMString requestedUrl);
+};
+
+
diff --git a/dom/presentation/interfaces/nsIPresentationDeviceManager.idl b/dom/presentation/interfaces/nsIPresentationDeviceManager.idl
new file mode 100644
index 000000000..adff9fc09
--- /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 000000000..2900eb59c
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationDevicePrompt.idl
@@ -0,0 +1,58 @@
+/* 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 nsIDOMEventTarget;
+interface nsIPresentationDevice;
+interface nsIPrincipal;
+
+%{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 DOMString origin;
+
+ // The array of candidate URLs.
+ readonly attribute nsIArray requestURLs;
+
+ // The XUL browser element that the request was originated in.
+ readonly attribute nsIDOMEventTarget 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 000000000..b2c5e530c
--- /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 DOMString url,
+ in DOMString presentationId,
+ in nsIPresentationControlChannel controlChannel);
+
+ /*
+ * Callback while the remote device is requesting to terminate a presentation session.
+ * @param device The remote device that sent session request.
+ * @param presentationId The Id for representing this session.
+ * @param controlChannel The control channel for this session.
+ * @param aIsFromReceiver true if termination is initiated by receiver.
+ */
+ void onTerminateRequest(in nsIPresentationDevice device,
+ in DOMString presentationId,
+ in nsIPresentationControlChannel controlChannel,
+ in boolean aIsFromReceiver);
+
+ /*
+ * 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 DOMString url,
+ in DOMString 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 000000000..546c2fd4b
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationListener.idl
@@ -0,0 +1,50 @@
+/* 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"
+
+[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 DOMString sessionId,
+ in unsigned short state,
+ in nsresult reason);
+
+ /*
+ * Called when receive messages.
+ */
+ void notifyMessage(in DOMString 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 DOMString sessionId);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationLocalDevice.idl b/dom/presentation/interfaces/nsIPresentationLocalDevice.idl
new file mode 100644
index 000000000..80e3b4041
--- /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 000000000..514075dfa
--- /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 000000000..dab1991e4
--- /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.
+ */
+ nsISupports sendRequest(in DOMString url,
+ in DOMString sessionId,
+ in nsIPresentationDevice device);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationService.idl b/dom/presentation/interfaces/nsIPresentationService.idl
new file mode 100644
index 000000000..c3c15bb9f
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationService.idl
@@ -0,0 +1,275 @@
+/* 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 nsIDOMBlob;
+interface nsIDOMEventTarget;
+interface nsIInputStream;
+interface nsIPresentationAvailabilityListener;
+interface nsIPresentationRespondingListener;
+interface nsIPresentationSessionListener;
+interface nsIPresentationTransportBuilderConstructor;
+interface nsIPrincipal;
+
+%{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 "nsTArray.h"
+
+class nsString;
+%}
+
+[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 DOMString 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 DOMString sessionId,
+ in DOMString origin,
+ in DOMString deviceId,
+ in unsigned long long windowId,
+ in nsIDOMEventTarget 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 DOMString sessionId,
+ in uint8_t role,
+ in DOMString 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 DOMString 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 DOMString sessionId,
+ in uint8_t role,
+ in nsIDOMBlob 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString 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 DOMString sessionId, in uint8_t role);
+};
diff --git a/dom/presentation/interfaces/nsIPresentationSessionRequest.idl b/dom/presentation/interfaces/nsIPresentationSessionRequest.idl
new file mode 100644
index 000000000..45a0e314c
--- /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 DOMString url;
+
+ // The Id for representing this session.
+ readonly attribute DOMString 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 000000000..a0b5617d7
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationSessionTransport.idl
@@ -0,0 +1,69 @@
+/* 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 nsIDOMBlob;
+interface nsIInputStream;
+interface nsINetAddr;
+
+%{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 DOMString 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 nsIDOMBlob 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 000000000..969f37d71
--- /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 DOMString 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 DOMString 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 000000000..a9f86fa0d
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIPresentationDevice;
+interface nsIPresentationControlChannel;
+
+%{C++
+#define PRESENTATION_TERMINATE_REQUEST_TOPIC "presentation-terminate-request"
+%}
+
+/*
+ * The event of a device requesting for terminating a presentation session. User can
+ * monitor the terminate request on every device by observing "presentation-terminate-request".
+ */
+[scriptable, uuid(3ddbf3a4-53ee-4b70-9bbc-58ac90dce6b5)]
+interface nsIPresentationTerminateRequest: nsISupports
+{
+ // The device which requesting to terminate presentation session.
+ readonly attribute nsIPresentationDevice device;
+
+ // The Id for representing this session.
+ readonly attribute DOMString presentationId;
+
+ // The control channel for this session.
+ // Should only use this channel to complete session termination.
+ readonly attribute nsIPresentationControlChannel controlChannel;
+
+ // True if termination is initiated by receiver.
+ readonly attribute boolean isFromReceiver;
+};
diff --git a/dom/presentation/ipc/PPresentation.ipdl b/dom/presentation/ipc/PPresentation.ipdl
new file mode 100644
index 000000000..e0f4d2888
--- /dev/null
+++ b/dom/presentation/ipc/PPresentation.ipdl
@@ -0,0 +1,112 @@
+/* -*- 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;
+
+using class IPC::Principal from "mozilla/dom/PermissionMessageUtils.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;
+ Principal 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 000000000..e32b02e8f
--- /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 000000000..fa99dfcab
--- /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 000000000..387332e9e
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderChild.cpp
@@ -0,0 +1,184 @@
+/* -*- 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 "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 = nsGlobalWindow::GetInnerWindowWithId(windowId)->AsInner();
+ 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;
+}
+
+bool
+PresentationBuilderChild::RecvOnOffer(const nsString& aSDP)
+{
+ if (NS_WARN_IF(!mBuilder)) {
+ return false;
+ }
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+
+ if (NS_WARN_IF(NS_FAILED(mBuilder->OnOffer(description)))) {
+ return false;
+ }
+ return true;
+}
+
+bool
+PresentationBuilderChild::RecvOnAnswer(const nsString& aSDP)
+{
+ if (NS_WARN_IF(!mBuilder)) {
+ return false;
+ }
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+
+ if (NS_WARN_IF(NS_FAILED(mBuilder->OnAnswer(description)))) {
+ return false;
+ }
+ return true;
+}
+
+bool
+PresentationBuilderChild::RecvOnIceCandidate(const nsString& aCandidate)
+{
+ if (NS_WARN_IF(mBuilder && NS_FAILED(mBuilder->OnIceCandidate(aCandidate)))) {
+ return false;
+ }
+ return true;
+}
+
+// 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 000000000..5ada53ab7
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderChild.h
@@ -0,0 +1,48 @@
+/* -*- 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/. */
+
+#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;
+
+ virtual bool RecvOnOffer(const nsString& aSDP) override;
+
+ virtual bool RecvOnAnswer(const nsString& aSDP) override;
+
+ virtual bool RecvOnIceCandidate(const nsString& aCandidate) override;
+
+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 000000000..dc784b13c
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderParent.cpp
@@ -0,0 +1,267 @@
+/* -*- 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 "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(nsIDOMBlob* 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)
+{
+ MOZ_COUNT_CTOR(PresentationBuilderParent);
+}
+
+PresentationBuilderParent::~PresentationBuilderParent()
+{
+ MOZ_COUNT_DTOR(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;
+}
+
+bool
+PresentationBuilderParent::RecvSendOffer(const nsString& aSDP)
+{
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendOffer(description)))) {
+ return false;
+ }
+ return true;
+}
+
+bool
+PresentationBuilderParent::RecvSendAnswer(const nsString& aSDP)
+{
+ RefPtr<DCPresentationChannelDescription> description =
+ new DCPresentationChannelDescription(aSDP);
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendAnswer(description)))) {
+ return false;
+ }
+ return true;
+}
+
+bool
+PresentationBuilderParent::RecvSendIceCandidate(const nsString& aCandidate)
+{
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->SendIceCandidate(aCandidate)))) {
+ return false;
+ }
+ return true;
+}
+
+bool
+PresentationBuilderParent::RecvClose(const nsresult& aReason)
+{
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->Close(aReason)))) {
+ return false;
+ }
+ return true;
+}
+
+// Delegate to nsIPresentationSessionTransportBuilderListener
+bool
+PresentationBuilderParent::RecvOnSessionTransport()
+{
+ RefPtr<PresentationBuilderParent> kungFuDeathGrip = this;
+ Unused <<
+ NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->OnSessionTransport(mIPCSessionTransport)));
+ return true;
+}
+
+bool
+PresentationBuilderParent::RecvOnSessionTransportError(const nsresult& aReason)
+{
+ if (NS_WARN_IF(!mBuilderListener ||
+ NS_FAILED(mBuilderListener->OnError(aReason)))) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/ipc/PresentationBuilderParent.h b/dom/presentation/ipc/PresentationBuilderParent.h
new file mode 100644
index 000000000..7fd211ce5
--- /dev/null
+++ b/dom/presentation/ipc/PresentationBuilderParent.h
@@ -0,0 +1,52 @@
+/* -*- 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/. */
+
+#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);
+
+ virtual bool RecvSendOffer(const nsString& aSDP) override;
+
+ virtual bool RecvSendAnswer(const nsString& aSDP) override;
+
+ virtual bool RecvSendIceCandidate(const nsString& aCandidate) override;
+
+ virtual bool RecvClose(const nsresult& aReason) override;
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual bool RecvOnSessionTransport() override;
+
+ virtual bool RecvOnSessionTransportError(const nsresult& aReason) override;
+
+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 000000000..d24ba9e8c
--- /dev/null
+++ b/dom/presentation/ipc/PresentationChild.cpp
@@ -0,0 +1,198 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "mozilla/StaticPtr.h"
+#include "PresentationBuilderChild.h"
+#include "PresentationChild.h"
+#include "PresentationIPCService.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::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)
+{
+ NS_NOTREACHED("We should never be manually allocating PPresentationRequestChild actors");
+ return nullptr;
+}
+
+bool
+PresentationChild::DeallocPPresentationRequestChild(PPresentationRequestChild* aActor)
+{
+ delete aActor;
+ return true;
+}
+
+bool PresentationChild::RecvPPresentationBuilderConstructor(
+ PPresentationBuilderChild* aActor,
+ const nsString& aSessionId,
+ const uint8_t& aRole)
+{
+ // Child will build the session transport
+ PresentationBuilderChild* actor = static_cast<PresentationBuilderChild*>(aActor);
+ return NS_WARN_IF(NS_FAILED(actor->Init())) ? false : true;
+}
+
+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;
+}
+
+
+bool
+PresentationChild::RecvNotifyAvailableChange(
+ nsTArray<nsString>&& aAvailabilityUrls,
+ const bool& aAvailable)
+{
+ if (mService) {
+ Unused <<
+ NS_WARN_IF(NS_FAILED(mService->NotifyAvailableChange(aAvailabilityUrls,
+ aAvailable)));
+ }
+ return true;
+}
+
+bool
+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 true;
+}
+
+bool
+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 true;
+}
+
+bool
+PresentationChild::RecvNotifySessionConnect(const uint64_t& aWindowId,
+ const nsString& aSessionId)
+{
+ if (mService) {
+ Unused << NS_WARN_IF(NS_FAILED(mService->NotifySessionConnect(aWindowId, aSessionId)));
+ }
+ return true;
+}
+
+bool
+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 true;
+}
+
+/*
+ * 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;
+}
+
+bool
+PresentationRequestChild::Recv__delete__(const nsresult& aResult)
+{
+ if (mActorDestroyed) {
+ return true;
+ }
+
+ if (mCallback) {
+ if (NS_FAILED(aResult)) {
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifyError(aResult)));
+ }
+ }
+
+ return true;
+}
+
+bool
+PresentationRequestChild::RecvNotifyRequestUrlSelected(const nsString& aUrl)
+{
+ Unused << NS_WARN_IF(NS_FAILED(mCallback->NotifySuccess(aUrl)));
+ return true;
+}
diff --git a/dom/presentation/ipc/PresentationChild.h b/dom/presentation/ipc/PresentationChild.h
new file mode 100644
index 000000000..1c625b8ce
--- /dev/null
+++ b/dom/presentation/ipc/PresentationChild.h
@@ -0,0 +1,101 @@
+/* -*- 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/. */
+
+#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;
+
+ virtual PPresentationRequestChild*
+ AllocPPresentationRequestChild(const PresentationIPCRequest& aRequest) override;
+
+ virtual bool
+ DeallocPPresentationRequestChild(PPresentationRequestChild* aActor) override;
+
+ bool RecvPPresentationBuilderConstructor(PPresentationBuilderChild* aActor,
+ const nsString& aSessionId,
+ const uint8_t& aRole) override;
+
+ virtual PPresentationBuilderChild*
+ AllocPPresentationBuilderChild(const nsString& aSessionId, const uint8_t& aRole) override;
+
+ virtual bool
+ DeallocPPresentationBuilderChild(PPresentationBuilderChild* aActor) override;
+
+ virtual bool
+ RecvNotifyAvailableChange(nsTArray<nsString>&& aAvailabilityUrls,
+ const bool& aAvailable) override;
+
+ virtual bool
+ RecvNotifySessionStateChange(const nsString& aSessionId,
+ const uint16_t& aState,
+ const nsresult& aReason) override;
+
+ virtual bool
+ RecvNotifyMessage(const nsString& aSessionId,
+ const nsCString& aData,
+ const bool& aIsBinary) override;
+
+ virtual bool
+ RecvNotifySessionConnect(const uint64_t& aWindowId,
+ const nsString& aSessionId) override;
+
+ virtual bool
+ RecvNotifyCloseSessionTransport(const nsString& aSessionId,
+ const uint8_t& aRole,
+ const nsresult& aReason) override;
+
+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;
+
+ virtual bool
+ Recv__delete__(const nsresult& aResult) override;
+
+ virtual bool
+ RecvNotifyRequestUrlSelected(const nsString& aUrl) override;
+
+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 000000000..071ea924f
--- /dev/null
+++ b/dom/presentation/ipc/PresentationContentSessionInfo.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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(nsIDOMBlob* 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 000000000..447485dbd
--- /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"
+
+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(nsIDOMBlob* aBlob);
+
+ nsresult Close(nsresult aReason);
+
+private:
+ virtual ~PresentationContentSessionInfo() {}
+
+ 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 000000000..8c85b239d
--- /dev/null
+++ b/dom/presentation/ipc/PresentationIPCService.cpp
@@ -0,0 +1,538 @@
+/* -*- 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 "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/PPresentation.h"
+#include "mozilla/dom/TabParent.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"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace {
+
+PresentationChild* sPresentationChild;
+
+} // anonymous
+
+NS_IMPL_ISUPPORTS(PresentationIPCService,
+ nsIPresentationService,
+ nsIPresentationAvailabilityListener)
+
+PresentationIPCService::PresentationIPCService()
+{
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ if (NS_WARN_IF(!contentChild)) {
+ 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,
+ nsIDOMEventTarget* aEventTarget,
+ nsIPrincipal* aPrincipal,
+ nsIPresentationServiceCallback* aCallback,
+ nsIPresentationTransportBuilderConstructor* aBuilderConstructor)
+{
+ if (aWindowId != 0) {
+ AddRespondingSessionId(aWindowId,
+ aSessionId,
+ nsIPresentationService::ROLE_CONTROLLER);
+ }
+
+ nsPIDOMWindowInner* window =
+ nsGlobalWindow::GetInnerWindowWithId(aWindowId)->AsInner();
+ TabId tabId = TabParent::GetTabIdFrom(window->GetDocShell());
+
+ return SendRequest(aCallback, StartSessionRequest(aUrls,
+ nsString(aSessionId),
+ nsString(aOrigin),
+ nsString(aDeviceId),
+ aWindowId,
+ tabId,
+ IPC::Principal(aPrincipal)));
+}
+
+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);
+ }
+
+ return SendRequest(nullptr, SendSessionMessageRequest(nsString(aSessionId),
+ aRole,
+ nsString(aData)));
+}
+
+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,
+ nsIDOMBlob* 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());
+
+ nsresult rv = SendRequest(nullptr, CloseSessionRequest(nsString(aSessionId),
+ aRole,
+ aClosedReason));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ 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());
+
+ nsresult rv = SendRequest(nullptr, TerminateSessionRequest(nsString(aSessionId), aRole));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ 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;
+ }
+
+ return SendRequest(aCallback, ReconnectSessionRequest(aUrls,
+ nsString(aSessionId),
+ aRole));
+}
+
+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;
+ }
+
+ return SendRequest(nullptr, BuildTransportRequest(nsString(aSessionId),
+ aRole));
+}
+
+nsresult
+PresentationIPCService::SendRequest(nsIPresentationServiceCallback* aCallback,
+ const PresentationIPCRequest& aRequest)
+{
+ if (sPresentationChild) {
+ PresentationRequestChild* actor = new PresentationRequestChild(aCallback);
+ Unused << NS_WARN_IF(!sPresentationChild->SendPPresentationRequestConstructor(actor, aRequest));
+ }
+ return NS_OK;
+}
+
+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, aListener);
+ return NS_OK;
+ }
+
+ mSessionListeners.Put(aSessionId, 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, 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, info);
+ } else {
+ mSessionInfoAtReceiver.Put(aSessionId, 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)
+{
+ return UpdateWindowIdBySessionIdInternal(aSessionId, aRole, aWindowId);
+}
+
+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)
+{
+ return mAvailabilityManager.DoNotifyAvailableChange(aAvailabilityUrls,
+ aAvailable);
+}
+
+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([windowId]() -> void {
+ PRES_DEBUG("Attempt to close window[%d]\n", windowId);
+
+ if (auto* window = nsGlobalWindow::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);
+}
diff --git a/dom/presentation/ipc/PresentationIPCService.h b/dom/presentation/ipc/PresentationIPCService.h
new file mode 100644
index 000000000..5eab7e68a
--- /dev/null
+++ b/dom/presentation/ipc/PresentationIPCService.h
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+#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();
+ nsresult 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 000000000..02f60500a
--- /dev/null
+++ b/dom/presentation/ipc/PresentationParent.cpp
@@ -0,0 +1,553 @@
+/* -*- 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 "DCPresentationChannelDescription.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/ipc/InputStreamUtils.h"
+#include "mozilla/Unused.h"
+#include "nsIPresentationDeviceManager.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()
+{
+ MOZ_COUNT_CTOR(PresentationParent);
+}
+
+/* virtual */ PresentationParent::~PresentationParent()
+{
+ MOZ_COUNT_DTOR(PresentationParent);
+}
+
+bool
+PresentationParent::Init(ContentParentId aContentParentId)
+{
+ MOZ_ASSERT(!mService);
+ mService = do_GetService(PRESENTATION_SERVICE_CONTRACTID);
+ mChildId = aContentParentId;
+ return NS_WARN_IF(!mService) ? false : true;
+}
+
+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;
+}
+
+bool
+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");
+ }
+
+ return NS_WARN_IF(NS_FAILED(rv)) ? false : true;
+}
+
+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)
+{
+ NS_NOTREACHED("We should never be manually allocating AllocPPresentationBuilderParent actors");
+ return nullptr;
+}
+
+bool
+PresentationParent::DeallocPPresentationBuilderParent(
+ PPresentationBuilderParent* aActor)
+{
+ return true;
+}
+
+bool
+PresentationParent::Recv__delete__()
+{
+ return true;
+}
+
+bool
+PresentationParent::RecvRegisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls)
+{
+ MOZ_ASSERT(mService);
+
+ Unused << NS_WARN_IF(NS_FAILED(mService->RegisterAvailabilityListener(
+ aAvailabilityUrls,
+ this)));
+ mContentAvailabilityUrls.AppendElements(aAvailabilityUrls);
+ return true;
+}
+
+bool
+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 true;
+}
+
+/* virtual */ bool
+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 true;
+ }
+
+ if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
+ mSessionIdsAtController.AppendElement(aSessionId);
+ } else {
+ mSessionIdsAtReceiver.AppendElement(aSessionId);
+ }
+ Unused << NS_WARN_IF(NS_FAILED(mService->RegisterSessionListener(aSessionId, aRole, this)));
+ return true;
+}
+
+/* virtual */ bool
+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 true;
+}
+
+/* virtual */ bool
+PresentationParent::RecvRegisterRespondingHandler(const uint64_t& aWindowId)
+{
+ MOZ_ASSERT(mService);
+
+ mWindowIds.AppendElement(aWindowId);
+ Unused << NS_WARN_IF(NS_FAILED(mService->RegisterRespondingListener(aWindowId, this)));
+ return true;
+}
+
+/* virtual */ bool
+PresentationParent::RecvUnregisterRespondingHandler(const uint64_t& aWindowId)
+{
+ MOZ_ASSERT(mService);
+ mWindowIds.RemoveElement(aWindowId);
+ Unused << NS_WARN_IF(NS_FAILED(mService->UnregisterRespondingListener(aWindowId)));
+ return true;
+}
+
+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;
+}
+
+bool
+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 true;
+}
+
+bool
+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 true;
+}
+
+/*
+ * Implementation of PresentationRequestParent
+ */
+
+NS_IMPL_ISUPPORTS(PresentationRequestParent, nsIPresentationServiceCallback)
+
+PresentationRequestParent::PresentationRequestParent(nsIPresentationService* aService,
+ ContentParentId aContentParentId)
+ : mService(aService)
+ , mChildId(aContentParentId)
+{
+ MOZ_COUNT_CTOR(PresentationRequestParent);
+}
+
+PresentationRequestParent::~PresentationRequestParent()
+{
+ MOZ_COUNT_DTOR(PresentationRequestParent);
+}
+
+void
+PresentationRequestParent::ActorDestroy(ActorDestroyReason aWhy)
+{
+ mActorDestroyed = true;
+ mService = nullptr;
+}
+
+nsresult
+PresentationRequestParent::DoRequest(const StartSessionRequest& aRequest)
+{
+ MOZ_ASSERT(mService);
+
+ mSessionId = aRequest.sessionId();
+
+ nsCOMPtr<nsIDOMEventTarget> eventTarget;
+ ContentProcessManager* cpm = ContentProcessManager::GetSingleton();
+ RefPtr<TabParent> tp =
+ cpm->GetTopLevelTabParentByProcessAndTabId(mChildId, aRequest.tabId());
+ if (tp) {
+ eventTarget = do_QueryInterface(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 000000000..b038aa216
--- /dev/null
+++ b/dom/presentation/ipc/PresentationParent.h
@@ -0,0 +1,137 @@
+/* -*- 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/. */
+
+#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"
+#include "nsIPresentationSessionTransportBuilder.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 bool
+ RecvPPresentationRequestConstructor(PPresentationRequestParent* aActor,
+ const PresentationIPCRequest& aRequest) override;
+
+ virtual PPresentationRequestParent*
+ AllocPPresentationRequestParent(const PresentationIPCRequest& aRequest) override;
+
+ virtual bool
+ DeallocPPresentationRequestParent(PPresentationRequestParent* aActor) override;
+
+ virtual PPresentationBuilderParent*
+ AllocPPresentationBuilderParent(const nsString& aSessionId,
+ const uint8_t& aRole) override;
+
+ virtual bool
+ DeallocPPresentationBuilderParent(
+ PPresentationBuilderParent* aActor) override;
+
+ virtual bool Recv__delete__() override;
+
+ virtual bool RecvRegisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls) override;
+
+ virtual bool RecvUnregisterAvailabilityHandler(
+ nsTArray<nsString>&& aAvailabilityUrls) override;
+
+ virtual bool RecvRegisterSessionHandler(const nsString& aSessionId,
+ const uint8_t& aRole) override;
+
+ virtual bool RecvUnregisterSessionHandler(const nsString& aSessionId,
+ const uint8_t& aRole) override;
+
+ virtual bool RecvRegisterRespondingHandler(const uint64_t& aWindowId) override;
+
+ virtual bool RecvUnregisterRespondingHandler(const uint64_t& aWindowId) override;
+
+ virtual bool RecvNotifyReceiverReady(const nsString& aSessionId,
+ const uint64_t& aWindowId,
+ const bool& aIsLoading) override;
+
+ virtual bool RecvNotifyTransportClosed(const nsString& aSessionId,
+ const uint8_t& aRole,
+ const nsresult& aReason) override;
+
+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 000000000..a7058382f
--- /dev/null
+++ b/dom/presentation/moz.build
@@ -0,0 +1,89 @@
+# -*- 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/.
+
+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',
+ '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',
+ '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_COMPONENTS += [
+ 'PresentationDataChannelSessionTransport.js',
+ 'PresentationDataChannelSessionTransport.manifest',
+ 'PresentationDeviceInfoManager.js',
+ 'PresentationDeviceInfoManager.manifest',
+]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
+ EXTRA_COMPONENTS += [
+ 'PresentationNetworkHelper.js',
+ 'PresentationNetworkHelper.manifest',
+ ]
+
+EXTRA_JS_MODULES += [
+ 'PresentationDeviceInfoManager.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/AndroidCastDeviceProvider.js b/dom/presentation/provider/AndroidCastDeviceProvider.js
new file mode 100644
index 000000000..cf555f77b
--- /dev/null
+++ b/dom/presentation/provider/AndroidCastDeviceProvider.js
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+// globals XPCOMUtils
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+// globals Messaging
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function log(str) {
+ // dump("-*- AndroidCastDeviceProvider -*-: " + str + "\n");
+}
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function descriptionToString(aDescription) {
+ let json = {};
+ json.type = aDescription.type;
+ switch(aDescription.type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+ json.tcpAddress = [];
+ for (let idx = 0; idx < addresses.length; idx++) {
+ let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+ json.tcpAddress.push(address.data);
+ }
+ json.tcpPort = aDescription.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ json.dataChannelSDP = aDescription.dataChannelSDP;
+ break;
+ }
+ return JSON.stringify(json);
+}
+
+const TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE = "AndroidCastDevice:SyncDevice";
+const TOPIC_ANDROID_CAST_DEVICE_ADDED = "AndroidCastDevice:Added";
+const TOPIC_ANDROID_CAST_DEVICE_REMOVED = "AndroidCastDevice:Removed";
+const TOPIC_ANDROID_CAST_DEVICE_START = "AndroidCastDevice:Start";
+const TOPIC_ANDROID_CAST_DEVICE_STOP = "AndroidCastDevice:Stop";
+const TOPIC_PRESENTATION_VIEW_READY = "presentation-view-ready";
+
+function LocalControlChannel(aProvider, aDeviceId, aRole) {
+ log("LocalControlChannel - create new LocalControlChannel for : "
+ + aRole);
+ this._provider = aProvider;
+ this._deviceId = aDeviceId;
+ this._role = aRole;
+}
+
+LocalControlChannel.prototype = {
+ _listener: null,
+ _provider: null,
+ _deviceId: null,
+ _role: null,
+ _isOnTerminating: false,
+ _isOnDisconnecting: false,
+ _pendingConnected: false,
+ _pendingDisconnect: null,
+ _pendingOffer: null,
+ _pendingCandidate: null,
+ /* For the controller, it would be the control channel of the receiver.
+ * For the receiver, it would be the control channel of the controller. */
+ _correspondingControlChannel: null,
+
+ set correspondingControlChannel(aCorrespondingControlChannel) {
+ this._correspondingControlChannel = aCorrespondingControlChannel;
+ },
+
+ get correspondingControlChannel() {
+ return this._correspondingControlChannel;
+ },
+
+ notifyConnected: function LCC_notifyConnected() {
+ this._pendingDisconnect = null;
+
+ if (!this._listener) {
+ this._pendingConnected = true;
+ } else {
+ this._listener.notifyConnected();
+ }
+ },
+
+ onOffer: function LCC_onOffer(aOffer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ log("LocalControlChannel - onOffer of controller should not be called.");
+ return;
+ }
+ if (!this._listener) {
+ this._pendingOffer = aOffer;
+ } else {
+ this._listener.onOffer(aOffer);
+ }
+ },
+
+ onAnswer: function LCC_onAnswer(aAnswer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+ log("LocalControlChannel - onAnswer of receiver should not be called.");
+ return;
+ }
+ this._listener.onAnswer(aAnswer);
+ },
+
+ notifyIceCandidate: function LCC_notifyIceCandidate(aCandidate) {
+ if (!this._listener) {
+ this._pendingCandidate = aCandidate;
+ } else {
+ this._listener.onIceCandidate(aCandidate);
+ }
+ },
+
+ // nsIPresentationControlChannel
+ get listener() {
+ return this._listener;
+ },
+
+ set listener(aListener) {
+ this._listener = aListener;
+
+ if (!this._listener) {
+ return;
+ }
+
+ if (this._pendingConnected) {
+ this.notifyConnected();
+ this._pendingConnected = false;
+ }
+
+ if (this._pendingOffer) {
+ this.onOffer(this._pendingOffer);
+ this._pendingOffer = null;
+ }
+
+ if (this._pendingCandidate) {
+ this.notifyIceCandidate(this._pendingCandidate);
+ this._pendingCandidate = null;
+ }
+
+ if (this._pendingDisconnect != null) {
+ this.disconnect(this._pendingDisconnect);
+ this._pendingDisconnect = null;
+ }
+ },
+
+ sendOffer: function LCC_sendOffer(aOffer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+ log("LocalControlChannel - sendOffer of receiver should not be called.");
+ return;
+ }
+ log("LocalControlChannel - sendOffer aOffer=" + descriptionToString(aOffer));
+ this._correspondingControlChannel.onOffer(aOffer);
+ },
+
+ sendAnswer: function LCC_sendAnswer(aAnswer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ log("LocalControlChannel - sendAnswer of controller should not be called.");
+ return;
+ }
+ log("LocalControlChannel - sendAnswer aAnswer=" + descriptionToString(aAnswer));
+ this._correspondingControlChannel.onAnswer(aAnswer);
+ },
+
+ sendIceCandidate: function LCC_sendIceCandidate(aCandidate) {
+ log("LocalControlChannel - sendAnswer aCandidate=" + aCandidate);
+ this._correspondingControlChannel.notifyIceCandidate(aCandidate);
+ },
+
+ launch: function LCC_launch(aPresentationId, aUrl) {
+ log("LocalControlChannel - launch aPresentationId="
+ + aPresentationId + " aUrl=" + aUrl);
+ // Create control channel for receiver directly.
+ let controlChannel = new LocalControlChannel(this._provider,
+ this._deviceId,
+ Ci.nsIPresentationService.ROLE_RECEIVER);
+
+ // Set up the corresponding control channels for both controller and receiver.
+ this._correspondingControlChannel = controlChannel;
+ controlChannel._correspondingControlChannel = this;
+
+ this._provider.onSessionRequest(this._deviceId,
+ aUrl,
+ aPresentationId,
+ controlChannel);
+ controlChannel.notifyConnected();
+ },
+
+ terminate: function LCC_terminate(aPresentationId) {
+ log("LocalControlChannel - terminate aPresentationId="
+ + aPresentationId);
+
+ if (this._isOnTerminating) {
+ return;
+ }
+
+ // Create control channel for corresponding role directly.
+ let correspondingRole = this._role == Ci.nsIPresentationService.ROLE_CONTROLLER
+ ? Ci.nsIPresentationService.ROLE_RECEIVER
+ : Ci.nsIPresentationService.ROLE_CONTROLLER;
+ let controlChannel = new LocalControlChannel(this._provider,
+ this._deviceId,
+ correspondingRole);
+ // Prevent the termination recursion.
+ controlChannel._isOnTerminating = true;
+
+ // Set up the corresponding control channels for both controller and receiver.
+ this._correspondingControlChannel = controlChannel;
+ controlChannel._correspondingControlChannel = this;
+
+ this._provider.onTerminateRequest(this._deviceId,
+ aPresentationId,
+ controlChannel,
+ this._role == Ci.nsIPresentationService.ROLE_RECEIVER);
+ controlChannel.notifyConnected();
+ },
+
+ disconnect: function LCC_disconnect(aReason) {
+ log("LocalControlChannel - disconnect aReason=" + aReason);
+
+ if (this._isOnDisconnecting) {
+ return;
+ }
+
+ this._pendingOffer = null;
+ this._pendingCandidate = null;
+ this._pendingConnected = false;
+
+ // this._pendingDisconnect is a nsresult.
+ // If it is null, it means no pending disconnect.
+ // If it is NS_OK, it means this control channel is disconnected normally.
+ // If it is other nsresult value, it means this control channel is
+ // disconnected abnormally.
+
+ // Remote endpoint closes the control channel with abnormal reason.
+ if (aReason == Cr.NS_OK &&
+ this._pendingDisconnect != null &&
+ this._pendingDisconnect != Cr.NS_OK) {
+ aReason = this._pendingDisconnect;
+ }
+
+ if (!this._listener) {
+ this._pendingDisconnect = aReason;
+ return;
+ }
+
+ this._isOnDisconnecting = true;
+ this._correspondingControlChannel.disconnect(aReason);
+ this._listener.notifyDisconnected(aReason);
+ },
+
+ reconnect: function LCC_reconnect(aPresentationId, aUrl) {
+ log("1-UA on Android doesn't support reconnect.");
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ classID: Components.ID("{c9be9450-e5c7-4294-a287-376971b017fd}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+};
+
+function ChromecastRemoteDisplayDevice(aProvider, aId, aName, aRole) {
+ this._provider = aProvider;
+ this._id = aId;
+ this._name = aName;
+ this._role = aRole;
+}
+
+ChromecastRemoteDisplayDevice.prototype = {
+ _id: null,
+ _name: null,
+ _role: null,
+ _provider: null,
+ _ctrlChannel: null,
+
+ update: function CRDD_update(aName) {
+ this._name = aName || this._name;
+ },
+
+ // nsIPresentationDevice
+ get id() { return this._id; },
+
+ get name() { return this._name; },
+
+ get type() { return "chromecast"; },
+
+ establishControlChannel: function CRDD_establishControlChannel() {
+ this._ctrlChannel = new LocalControlChannel(this._provider,
+ this._id,
+ this._role);
+
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ // Only connect to Chromecast for controller.
+ // Monitor the receiver being ready.
+ Services.obs.addObserver(this, TOPIC_PRESENTATION_VIEW_READY, true);
+
+ // Launch Chromecast service in Android.
+ Messaging.sendRequestForResult({
+ type: TOPIC_ANDROID_CAST_DEVICE_START,
+ id: this.id
+ }).then(result => {
+ log("Chromecast is connected.");
+ }).catch(error => {
+ log("Can not connect to Chromecast.");
+ // If Chromecast can not be launched, remove the observer.
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+ this._ctrlChannel.disconnect(Cr.NS_ERROR_FAILURE);
+ });
+ } else {
+ // If establishControlChannel called from the receiver, we don't need to
+ // wait the 'presentation-view-ready' event.
+ this._ctrlChannel.notifyConnected();
+ }
+
+ return this._ctrlChannel;
+ },
+
+ disconnect: function CRDD_disconnect() {
+ // Disconnect from Chromecast.
+ Messaging.sendRequestForResult({
+ type: TOPIC_ANDROID_CAST_DEVICE_STOP,
+ id: this.id
+ });
+ },
+
+ isRequestedUrlSupported: function CRDD_isRequestedUrlSupported(aUrl) {
+ let url = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(aUrl, null, null);
+ return url.scheme == "http" || url.scheme == "https";
+ },
+
+ // nsIPresentationLocalDevice
+ get windowId() { return this._id; },
+
+ // nsIObserver
+ observe: function CRDD_observe(aSubject, aTopic, aData) {
+ if (aTopic == TOPIC_PRESENTATION_VIEW_READY) {
+ log("ChromecastRemoteDisplayDevice - observe: aTopic="
+ + aTopic + " data=" + aData);
+ if (this.windowId === aData) {
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+ this._ctrlChannel.notifyConnected();
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice,
+ Ci.nsIPresentationLocalDevice,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver]),
+};
+
+function AndroidCastDeviceProvider() {
+}
+
+AndroidCastDeviceProvider.prototype = {
+ _listener: null,
+ _deviceList: new Map(),
+
+ onSessionRequest: function APDP_onSessionRequest(aDeviceId,
+ aUrl,
+ aPresentationId,
+ aControlChannel) {
+ log("AndroidCastDeviceProvider - onSessionRequest"
+ + " aDeviceId=" + aDeviceId);
+ let device = this._deviceList.get(aDeviceId);
+ let receiverDevice = new ChromecastRemoteDisplayDevice(this,
+ device.id,
+ device.name,
+ Ci.nsIPresentationService.ROLE_RECEIVER);
+ this._listener.onSessionRequest(receiverDevice,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ },
+
+ onTerminateRequest: function APDP_onTerminateRequest(aDeviceId,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver) {
+ log("AndroidCastDeviceProvider - onTerminateRequest"
+ + " aDeviceId=" + aDeviceId
+ + " aPresentationId=" + aPresentationId
+ + " aIsFromReceiver=" + aIsFromReceiver);
+ let device = this._deviceList.get(aDeviceId);
+ this._listener.onTerminateRequest(device,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver);
+ },
+
+ // nsIPresentationDeviceProvider
+ set listener(aListener) {
+ this._listener = aListener;
+
+ // When unload this provider.
+ if (!this._listener) {
+ // remove observer
+ Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED);
+ Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED);
+ return;
+ }
+
+ // Sync all device already found by Android.
+ Services.obs.notifyObservers(null, TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE, "");
+ // Observer registration
+ Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED, false);
+ Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED, false);
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ forceDiscovery: function APDP_forceDiscovery() {
+ // There is no API to do force discovery in Android SDK.
+ },
+
+ // nsIObserver
+ observe: function APDP_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case TOPIC_ANDROID_CAST_DEVICE_ADDED: {
+ let deviceInfo = JSON.parse(aData);
+ let deviceId = deviceInfo.uuid;
+
+ if (!this._deviceList.has(deviceId)) {
+ let device = new ChromecastRemoteDisplayDevice(this,
+ deviceInfo.uuid,
+ deviceInfo.friendlyName,
+ Ci.nsIPresentationService.ROLE_CONTROLLER);
+ this._deviceList.set(device.id, device);
+ this._listener.addDevice(device);
+ } else {
+ let device = this._deviceList.get(deviceId);
+ device.update(deviceInfo.friendlyName);
+ this._listener.updateDevice(device);
+ }
+ break;
+ }
+ case TOPIC_ANDROID_CAST_DEVICE_REMOVED: {
+ let deviceId = aData;
+ let device = this._deviceList.get(deviceId);
+ this._listener.removeDevice(device);
+ this._deviceList.delete(deviceId);
+ break;
+ }
+ }
+ },
+
+ classID: Components.ID("{7394f24c-dbc3-48c8-8a47-cd10169b7c6b}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIPresentationDeviceProvider]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AndroidCastDeviceProvider]);
diff --git a/dom/presentation/provider/AndroidCastDeviceProvider.manifest b/dom/presentation/provider/AndroidCastDeviceProvider.manifest
new file mode 100644
index 000000000..db2aa101b
--- /dev/null
+++ b/dom/presentation/provider/AndroidCastDeviceProvider.manifest
@@ -0,0 +1,4 @@
+# AndroidCastDeviceProvider.js
+component {7394f24c-dbc3-48c8-8a47-cd10169b7c6b} AndroidCastDeviceProvider.js
+contract @mozilla.org/presentation-device/android-cast-device-provider;1 {7394f24c-dbc3-48c8-8a47-cd10169b7c6b}
+category presentation-device-provider AndroidCastDeviceProvider @mozilla.org/presentation-device/android-cast-device-provider;1
diff --git a/dom/presentation/provider/BuiltinProviders.manifest b/dom/presentation/provider/BuiltinProviders.manifest
new file mode 100644
index 000000000..0ba7bcaa7
--- /dev/null
+++ b/dom/presentation/provider/BuiltinProviders.manifest
@@ -0,0 +1,2 @@
+component {f4079b8b-ede5-4b90-a112-5b415a931deb} PresentationControlService.js
+contract @mozilla.org/presentation/control-service;1 {f4079b8b-ede5-4b90-a112-5b415a931deb}
diff --git a/dom/presentation/provider/ControllerStateMachine.jsm b/dom/presentation/provider/ControllerStateMachine.jsm
new file mode 100644
index 000000000..b568a8e9a
--- /dev/null
+++ b/dom/presentation/provider/ControllerStateMachine.jsm
@@ -0,0 +1,240 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ControllerStateMachine"]; // jshint ignore:line
+
+const { utils: Cu } = Components;
+
+/* globals State, CommandType */
+Cu.import("resource://gre/modules/presentation/StateMachineHelper.jsm");
+
+const DEBUG = false;
+function debug(str) {
+ dump("-*- ControllerStateMachine: " + str + "\n");
+}
+
+var handlers = [
+ function _initHandler(stateMachine, command) {
+ // shouldn't receive any command at init state.
+ DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+ },
+ function _connectingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.CONNECT_ACK:
+ stateMachine.state = State.CONNECTED;
+ stateMachine._notifyDeviceConnected();
+ break;
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _connectedHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ case CommandType.LAUNCH_ACK:
+ stateMachine._notifyLaunch(command.presentationId);
+ break;
+ case CommandType.TERMINATE:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.TERMINATE_ACK:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.ANSWER:
+ case CommandType.ICE_CANDIDATE:
+ stateMachine._notifyChannelDescriptor(command);
+ break;
+ case CommandType.RECONNECT_ACK:
+ stateMachine._notifyReconnect(command.presentationId);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _closingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command.
+ break;
+ }
+ },
+ function _closedHandler(stateMachine, command) {
+ // ignore every command in closed state.
+ DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+ },
+];
+
+function ControllerStateMachine(channel, deviceId) {
+ this.state = State.INIT;
+ this._channel = channel;
+ this._deviceId = deviceId;
+}
+
+ControllerStateMachine.prototype = {
+ launch: function _launch(presentationId, url) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.LAUNCH,
+ presentationId: presentationId,
+ url: url,
+ });
+ }
+ },
+
+ terminate: function _terminate(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE,
+ presentationId: presentationId,
+ });
+ }
+ },
+
+ terminateAck: function _terminateAck(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE_ACK,
+ presentationId: presentationId,
+ });
+ }
+ },
+
+ reconnect: function _reconnect(presentationId, url) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.RECONNECT,
+ presentationId: presentationId,
+ url: url,
+ });
+ }
+ },
+
+ sendOffer: function _sendOffer(offer) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.OFFER,
+ offer: offer,
+ });
+ }
+ },
+
+ sendAnswer: function _sendAnswer() {
+ // answer can only be sent by presenting UA.
+ debug("controller shouldn't generate answer");
+ },
+
+ updateIceCandidate: function _updateIceCandidate(candidate) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.ICE_CANDIDATE,
+ candidate: candidate,
+ });
+ }
+ },
+
+ onCommand: function _onCommand(command) {
+ handlers[this.state](this, command);
+ },
+
+ onChannelReady: function _onChannelReady() {
+ if (this.state === State.INIT) {
+ this._sendCommand({
+ type: CommandType.CONNECT,
+ deviceId: this._deviceId
+ });
+ this.state = State.CONNECTING;
+ }
+ },
+
+ onChannelClosed: function _onChannelClose(reason, isByRemote) {
+ switch (this.state) {
+ case State.CONNECTED:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ this._notifyDisconnected(reason);
+ } else {
+ this._sendCommand({
+ type: CommandType.DISCONNECT,
+ reason: reason
+ });
+ this.state = State.CLOSING;
+ this._closeReason = reason;
+ }
+ break;
+ case State.CLOSING:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ if (this._closeReason) {
+ reason = this._closeReason;
+ delete this._closeReason;
+ }
+ this._notifyDisconnected(reason);
+ }
+ break;
+ default:
+ DEBUG && debug("unexpected channel close: " + reason + ", " + isByRemote); // jshint ignore:line
+ break;
+ }
+ },
+
+ _sendCommand: function _sendCommand(command) {
+ this._channel.sendCommand(command);
+ },
+
+ _notifyDeviceConnected: function _notifyDeviceConnected() {
+ //XXX trigger following command
+ this._channel.notifyDeviceConnected();
+ },
+
+ _notifyDisconnected: function _notifyDisconnected(reason) {
+ this._channel.notifyDisconnected(reason);
+ },
+
+ _notifyLaunch: function _notifyLaunch(presentationId) {
+ this._channel.notifyLaunch(presentationId);
+ },
+
+ _notifyTerminate: function _notifyTerminate(presentationId) {
+ this._channel.notifyTerminate(presentationId);
+ },
+
+ _notifyReconnect: function _notifyReconnect(presentationId) {
+ this._channel.notifyReconnect(presentationId);
+ },
+
+ _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
+ switch (command.type) {
+ case CommandType.ANSWER:
+ this._channel.notifyAnswer(command.answer);
+ break;
+ case CommandType.ICE_CANDIDATE:
+ this._channel.notifyIceCandidate(command.candidate);
+ break;
+ }
+ },
+};
+
+this.ControllerStateMachine = ControllerStateMachine; // jshint ignore:line
diff --git a/dom/presentation/provider/DeviceProviderHelpers.cpp b/dom/presentation/provider/DeviceProviderHelpers.cpp
new file mode 100644
index 000000000..00b2c12f1
--- /dev/null
+++ b/dom/presentation/provider/DeviceProviderHelpers.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeviceProviderHelpers.h"
+
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+static const char* const kFxTVPresentationAppUrls[] = {
+ "app://fling-player.gaiamobile.org/index.html",
+ "app://notification-receiver.gaiamobile.org/index.html",
+ nullptr
+};
+
+/* static */ bool
+DeviceProviderHelpers::IsCommonlySupportedScheme(const nsAString& aUrl)
+{
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl);
+ if (NS_FAILED(rv) || !uri) {
+ return false;
+ }
+
+ nsAutoCString scheme;
+ uri->GetScheme(scheme);
+ if (scheme.LowerCaseEqualsLiteral("http") ||
+ scheme.LowerCaseEqualsLiteral("https")) {
+ return true;
+ }
+
+ return false;
+}
+
+/* static */ bool
+DeviceProviderHelpers::IsFxTVSupportedAppUrl(const nsAString& aUrl)
+{
+ // Check if matched with any presentation Apps on TV.
+ for (uint32_t i = 0; kFxTVPresentationAppUrls[i]; i++) {
+ if (aUrl.EqualsASCII(kFxTVPresentationAppUrls[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/DeviceProviderHelpers.h b/dom/presentation/provider/DeviceProviderHelpers.h
new file mode 100644
index 000000000..4bde09bed
--- /dev/null
+++ b/dom/presentation/provider/DeviceProviderHelpers.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_presentation_DeviceProviderHelpers_h
+#define mozilla_dom_presentation_DeviceProviderHelpers_h
+
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class DeviceProviderHelpers final
+{
+public:
+ static bool IsCommonlySupportedScheme(const nsAString& aUrl);
+ static bool IsFxTVSupportedAppUrl(const nsAString& aUrl);
+
+private:
+ DeviceProviderHelpers() = delete;
+};
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_presentation_DeviceProviderHelpers_h
diff --git a/dom/presentation/provider/DisplayDeviceProvider.cpp b/dom/presentation/provider/DisplayDeviceProvider.cpp
new file mode 100644
index 000000000..3f88aba5e
--- /dev/null
+++ b/dom/presentation/provider/DisplayDeviceProvider.cpp
@@ -0,0 +1,580 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DisplayDeviceProvider.h"
+
+#include "DeviceProviderHelpers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+#include "nsIObserverService.h"
+#include "nsIServiceManager.h"
+#include "nsIWindowWatcher.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsSimpleURI.h"
+#include "nsTCPDeviceInfo.h"
+#include "nsThreadUtils.h"
+
+static mozilla::LazyLogModule gDisplayDeviceProviderLog("DisplayDeviceProvider");
+
+#define LOG(format) MOZ_LOG(gDisplayDeviceProviderLog, mozilla::LogLevel::Debug, format)
+
+#define DISPLAY_CHANGED_NOTIFICATION "display-changed"
+#define DEFAULT_CHROME_FEATURES_PREF "toolkit.defaultChromeFeatures"
+#define CHROME_REMOTE_URL_PREF "b2g.multiscreen.chrome_remote_url"
+#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS "dom.presentation.discoverable.retry_ms"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+/**
+ * This wrapper is used to break circular-reference problem.
+ */
+class DisplayDeviceProviderWrappedListener final
+ : public nsIPresentationControlServerListener
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener)
+
+ explicit DisplayDeviceProviderWrappedListener() = default;
+
+ nsresult SetListener(DisplayDeviceProvider* aListener)
+ {
+ mListener = aListener;
+ return NS_OK;
+ }
+
+private:
+ virtual ~DisplayDeviceProviderWrappedListener() = default;
+
+ DisplayDeviceProvider* mListener = nullptr;
+};
+
+NS_IMPL_ISUPPORTS(DisplayDeviceProviderWrappedListener,
+ nsIPresentationControlServerListener)
+
+NS_IMPL_ISUPPORTS(DisplayDeviceProvider::HDMIDisplayDevice,
+ nsIPresentationDevice,
+ nsIPresentationLocalDevice)
+
+// nsIPresentationDevice
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::GetId(nsACString& aId)
+{
+ aId = mWindowId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::GetName(nsACString& aName)
+{
+ aName = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::GetType(nsACString& aType)
+{
+ aType = mType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::GetWindowId(nsACString& aWindowId)
+{
+ aWindowId = mWindowId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice
+ ::EstablishControlChannel(nsIPresentationControlChannel** aControlChannel)
+{
+ nsresult rv = OpenTopLevelWindow();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<DisplayDeviceProvider> provider = mProvider.get();
+ if (NS_WARN_IF(!provider)) {
+ return NS_ERROR_FAILURE;
+ }
+ return provider->Connect(this, aControlChannel);
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::Disconnect()
+{
+ nsresult rv = CloseTopLevelWindow();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::HDMIDisplayDevice::IsRequestedUrlSupported(
+ const nsAString& aRequestedUrl,
+ bool* aRetVal)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aRetVal) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ // 1-UA device only supports HTTP/HTTPS hosted receiver page.
+ *aRetVal = DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl);
+
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::HDMIDisplayDevice::OpenTopLevelWindow()
+{
+ MOZ_ASSERT(!mWindow);
+
+ nsresult rv;
+ nsAutoCString flags(Preferences::GetCString(DEFAULT_CHROME_FEATURES_PREF));
+ if (flags.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ flags.AppendLiteral(",mozDisplayId=");
+ flags.AppendInt(mScreenId);
+
+ nsAutoCString remoteShellURLString(Preferences::GetCString(CHROME_REMOTE_URL_PREF));
+ remoteShellURLString.AppendLiteral("#");
+ remoteShellURLString.Append(mWindowId);
+
+ // URI validation
+ nsCOMPtr<nsIURI> remoteShellURL;
+ rv = NS_NewURI(getter_AddRefs(remoteShellURL), remoteShellURLString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = remoteShellURL->GetSpec(remoteShellURLString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID);
+ MOZ_ASSERT(ww);
+
+ rv = ww->OpenWindow(nullptr,
+ remoteShellURLString.get(),
+ "_blank",
+ flags.get(),
+ nullptr,
+ getter_AddRefs(mWindow));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::HDMIDisplayDevice::CloseTopLevelWindow()
+{
+ MOZ_ASSERT(mWindow);
+
+ nsCOMPtr<nsPIDOMWindowOuter> piWindow = nsPIDOMWindowOuter::From(mWindow);
+ nsresult rv = piWindow->Close();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(DisplayDeviceProvider,
+ nsIObserver,
+ nsIPresentationDeviceProvider,
+ nsIPresentationControlServerListener)
+
+DisplayDeviceProvider::~DisplayDeviceProvider()
+{
+ Uninit();
+}
+
+nsresult
+DisplayDeviceProvider::Init()
+{
+ // Provider must be initialized only once.
+ if (mInitialized) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ mServerRetryMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS);
+ mServerRetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ MOZ_ASSERT(obs);
+
+ obs->AddObserver(this, DISPLAY_CHANGED_NOTIFICATION, false);
+
+ mDevice = new HDMIDisplayDevice(this);
+
+ mWrappedListener = new DisplayDeviceProviderWrappedListener();
+ rv = mWrappedListener->SetListener(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mPresentationService = do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID,
+ &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = StartTCPService();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::Uninit()
+{
+ // Provider must be deleted only once.
+ if (!mInitialized) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, DISPLAY_CHANGED_NOTIFICATION);
+ }
+
+ // Remove device from device manager when the provider is uninit
+ RemoveExternalScreen();
+
+ AbortServerRetry();
+
+ mInitialized = false;
+ mWrappedListener->SetListener(nullptr);
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::StartTCPService()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv;
+ rv = mPresentationService->SetId(NS_LITERAL_CSTRING("DisplayDeviceProvider"));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint16_t servicePort;
+ rv = mPresentationService->GetPort(&servicePort);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ /*
+ * If |servicePort| is non-zero, it means PresentationServer is running.
+ * Otherwise, we should make it start serving.
+ */
+ if (servicePort) {
+ mPort = servicePort;
+ return NS_OK;
+ }
+
+ rv = mPresentationService->SetListener(mWrappedListener);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ AbortServerRetry();
+
+ // 1-UA doesn't need encryption.
+ rv = mPresentationService->StartServer(/* aEncrypted = */ false,
+ /* aPort = */ 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void
+DisplayDeviceProvider::AbortServerRetry()
+{
+ if (mIsServerRetrying) {
+ mIsServerRetrying = false;
+ mServerRetryTimer->Cancel();
+ }
+}
+
+nsresult
+DisplayDeviceProvider::AddExternalScreen()
+{
+ MOZ_ASSERT(mDeviceListener);
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ rv = GetListener(getter_AddRefs(listener));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = listener->AddDevice(mDevice);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::RemoveExternalScreen()
+{
+ MOZ_ASSERT(mDeviceListener);
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ rv = GetListener(getter_AddRefs(listener));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = listener->RemoveDevice(mDevice);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mDevice->Disconnect();
+ return NS_OK;
+}
+
+// nsIPresentationDeviceProvider
+NS_IMETHODIMP
+DisplayDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener)
+{
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener =
+ do_QueryReferent(mDeviceListener, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ listener.forget(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener)
+{
+ mDeviceListener = do_GetWeakReference(aListener);
+ nsresult rv = mDeviceListener ? Init() : Uninit();
+ if(NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::ForceDiscovery()
+{
+ return NS_OK;
+}
+
+// nsIPresentationControlServerListener
+NS_IMETHODIMP
+DisplayDeviceProvider::OnServerReady(uint16_t aPort,
+ const nsACString& aCertFingerprint)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mPort = aPort;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::OnServerStopped(nsresult aResult)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Try restart server if it is stopped abnormally.
+ if (NS_FAILED(aResult)) {
+ mIsServerRetrying = true;
+ mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::OnSessionRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDeviceInfo);
+ MOZ_ASSERT(aControlChannel);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ rv = GetListener(getter_AddRefs(listener));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(!listener);
+
+ rv = listener->OnSessionRequest(mDevice,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel,
+ bool aIsFromReceiver)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDeviceInfo);
+ MOZ_ASSERT(aControlChannel);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ rv = GetListener(getter_AddRefs(listener));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(!listener);
+
+ rv = listener->OnTerminateRequest(mDevice,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DisplayDeviceProvider::OnReconnectRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDeviceInfo);
+ MOZ_ASSERT(aControlChannel);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ rv = GetListener(getter_AddRefs(listener));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(!listener);
+
+ rv = listener->OnReconnectRequest(mDevice,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// nsIObserver
+NS_IMETHODIMP
+DisplayDeviceProvider::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) {
+ nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
+ MOZ_ASSERT(displayInfo);
+
+ int32_t type;
+ bool isConnected;
+ displayInfo->GetConnected(&isConnected);
+ // XXX The ID is as same as the type of display.
+ // See Bug 1138287 and nsScreenManagerGonk::AddScreen() for more detail.
+ displayInfo->GetId(&type);
+
+ if (type == DisplayType::DISPLAY_EXTERNAL) {
+ nsresult rv = isConnected ? AddExternalScreen() : RemoveExternalScreen();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
+ nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject);
+ if (!timer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (timer == mServerRetryTimer) {
+ mIsServerRetrying = false;
+ StartTCPService();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+DisplayDeviceProvider::Connect(HDMIDisplayDevice* aDevice,
+ nsIPresentationControlChannel** aControlChannel)
+{
+ MOZ_ASSERT(aDevice);
+ MOZ_ASSERT(mPresentationService);
+ NS_ENSURE_ARG_POINTER(aControlChannel);
+ *aControlChannel = nullptr;
+
+ nsCOMPtr<nsITCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(),
+ aDevice->Address(),
+ mPort,
+ EmptyCString());
+
+ return mPresentationService->Connect(deviceInfo, aControlChannel);
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/DisplayDeviceProvider.h b/dom/presentation/provider/DisplayDeviceProvider.h
new file mode 100644
index 000000000..ebd5db394
--- /dev/null
+++ b/dom/presentation/provider/DisplayDeviceProvider.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_presentation_provider_DisplayDeviceProvider_h
+#define mozilla_dom_presentation_provider_DisplayDeviceProvider_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCOMPtr.h"
+#include "nsIDOMWindow.h"
+#include "nsIDisplayInfo.h"
+#include "nsIObserver.h"
+#include "nsIPresentationDeviceProvider.h"
+#include "nsIPresentationLocalDevice.h"
+#include "nsIPresentationControlService.h"
+#include "nsITimer.h"
+#include "nsIWindowWatcher.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+// Consistent definition with the definition in
+// widget/gonk/libdisplay/GonkDisplay.h.
+enum DisplayType {
+ DISPLAY_PRIMARY,
+ DISPLAY_EXTERNAL,
+ DISPLAY_VIRTUAL,
+ NUM_DISPLAY_TYPES
+};
+
+class DisplayDeviceProviderWrappedListener;
+
+class DisplayDeviceProvider final : public nsIObserver
+ , public nsIPresentationDeviceProvider
+ , public nsIPresentationControlServerListener
+ , public SupportsWeakPtr<DisplayDeviceProvider>
+{
+private:
+ class HDMIDisplayDevice final : public nsIPresentationLocalDevice
+ {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICE
+ NS_DECL_NSIPRESENTATIONLOCALDEVICE
+
+ // mScreenId is as same as the definition of display type.
+ explicit HDMIDisplayDevice(DisplayDeviceProvider* aProvider)
+ : mScreenId(DisplayType::DISPLAY_EXTERNAL)
+ , mName("HDMI")
+ , mType("external")
+ , mWindowId("hdmi")
+ , mAddress("127.0.0.1")
+ , mProvider(aProvider)
+ {}
+
+ nsresult OpenTopLevelWindow();
+ nsresult CloseTopLevelWindow();
+
+ const nsCString& Id() const { return mWindowId; }
+ const nsCString& Address() const { return mAddress; }
+
+ private:
+ virtual ~HDMIDisplayDevice() = default;
+
+ // Due to the limitation of nsWinodw, mScreenId must be an integer.
+ // And mScreenId is also align to the display type defined in
+ // widget/gonk/libdisplay/GonkDisplay.h.
+ // HDMI display is DisplayType::DISPLAY_EXTERNAL.
+ uint32_t mScreenId;
+ nsCString mName;
+ nsCString mType;
+ nsCString mWindowId;
+ nsCString mAddress;
+
+ nsCOMPtr<mozIDOMWindowProxy> mWindow;
+ // weak pointer
+ // Provider hold a strong pointer to the device. Use weak pointer to prevent
+ // the reference cycle.
+ WeakPtr<DisplayDeviceProvider> mProvider;
+ };
+
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIPRESENTATIONDEVICEPROVIDER
+ NS_DECL_NSIPRESENTATIONCONTROLSERVERLISTENER
+ // For using WeakPtr when MOZ_REFCOUNTED_LEAK_CHECKING defined
+ MOZ_DECLARE_WEAKREFERENCE_TYPENAME(DisplayDeviceProvider)
+
+ nsresult Connect(HDMIDisplayDevice* aDevice,
+ nsIPresentationControlChannel** aControlChannel);
+private:
+ virtual ~DisplayDeviceProvider();
+
+ nsresult Init();
+ nsresult Uninit();
+
+ nsresult AddExternalScreen();
+ nsresult RemoveExternalScreen();
+
+ nsresult StartTCPService();
+
+ void AbortServerRetry();
+
+ // Now support HDMI display only and there should be only one HDMI display.
+ nsCOMPtr<nsIPresentationLocalDevice> mDevice = nullptr;
+ // weak pointer
+ // PresentationDeviceManager (mDeviceListener) hold strong pointer to
+ // DisplayDeviceProvider. Use nsWeakPtr to avoid reference cycle.
+ nsWeakPtr mDeviceListener = nullptr;
+ nsCOMPtr<nsIPresentationControlService> mPresentationService;
+ // Used to prevent reference cycle between DisplayDeviceProvider and
+ // TCPPresentationServer.
+ RefPtr<DisplayDeviceProviderWrappedListener> mWrappedListener;
+
+ bool mInitialized = false;
+ uint16_t mPort;
+
+ bool mIsServerRetrying = false;
+ uint32_t mServerRetryMs;
+ nsCOMPtr<nsITimer> mServerRetryTimer;
+};
+
+} // mozilla
+} // dom
+} // presentation
+
+#endif // mozilla_dom_presentation_provider_DisplayDeviceProvider_h
+
diff --git a/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp b/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp
new file mode 100644
index 000000000..54849c9e3
--- /dev/null
+++ b/dom/presentation/provider/LegacyMDNSDeviceProvider.cpp
@@ -0,0 +1,774 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "LegacyMDNSDeviceProvider.h"
+
+#include "DeviceProviderHelpers.h"
+#include "MainThreadUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIObserverService.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTCPDeviceInfo.h"
+#include "nsThreadUtils.h"
+#include "nsIPropertyBag2.h"
+
+#define PREF_PRESENTATION_DISCOVERY_LEGACY "dom.presentation.discovery.legacy.enabled"
+#define PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS "dom.presentation.discovery.timeout_ms"
+#define PREF_PRESENTATION_DEVICE_NAME "dom.presentation.device.name"
+
+#define LEGACY_SERVICE_TYPE "_mozilla_papi._tcp"
+
+#define LEGACY_PRESENTATION_CONTROL_SERVICE_CONTACT_ID "@mozilla.org/presentation/legacy-control-service;1"
+
+static mozilla::LazyLogModule sLegacyMDNSProviderLogModule("LegacyMDNSDeviceProvider");
+
+#undef LOG_I
+#define LOG_I(...) MOZ_LOG(sLegacyMDNSProviderLogModule, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#undef LOG_E
+#define LOG_E(...) MOZ_LOG(sLegacyMDNSProviderLogModule, mozilla::LogLevel::Error, (__VA_ARGS__))
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+namespace legacy {
+
+static const char* kObservedPrefs[] = {
+ PREF_PRESENTATION_DISCOVERY_LEGACY,
+ PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS,
+ PREF_PRESENTATION_DEVICE_NAME,
+ nullptr
+};
+
+namespace {
+
+static void
+GetAndroidDeviceName(nsACString& aRetVal)
+{
+ nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1");
+ MOZ_ASSERT(infoService, "Could not find a system info service");
+
+ Unused << NS_WARN_IF(NS_FAILED(infoService->GetPropertyAsACString(
+ NS_LITERAL_STRING("device"), aRetVal)));
+}
+
+} //anonymous namespace
+
+/**
+ * This wrapper is used to break circular-reference problem.
+ */
+class DNSServiceWrappedListener final
+ : public nsIDNSServiceDiscoveryListener
+ , public nsIDNSServiceResolveListener
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIDNSSERVICEDISCOVERYLISTENER(mListener)
+ NS_FORWARD_SAFE_NSIDNSSERVICERESOLVELISTENER(mListener)
+
+ explicit DNSServiceWrappedListener() = default;
+
+ nsresult SetListener(LegacyMDNSDeviceProvider* aListener)
+ {
+ mListener = aListener;
+ return NS_OK;
+ }
+
+private:
+ virtual ~DNSServiceWrappedListener() = default;
+
+ LegacyMDNSDeviceProvider* mListener = nullptr;
+};
+
+NS_IMPL_ISUPPORTS(DNSServiceWrappedListener,
+ nsIDNSServiceDiscoveryListener,
+ nsIDNSServiceResolveListener)
+
+NS_IMPL_ISUPPORTS(LegacyMDNSDeviceProvider,
+ nsIPresentationDeviceProvider,
+ nsIDNSServiceDiscoveryListener,
+ nsIDNSServiceResolveListener,
+ nsIObserver)
+
+LegacyMDNSDeviceProvider::~LegacyMDNSDeviceProvider()
+{
+ Uninit();
+}
+
+nsresult
+LegacyMDNSDeviceProvider::Init()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mInitialized) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ mMulticastDNS = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mWrappedListener = new DNSServiceWrappedListener();
+ if (NS_WARN_IF(NS_FAILED(rv = mWrappedListener->SetListener(this)))) {
+ return rv;
+ }
+
+ mPresentationService = do_CreateInstance(LEGACY_PRESENTATION_CONTROL_SERVICE_CONTACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mDiscoveryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ Preferences::AddStrongObservers(this, kObservedPrefs);
+
+ mDiscoveryEnabled = Preferences::GetBool(PREF_PRESENTATION_DISCOVERY_LEGACY);
+ mDiscoveryTimeoutMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS);
+ mServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME);
+
+ // FIXME: Bug 1185806 - Provide a common device name setting.
+ if (mServiceName.IsEmpty()) {
+ GetAndroidDeviceName(mServiceName);
+ Unused << Preferences::SetCString(PREF_PRESENTATION_DEVICE_NAME, mServiceName);
+ }
+
+ Unused << mPresentationService->SetId(mServiceName);
+
+ if (mDiscoveryEnabled && NS_WARN_IF(NS_FAILED(rv = ForceDiscovery()))) {
+ return rv;
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::Uninit()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mInitialized) {
+ return NS_OK;
+ }
+
+ ClearDevices();
+
+ Preferences::RemoveObservers(this, kObservedPrefs);
+
+ StopDiscovery(NS_OK);
+
+ mMulticastDNS = nullptr;
+
+ if (mWrappedListener) {
+ mWrappedListener->SetListener(nullptr);
+ mWrappedListener = nullptr;
+ }
+
+ mInitialized = false;
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::StopDiscovery(nsresult aReason)
+{
+ LOG_I("StopDiscovery (0x%08x)", aReason);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (mDiscoveryRequest) {
+ mDiscoveryRequest->Cancel(aReason);
+ mDiscoveryRequest = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::Connect(Device* aDevice,
+ nsIPresentationControlChannel** aRetVal)
+{
+ MOZ_ASSERT(aDevice);
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<TCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(),
+ aDevice->Address(),
+ aDevice->Port(),
+ EmptyCString());
+
+ return mPresentationService->Connect(deviceInfo, aRetVal);
+}
+
+nsresult
+LegacyMDNSDeviceProvider::AddDevice(const nsACString& aId,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<Device> device = new Device(aId, /* ID */
+ aServiceName,
+ aServiceType,
+ aAddress,
+ aPort,
+ DeviceState::eActive,
+ this);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->AddDevice(device);
+ }
+
+ mDevices.AppendElement(device);
+
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::UpdateDevice(const uint32_t aIndex,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+ device->Update(aServiceName, aServiceType, aAddress, aPort);
+ device->ChangeState(DeviceState::eActive);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->UpdateDevice(device);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::RemoveDevice(const uint32_t aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+
+ LOG_I("RemoveDevice: %s", device->Id().get());
+ mDevices.RemoveElementAt(aIndex);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->RemoveDevice(device);
+ }
+
+ return NS_OK;
+}
+
+bool
+LegacyMDNSDeviceProvider::FindDeviceById(const nsACString& aId,
+ uint32_t& aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(aId,
+ /* aName = */ EmptyCString(),
+ /* aType = */ EmptyCString(),
+ /* aHost = */ EmptyCString(),
+ /* aPort = */ 0,
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceIdComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+bool
+LegacyMDNSDeviceProvider::FindDeviceByAddress(const nsACString& aAddress,
+ uint32_t& aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(/* aId = */ EmptyCString(),
+ /* aName = */ EmptyCString(),
+ /* aType = */ EmptyCString(),
+ aAddress,
+ /* aPort = */ 0,
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceAddressComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+void
+LegacyMDNSDeviceProvider::MarkAllDevicesUnknown()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (auto& device : mDevices) {
+ device->ChangeState(DeviceState::eUnknown);
+ }
+}
+
+void
+LegacyMDNSDeviceProvider::ClearUnknownDevices()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ if (mDevices[i]->State() == DeviceState::eUnknown) {
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+ }
+}
+
+void
+LegacyMDNSDeviceProvider::ClearDevices()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+}
+
+// nsIPresentationDeviceProvider
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener =
+ do_QueryReferent(mDeviceListener, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ listener.forget(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDeviceListener = do_GetWeakReference(aListener);
+
+ nsresult rv;
+ if (mDeviceListener) {
+ if (NS_WARN_IF(NS_FAILED(rv = Init()))) {
+ return rv;
+ }
+ } else {
+ if (NS_WARN_IF(NS_FAILED(rv = Uninit()))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::ForceDiscovery()
+{
+ LOG_I("ForceDiscovery (%d)", mDiscoveryEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDiscoveryEnabled) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mDiscoveryTimer);
+ MOZ_ASSERT(mMulticastDNS);
+
+ // if it's already discovering, extend existing discovery timeout.
+ nsresult rv;
+ if (mIsDiscovering) {
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (NS_WARN_IF(NS_FAILED( rv = mDiscoveryTimer->Init(this,
+ mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ StopDiscovery(NS_OK);
+
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->StartDiscovery(
+ NS_LITERAL_CSTRING(LEGACY_SERVICE_TYPE),
+ mWrappedListener,
+ getter_AddRefs(mDiscoveryRequest))))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// nsIDNSServiceDiscoveryListener
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnDiscoveryStarted(const nsACString& aServiceType)
+{
+ LOG_I("OnDiscoveryStarted");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ MarkAllDevicesUnknown();
+
+ nsresult rv;
+ if (NS_WARN_IF(NS_FAILED(rv = mDiscoveryTimer->Init(this,
+ mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+
+ mIsDiscovering = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnDiscoveryStopped(const nsACString& aServiceType)
+{
+ LOG_I("OnDiscoveryStopped");
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ClearUnknownDevices();
+
+ mIsDiscovering = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnServiceFound(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv ;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceFound: %s", serviceName.get());
+
+ if (mMulticastDNS) {
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService(
+ aServiceInfo, mWrappedListener)))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnServiceLost(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceLost: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ uint32_t index;
+ if (!FindDeviceById(host, index)) {
+ // given device was not found
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = RemoveDevice(index)))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnStartDiscoveryFailed(const nsACString& aServiceType,
+ int32_t aErrorCode)
+{
+ LOG_E("OnStartDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnStopDiscoveryFailed(const nsACString& aServiceType,
+ int32_t aErrorCode)
+{
+ LOG_E("OnStopDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIDNSServiceResolveListener
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceResolved: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ nsAutoCString address;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetAddress(address)))) {
+ return rv;
+ }
+
+ uint16_t port;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetPort(&port)))) {
+ return rv;
+ }
+
+ nsAutoCString serviceType;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceType(serviceType)))) {
+ return rv;
+ }
+
+ uint32_t index;
+ if (FindDeviceById(host, index)) {
+ return UpdateDevice(index,
+ serviceName,
+ serviceType,
+ address,
+ port);
+ } else {
+ return AddDevice(host,
+ serviceName,
+ serviceType,
+ address,
+ port);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo,
+ int32_t aErrorCode)
+{
+ LOG_E("OnResolveFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIObserver
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ConvertUTF16toUTF8 data(aData);
+ LOG_I("Observe: topic = %s, data = %s", aTopic, data.get());
+
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_LEGACY)) {
+ OnDiscoveryChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERY_LEGACY));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)) {
+ OnDiscoveryTimeoutChanged(Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DEVICE_NAME)) {
+ nsAdoptingCString newServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME);
+ if (!mServiceName.Equals(newServiceName)) {
+ OnServiceNameChanged(newServiceName);
+ }
+ }
+ } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
+ StopDiscovery(NS_OK);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::OnDiscoveryChanged(bool aEnabled)
+{
+ LOG_I("DiscoveryEnabled = %d\n", aEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryEnabled = aEnabled;
+
+ if (mDiscoveryEnabled) {
+ return ForceDiscovery();
+ }
+
+ return StopDiscovery(NS_OK);
+}
+
+nsresult
+LegacyMDNSDeviceProvider::OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs)
+{
+ LOG_I("OnDiscoveryTimeoutChanged = %d\n", aTimeoutMs);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryTimeoutMs = aTimeoutMs;
+
+ return NS_OK;
+}
+
+nsresult
+LegacyMDNSDeviceProvider::OnServiceNameChanged(const nsACString& aServiceName)
+{
+ LOG_I("serviceName = %s\n", PromiseFlatCString(aServiceName).get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mServiceName = aServiceName;
+ mPresentationService->SetId(mServiceName);
+
+ return NS_OK;
+}
+
+// LegacyMDNSDeviceProvider::Device
+NS_IMPL_ISUPPORTS(LegacyMDNSDeviceProvider::Device,
+ nsIPresentationDevice)
+
+// nsIPresentationDevice
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::GetId(nsACString& aId)
+{
+ aId = mId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::GetName(nsACString& aName)
+{
+ aName = mName;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::GetType(nsACString& aType)
+{
+ aType = mType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::EstablishControlChannel(
+ nsIPresentationControlChannel** aRetVal)
+{
+ if (!mProvider) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return mProvider->Connect(this, aRetVal);
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::Disconnect()
+{
+ // No need to do anything when disconnect.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LegacyMDNSDeviceProvider::Device::IsRequestedUrlSupported(
+ const nsAString& aRequestedUrl,
+ bool* aRetVal)
+{
+ if (!aRetVal) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ // Legacy TV 2.5 device only support a fixed set of presentation Apps.
+ *aRetVal = DeviceProviderHelpers::IsFxTVSupportedAppUrl(aRequestedUrl);
+
+ return NS_OK;
+}
+
+} // namespace legacy
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/LegacyMDNSDeviceProvider.h b/dom/presentation/provider/LegacyMDNSDeviceProvider.h
new file mode 100644
index 000000000..33ba877d3
--- /dev/null
+++ b/dom/presentation/provider/LegacyMDNSDeviceProvider.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h
+#define mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsICancelable.h"
+#include "nsIDNSServiceDiscovery.h"
+#include "nsIObserver.h"
+#include "nsIPresentationDevice.h"
+#include "nsIPresentationDeviceProvider.h"
+#include "nsIPresentationControlService.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakPtr.h"
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+namespace legacy {
+
+class DNSServiceWrappedListener;
+class MulticastDNSService;
+
+class LegacyMDNSDeviceProvider final
+ : public nsIPresentationDeviceProvider
+ , public nsIDNSServiceDiscoveryListener
+ , public nsIDNSServiceResolveListener
+ , public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICEPROVIDER
+ NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER
+ NS_DECL_NSIDNSSERVICERESOLVELISTENER
+ NS_DECL_NSIOBSERVER
+
+ explicit LegacyMDNSDeviceProvider() = default;
+ nsresult Init();
+ nsresult Uninit();
+
+private:
+ enum class DeviceState : uint32_t {
+ eUnknown,
+ eActive
+ };
+
+ class Device final : public nsIPresentationDevice
+ {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICE
+
+ explicit Device(const nsACString& aId,
+ const nsACString& aName,
+ const nsACString& aType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ DeviceState aState,
+ LegacyMDNSDeviceProvider* aProvider)
+ : mId(aId)
+ , mName(aName)
+ , mType(aType)
+ , mAddress(aAddress)
+ , mPort(aPort)
+ , mState(aState)
+ , mProvider(aProvider)
+ {
+ }
+
+ const nsCString& Id() const
+ {
+ return mId;
+ }
+
+ const nsCString& Address() const
+ {
+ return mAddress;
+ }
+
+ uint16_t Port() const
+ {
+ return mPort;
+ }
+
+ DeviceState State() const
+ {
+ return mState;
+ }
+
+ void ChangeState(DeviceState aState)
+ {
+ mState = aState;
+ }
+
+ void Update(const nsACString& aName,
+ const nsACString& aType,
+ const nsACString& aAddress,
+ const uint16_t aPort)
+ {
+ mName = aName;
+ mType = aType;
+ mAddress = aAddress;
+ mPort = aPort;
+ }
+
+ private:
+ virtual ~Device() = default;
+
+ nsCString mId;
+ nsCString mName;
+ nsCString mType;
+ nsCString mAddress;
+ uint16_t mPort;
+ DeviceState mState;
+ LegacyMDNSDeviceProvider* mProvider;
+ };
+
+ struct DeviceIdComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Id() == aB->Id();
+ }
+ };
+
+ struct DeviceAddressComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Address() == aB->Address();
+ }
+ };
+
+ virtual ~LegacyMDNSDeviceProvider();
+ nsresult StopDiscovery(nsresult aReason);
+ nsresult Connect(Device* aDevice,
+ nsIPresentationControlChannel** aRetVal);
+
+ // device manipulation
+ nsresult AddDevice(const nsACString& aId,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort);
+ nsresult UpdateDevice(const uint32_t aIndex,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort);
+ nsresult RemoveDevice(const uint32_t aIndex);
+ bool FindDeviceById(const nsACString& aId,
+ uint32_t& aIndex);
+
+ bool FindDeviceByAddress(const nsACString& aAddress,
+ uint32_t& aIndex);
+
+ void MarkAllDevicesUnknown();
+ void ClearUnknownDevices();
+ void ClearDevices();
+
+ // preferences
+ nsresult OnDiscoveryChanged(bool aEnabled);
+ nsresult OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs);
+ nsresult OnServiceNameChanged(const nsACString& aServiceName);
+
+ bool mInitialized = false;
+ nsWeakPtr mDeviceListener;
+ nsCOMPtr<nsIPresentationControlService> mPresentationService;
+ nsCOMPtr<nsIDNSServiceDiscovery> mMulticastDNS;
+ RefPtr<DNSServiceWrappedListener> mWrappedListener;
+
+ nsCOMPtr<nsICancelable> mDiscoveryRequest;
+
+ nsTArray<RefPtr<Device>> mDevices;
+
+ bool mDiscoveryEnabled = false;
+ bool mIsDiscovering = false;
+ uint32_t mDiscoveryTimeoutMs;
+ nsCOMPtr<nsITimer> mDiscoveryTimer;
+
+ nsCString mServiceName;
+};
+
+} // namespace legacy
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_presentation_provider_LegacyMDNSDeviceProvider_h
diff --git a/dom/presentation/provider/LegacyPresentationControlService.js b/dom/presentation/provider/LegacyPresentationControlService.js
new file mode 100644
index 000000000..b27177b63
--- /dev/null
+++ b/dom/presentation/provider/LegacyPresentationControlService.js
@@ -0,0 +1,488 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* globals XPCOMUtils */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+/* globals Services */
+Cu.import("resource://gre/modules/Services.jsm");
+/* globals NetUtil */
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug");
+function log(aMsg) {
+ dump("-*- LegacyPresentationControlService.js: " + aMsg + "\n");
+}
+
+function LegacyPresentationControlService() {
+ DEBUG && log("LegacyPresentationControlService - ctor"); //jshint ignore:line
+ this._id = null;
+}
+
+LegacyPresentationControlService.prototype = {
+ startServer: function() {
+ DEBUG && log("LegacyPresentationControlService - doesn't support receiver mode"); //jshint ignore:line
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ get id() {
+ return this._id;
+ },
+
+ set id(aId) {
+ this._id = aId;
+ },
+
+ get port() {
+ return 0;
+ },
+
+ get version() {
+ return 0;
+ },
+
+ set listener(aListener) { //jshint ignore:line
+ DEBUG && log("LegacyPresentationControlService - doesn't support receiver mode"); //jshint ignore:line
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ get listener() {
+ return null;
+ },
+
+ connect: function(aDeviceInfo) {
+ if (!this.id) {
+ DEBUG && log("LegacyPresentationControlService - Id has not initialized; requestSession fails"); //jshint ignore:line
+ return null;
+ }
+ DEBUG && log("LegacyPresentationControlService - requestSession to " + aDeviceInfo.id); //jshint ignore:line
+
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsISocketTransportService);
+
+ let socketTransport;
+ try {
+ socketTransport = sts.createTransport(null,
+ 0,
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ null);
+ } catch (e) {
+ DEBUG && log("LegacyPresentationControlService - createTransport throws: " + e); //jshint ignore:line
+ // Pop the exception to |TCPDevice.establishControlChannel|
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ return new LegacyTCPControlChannel(this.id,
+ socketTransport,
+ aDeviceInfo);
+ },
+
+ close: function() {
+ DEBUG && log("LegacyPresentationControlService - close"); //jshint ignore:line
+ },
+
+ classID: Components.ID("{b21816fe-8aff-4811-86d2-85a7444c557e}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+};
+
+function ChannelDescription(aInit) {
+ this._type = aInit.type;
+ switch (this._type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ this._tcpAddresses = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ for (let address of aInit.tcpAddress) {
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"]
+ .createInstance(Ci.nsISupportsCString);
+ wrapper.data = address;
+ this._tcpAddresses.appendElement(wrapper, false);
+ }
+
+ this._tcpPort = aInit.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ this._dataChannelSDP = aInit.dataChannelSDP;
+ break;
+ }
+}
+
+ChannelDescription.prototype = {
+ _type: 0,
+ _tcpAddresses: null,
+ _tcpPort: 0,
+ _dataChannelSDP: "",
+
+ get type() {
+ return this._type;
+ },
+
+ get tcpAddress() {
+ return this._tcpAddresses;
+ },
+
+ get tcpPort() {
+ return this._tcpPort;
+ },
+
+ get dataChannelSDP() {
+ return this._dataChannelSDP;
+ },
+
+ classID: Components.ID("{d69fc81c-4f40-47a3-97e6-b4cf5db2294e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+};
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function discriptionAsJson(aDescription) {
+ let json = {};
+ json.type = aDescription.type;
+ switch(aDescription.type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+ json.tcpAddress = [];
+ for (let idx = 0; idx < addresses.length; idx++) {
+ let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+ json.tcpAddress.push(address.data);
+ }
+ json.tcpPort = aDescription.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ json.dataChannelSDP = aDescription.dataChannelSDP;
+ break;
+ }
+ return json;
+}
+
+function LegacyTCPControlChannel(id,
+ transport,
+ deviceInfo) {
+ DEBUG && log("create LegacyTCPControlChannel"); //jshint ignore:line
+ this._deviceInfo = deviceInfo;
+ this._transport = transport;
+
+ this._id = id;
+
+ let currentThread = Services.tm.currentThread;
+ transport.setEventSink(this, currentThread);
+
+ this._input = this._transport.openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this._input.asyncWait(this.QueryInterface(Ci.nsIStreamListener),
+ Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY,
+ 0,
+ currentThread);
+
+ this._output = this._transport
+ .openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED, 0, 0);
+}
+
+LegacyTCPControlChannel.prototype = {
+ _connected: false,
+ _pendingOpen: false,
+ _pendingAnswer: null,
+ _pendingClose: null,
+ _pendingCloseReason: null,
+
+ _sendMessage: function(aJSONData, aOnThrow) {
+ if (!aOnThrow) {
+ aOnThrow = function(e) {throw e.result;};
+ }
+
+ if (!aJSONData) {
+ aOnThrow();
+ return;
+ }
+
+ if (!this._connected) {
+ DEBUG && log("LegacyTCPControlChannel - send" + aJSONData.type + " fails"); //jshint ignore:line
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ try {
+ this._send(aJSONData);
+ } catch (e) {
+ aOnThrow(e);
+ }
+ },
+
+ _sendInit: function() {
+ let msg = {
+ type: "requestSession:Init",
+ presentationId: this._presentationId,
+ url: this._url,
+ id: this._id,
+ };
+
+ this._sendMessage(msg, function(e) {
+ this.disconnect();
+ this._notifyDisconnected(e.result);
+ });
+ },
+
+ launch: function(aPresentationId, aUrl) {
+ this._presentationId = aPresentationId;
+ this._url = aUrl;
+
+ this._sendInit();
+ },
+
+ terminate: function() {
+ // Legacy protocol doesn't support extra terminate protocol.
+ // Trigger error handling for browser to shutdown all the resource locally.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ sendOffer: function(aOffer) {
+ let msg = {
+ type: "requestSession:Offer",
+ presentationId: this._presentationId,
+ offer: discriptionAsJson(aOffer),
+ };
+ this._sendMessage(msg);
+ },
+
+ sendAnswer: function(aAnswer) { //jshint ignore:line
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ sendIceCandidate: function(aCandidate) {
+ let msg = {
+ type: "requestSession:IceCandidate",
+ presentationId: this._presentationId,
+ iceCandidate: aCandidate,
+ };
+ this._sendMessage(msg);
+ },
+ // may throw an exception
+ _send: function(aMsg) {
+ DEBUG && log("LegacyTCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); //jshint ignore:line
+
+ /**
+ * XXX In TCP streaming, it is possible that more than one message in one
+ * TCP packet. We use line delimited JSON to identify where one JSON encoded
+ * object ends and the next begins. Therefore, we do not allow newline
+ * characters whithin the whole message, and add a newline at the end.
+ * Please see the parser code in |onDataAvailable|.
+ */
+ let message = JSON.stringify(aMsg).replace(["\n"], "") + "\n";
+ try {
+ this._output.write(message, message.length);
+ } catch(e) {
+ DEBUG && log("LegacyTCPControlChannel - Failed to send message: " + e.name); //jshint ignore:line
+ throw e;
+ }
+ },
+
+ // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait)
+ // Only used for detecting connection refused
+ onInputStreamReady: function(aStream) {
+ try {
+ aStream.available();
+ } catch (e) {
+ DEBUG && log("LegacyTCPControlChannel - onInputStreamReady error: " + e.name); //jshint ignore:line
+ // NS_ERROR_CONNECTION_REFUSED
+ this._listener.notifyDisconnected(e.result);
+ }
+ },
+
+ // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink)
+ onTransportStatus: function(aTransport, aStatus, aProg, aProgMax) { //jshint ignore:line
+ DEBUG && log("LegacyTCPControlChannel - onTransportStatus: "
+ + aStatus.toString(16)); //jshint ignore:line
+ if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ this._connected = true;
+
+ if (!this._pump) {
+ this._createInputStreamPump();
+ }
+
+ this._notifyConnected();
+ }
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStartRequest: function() {
+ DEBUG && log("LegacyTCPControlChannel - onStartRequest"); //jshint ignore:line
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStopRequest: function(aRequest, aContext, aStatus) {
+ DEBUG && log("LegacyTCPControlChannel - onStopRequest: " + aStatus); //jshint ignore:line
+ this.disconnect(aStatus);
+ this._notifyDisconnected(aStatus);
+ },
+
+ // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead)
+ onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { //jshint ignore:line
+ let data = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ DEBUG && log("LegacyTCPControlChannel - onDataAvailable: " + data); //jshint ignore:line
+
+ // Parser of line delimited JSON. Please see |_send| for more informaiton.
+ let jsonArray = data.split("\n");
+ jsonArray.pop();
+ for (let json of jsonArray) {
+ let msg;
+ try {
+ msg = JSON.parse(json);
+ } catch (e) {
+ DEBUG && log("LegacyTCPSignalingChannel - error in parsing json: " + e); //jshint ignore:line
+ }
+
+ this._handleMessage(msg);
+ }
+ },
+
+ _createInputStreamPump: function() {
+ DEBUG && log("LegacyTCPControlChannel - create pump"); //jshint ignore:line
+ this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].
+ createInstance(Ci.nsIInputStreamPump);
+ this._pump.init(this._input, -1, -1, 0, 0, false);
+ this._pump.asyncRead(this, null);
+ },
+
+ // Handle command from remote side
+ _handleMessage: function(aMsg) {
+ DEBUG && log("LegacyTCPControlChannel - handleMessage from "
+ + JSON.stringify(this._deviceInfo) + ": " + JSON.stringify(aMsg)); //jshint ignore:line
+ switch (aMsg.type) {
+ case "requestSession:Answer": {
+ this._onAnswer(aMsg.answer);
+ break;
+ }
+ case "requestSession:IceCandidate": {
+ this._listener.onIceCandidate(aMsg.iceCandidate);
+ break;
+ }
+ case "requestSession:CloseReason": {
+ this._pendingCloseReason = aMsg.reason;
+ break;
+ }
+ }
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ set listener(aListener) {
+ DEBUG && log("LegacyTCPControlChannel - set listener: " + aListener); //jshint ignore:line
+ if (!aListener) {
+ this._listener = null;
+ return;
+ }
+
+ this._listener = aListener;
+ if (this._pendingOpen) {
+ this._pendingOpen = false;
+ DEBUG && log("LegacyTCPControlChannel - notify pending opened"); //jshint ignore:line
+ this._listener.notifyConnected();
+ }
+
+ if (this._pendingAnswer) {
+ let answer = this._pendingAnswer;
+ DEBUG && log("LegacyTCPControlChannel - notify pending answer: " +
+ JSON.stringify(answer)); // jshint ignore:line
+ this._listener.onAnswer(new ChannelDescription(answer));
+ this._pendingAnswer = null;
+ }
+
+ if (this._pendingClose) {
+ DEBUG && log("LegacyTCPControlChannel - notify pending closed"); //jshint ignore:line
+ this._notifyDisconnected(this._pendingCloseReason);
+ this._pendingClose = null;
+ }
+ },
+
+ /**
+ * These functions are designed to handle the interaction with listener
+ * appropriately. |_FUNC| is to handle |this._listener.FUNC|.
+ */
+ _onAnswer: function(aAnswer) {
+ if (!this._connected) {
+ return;
+ }
+ if (!this._listener) {
+ this._pendingAnswer = aAnswer;
+ return;
+ }
+ DEBUG && log("LegacyTCPControlChannel - notify answer: " + JSON.stringify(aAnswer)); //jshint ignore:line
+ this._listener.onAnswer(new ChannelDescription(aAnswer));
+ },
+
+ _notifyConnected: function() {
+ this._connected = true;
+ this._pendingClose = false;
+ this._pendingCloseReason = Cr.NS_OK;
+
+ if (!this._listener) {
+ this._pendingOpen = true;
+ return;
+ }
+
+ DEBUG && log("LegacyTCPControlChannel - notify opened"); //jshint ignore:line
+ this._listener.notifyConnected();
+ },
+
+ _notifyDisconnected: function(aReason) {
+ this._connected = false;
+ this._pendingOpen = false;
+ this._pendingAnswer = null;
+
+ // Remote endpoint closes the control channel with abnormal reason.
+ if (aReason == Cr.NS_OK && this._pendingCloseReason != Cr.NS_OK) {
+ aReason = this._pendingCloseReason;
+ }
+
+ if (!this._listener) {
+ this._pendingClose = true;
+ this._pendingCloseReason = aReason;
+ return;
+ }
+
+ DEBUG && log("LegacyTCPControlChannel - notify closed"); //jshint ignore:line
+ this._listener.notifyDisconnected(aReason);
+ },
+
+ disconnect: function(aReason) {
+ DEBUG && log("LegacyTCPControlChannel - close with reason: " + aReason); //jshint ignore:line
+
+ if (this._connected) {
+ // default reason is NS_OK
+ if (typeof aReason !== "undefined" && aReason !== Cr.NS_OK) {
+ let msg = {
+ type: "requestSession:CloseReason",
+ presentationId: this._presentationId,
+ reason: aReason,
+ };
+ this._sendMessage(msg);
+ this._pendingCloseReason = aReason;
+ }
+
+ this._transport.setEventSink(null, null);
+ this._pump = null;
+
+ this._input.close();
+ this._output.close();
+
+ this._connected = false;
+ }
+ },
+
+ reconnect: function() {
+ // Legacy protocol doesn't support extra reconnect protocol.
+ // Trigger error handling for browser to shutdown all the resource locally.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ classID: Components.ID("{4027ce3d-06e3-4d06-a235-df329cb0d411}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel,
+ Ci.nsIStreamListener]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LegacyPresentationControlService]); //jshint ignore:line
diff --git a/dom/presentation/provider/LegacyProviders.manifest b/dom/presentation/provider/LegacyProviders.manifest
new file mode 100644
index 000000000..9408da063
--- /dev/null
+++ b/dom/presentation/provider/LegacyProviders.manifest
@@ -0,0 +1,2 @@
+component {b21816fe-8aff-4811-86d2-85a7444c557e} LegacyPresentationControlService.js
+contract @mozilla.org/presentation/legacy-control-service;1 {b21816fe-8aff-4811-86d2-85a7444c557e}
diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
new file mode 100644
index 000000000..0cab915ac
--- /dev/null
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ -0,0 +1,1249 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MulticastDNSDeviceProvider.h"
+
+#include "DeviceProviderHelpers.h"
+#include "MainThreadUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIObserverService.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTCPDeviceInfo.h"
+#include "nsThreadUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "nsIPropertyBag2.h"
+#endif // MOZ_WIDGET_ANDROID
+
+#define PREF_PRESENTATION_DISCOVERY "dom.presentation.discovery.enabled"
+#define PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS "dom.presentation.discovery.timeout_ms"
+#define PREF_PRESENTATION_DISCOVERABLE "dom.presentation.discoverable"
+#define PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED "dom.presentation.discoverable.encrypted"
+#define PREF_PRESENTATION_DISCOVERABLE_RETRY_MS "dom.presentation.discoverable.retry_ms"
+#define PREF_PRESENTATION_DEVICE_NAME "dom.presentation.device.name"
+
+#define SERVICE_TYPE "_presentation-ctrl._tcp"
+#define PROTOCOL_VERSION_TAG "version"
+#define CERT_FINGERPRINT_TAG "certFingerprint"
+
+static mozilla::LazyLogModule sMulticastDNSProviderLogModule("MulticastDNSDeviceProvider");
+
+#undef LOG_I
+#define LOG_I(...) MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#undef LOG_E
+#define LOG_E(...) MOZ_LOG(sMulticastDNSProviderLogModule, mozilla::LogLevel::Error, (__VA_ARGS__))
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+static const char* kObservedPrefs[] = {
+ PREF_PRESENTATION_DISCOVERY,
+ PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS,
+ PREF_PRESENTATION_DISCOVERABLE,
+ PREF_PRESENTATION_DEVICE_NAME,
+ nullptr
+};
+
+namespace {
+
+#ifdef MOZ_WIDGET_ANDROID
+static void
+GetAndroidDeviceName(nsACString& aRetVal)
+{
+ nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1");
+ MOZ_ASSERT(infoService, "Could not find a system info service");
+
+ Unused << NS_WARN_IF(NS_FAILED(infoService->GetPropertyAsACString(
+ NS_LITERAL_STRING("device"), aRetVal)));
+}
+#endif // MOZ_WIDGET_ANDROID
+
+} //anonymous namespace
+
+/**
+ * This wrapper is used to break circular-reference problem.
+ */
+class DNSServiceWrappedListener final
+ : public nsIDNSServiceDiscoveryListener
+ , public nsIDNSRegistrationListener
+ , public nsIDNSServiceResolveListener
+ , public nsIPresentationControlServerListener
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIDNSSERVICEDISCOVERYLISTENER(mListener)
+ NS_FORWARD_SAFE_NSIDNSREGISTRATIONLISTENER(mListener)
+ NS_FORWARD_SAFE_NSIDNSSERVICERESOLVELISTENER(mListener)
+ NS_FORWARD_SAFE_NSIPRESENTATIONCONTROLSERVERLISTENER(mListener)
+
+ explicit DNSServiceWrappedListener() = default;
+
+ nsresult SetListener(MulticastDNSDeviceProvider* aListener)
+ {
+ mListener = aListener;
+ return NS_OK;
+ }
+
+private:
+ virtual ~DNSServiceWrappedListener() = default;
+
+ MulticastDNSDeviceProvider* mListener = nullptr;
+};
+
+NS_IMPL_ISUPPORTS(DNSServiceWrappedListener,
+ nsIDNSServiceDiscoveryListener,
+ nsIDNSRegistrationListener,
+ nsIDNSServiceResolveListener,
+ nsIPresentationControlServerListener)
+
+NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider,
+ nsIPresentationDeviceProvider,
+ nsIDNSServiceDiscoveryListener,
+ nsIDNSRegistrationListener,
+ nsIDNSServiceResolveListener,
+ nsIPresentationControlServerListener,
+ nsIObserver)
+
+MulticastDNSDeviceProvider::~MulticastDNSDeviceProvider()
+{
+ Uninit();
+}
+
+nsresult
+MulticastDNSDeviceProvider::Init()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mInitialized) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ mMulticastDNS = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mWrappedListener = new DNSServiceWrappedListener();
+ if (NS_WARN_IF(NS_FAILED(rv = mWrappedListener->SetListener(this)))) {
+ return rv;
+ }
+
+ mPresentationService = do_CreateInstance(PRESENTATION_CONTROL_SERVICE_CONTACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mDiscoveryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mServerRetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ Preferences::AddStrongObservers(this, kObservedPrefs);
+
+ mDiscoveryEnabled = Preferences::GetBool(PREF_PRESENTATION_DISCOVERY);
+ mDiscoveryTimeoutMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS);
+ mDiscoverable = Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE);
+ mDiscoverableEncrypted = Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE_ENCRYPTED);
+ mServerRetryMs = Preferences::GetUint(PREF_PRESENTATION_DISCOVERABLE_RETRY_MS);
+ mServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME);
+
+#ifdef MOZ_WIDGET_ANDROID
+ // FIXME: Bug 1185806 - Provide a common device name setting.
+ if (mServiceName.IsEmpty()) {
+ GetAndroidDeviceName(mServiceName);
+ Unused << Preferences::SetCString(PREF_PRESENTATION_DEVICE_NAME, mServiceName);
+ }
+#endif // MOZ_WIDGET_ANDROID
+
+ Unused << mPresentationService->SetId(mServiceName);
+
+ if (mDiscoveryEnabled && NS_WARN_IF(NS_FAILED(rv = ForceDiscovery()))) {
+ return rv;
+ }
+
+ if (mDiscoverable && NS_WARN_IF(NS_FAILED(rv = StartServer()))) {
+ return rv;
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::Uninit()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mInitialized) {
+ return NS_OK;
+ }
+
+ ClearDevices();
+
+ Preferences::RemoveObservers(this, kObservedPrefs);
+
+ StopDiscovery(NS_OK);
+ StopServer();
+
+ mMulticastDNS = nullptr;
+
+ if (mWrappedListener) {
+ mWrappedListener->SetListener(nullptr);
+ mWrappedListener = nullptr;
+ }
+
+ mInitialized = false;
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::StartServer()
+{
+ LOG_I("StartServer: %s (%d)", mServiceName.get(), mDiscoverable);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDiscoverable) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ uint16_t servicePort;
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->GetPort(&servicePort)))) {
+ return rv;
+ }
+
+ /**
+ * If |servicePort| is non-zero, it means PresentationControlService is running.
+ * Otherwise, we should make it start serving.
+ */
+ if (servicePort) {
+ return RegisterMDNSService();
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->SetListener(mWrappedListener)))) {
+ return rv;
+ }
+
+ AbortServerRetry();
+
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->StartServer(mDiscoverableEncrypted, 0)))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::StopServer()
+{
+ LOG_I("StopServer: %s", mServiceName.get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ UnregisterMDNSService(NS_OK);
+
+ AbortServerRetry();
+
+ if (mPresentationService) {
+ mPresentationService->SetListener(nullptr);
+ mPresentationService->Close();
+ }
+
+ return NS_OK;
+}
+
+void
+MulticastDNSDeviceProvider::AbortServerRetry()
+{
+ if (mIsServerRetrying) {
+ mIsServerRetrying = false;
+ mServerRetryTimer->Cancel();
+ }
+}
+
+nsresult
+MulticastDNSDeviceProvider::RegisterMDNSService()
+{
+ LOG_I("RegisterMDNSService: %s", mServiceName.get());
+
+ if (!mDiscoverable) {
+ return NS_OK;
+ }
+
+ // Cancel on going service registration.
+ UnregisterMDNSService(NS_OK);
+
+ nsresult rv;
+
+ uint16_t servicePort;
+ if (NS_FAILED(rv = mPresentationService->GetPort(&servicePort)) ||
+ !servicePort) {
+ // Abort service registration if server port is not available.
+ return rv;
+ }
+
+ /**
+ * Register the presentation control channel server as an mDNS service.
+ */
+ nsCOMPtr<nsIDNSServiceInfo> serviceInfo =
+ do_CreateInstance(DNSSERVICEINFO_CONTRACT_ID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetServiceType(
+ NS_LITERAL_CSTRING(SERVICE_TYPE))))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetServiceName(mServiceName)))) {
+ return rv;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetPort(servicePort)))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+ MOZ_ASSERT(propBag);
+
+ uint32_t version;
+ rv = mPresentationService->GetVersion(&version);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = propBag->SetPropertyAsUint32(NS_LITERAL_STRING(PROTOCOL_VERSION_TAG),
+ version);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ if (mDiscoverableEncrypted) {
+ nsAutoCString certFingerprint;
+ rv = mPresentationService->GetCertFingerprint(certFingerprint);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = propBag->SetPropertyAsACString(NS_LITERAL_STRING(CERT_FINGERPRINT_TAG),
+ certFingerprint);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = serviceInfo->SetAttributes(propBag)))) {
+ return rv;
+ }
+
+ return mMulticastDNS->RegisterService(serviceInfo,
+ mWrappedListener,
+ getter_AddRefs(mRegisterRequest));
+}
+
+nsresult
+MulticastDNSDeviceProvider::UnregisterMDNSService(nsresult aReason)
+{
+ LOG_I("UnregisterMDNSService: %s (0x%08x)", mServiceName.get(), aReason);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mRegisterRequest) {
+ mRegisterRequest->Cancel(aReason);
+ mRegisterRequest = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::StopDiscovery(nsresult aReason)
+{
+ LOG_I("StopDiscovery (0x%08x)", aReason);
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (mDiscoveryRequest) {
+ mDiscoveryRequest->Cancel(aReason);
+ mDiscoveryRequest = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::Connect(Device* aDevice,
+ nsIPresentationControlChannel** aRetVal)
+{
+ MOZ_ASSERT(aDevice);
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<TCPDeviceInfo> deviceInfo = new TCPDeviceInfo(aDevice->Id(),
+ aDevice->Address(),
+ aDevice->Port(),
+ aDevice->CertFingerprint());
+
+ return mPresentationService->Connect(deviceInfo, aRetVal);
+}
+
+bool
+MulticastDNSDeviceProvider::IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(aServiceInfo);
+
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ if (NS_WARN_IF(NS_FAILED(
+ aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) || !propBag) {
+ return false;
+ }
+
+ uint32_t remoteVersion;
+ if (NS_WARN_IF(NS_FAILED(
+ propBag->GetPropertyAsUint32(NS_LITERAL_STRING(PROTOCOL_VERSION_TAG),
+ &remoteVersion)))) {
+ return false;
+ }
+
+ bool isCompatible = false;
+ Unused << NS_WARN_IF(NS_FAILED(
+ mPresentationService->IsCompatibleServer(remoteVersion,
+ &isCompatible)));
+
+ return isCompatible;
+}
+
+nsresult
+MulticastDNSDeviceProvider::AddDevice(const nsACString& aId,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ RefPtr<Device> device = new Device(aId, /* ID */
+ aServiceName,
+ aServiceType,
+ aAddress,
+ aPort,
+ aCertFingerprint,
+ DeviceState::eActive,
+ this);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->AddDevice(device);
+ }
+
+ mDevices.AppendElement(device);
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::UpdateDevice(const uint32_t aIndex,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+ device->Update(aServiceName, aServiceType, aAddress, aPort, aCertFingerprint);
+ device->ChangeState(DeviceState::eActive);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->UpdateDevice(device);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::RemoveDevice(const uint32_t aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPresentationService);
+
+ if (NS_WARN_IF(aIndex >= mDevices.Length())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Device> device = mDevices[aIndex];
+
+ LOG_I("RemoveDevice: %s", device->Id().get());
+ mDevices.RemoveElementAt(aIndex);
+
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->RemoveDevice(device);
+ }
+
+ return NS_OK;
+}
+
+bool
+MulticastDNSDeviceProvider::FindDeviceById(const nsACString& aId,
+ uint32_t& aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(aId,
+ /* aName = */ EmptyCString(),
+ /* aType = */ EmptyCString(),
+ /* aHost = */ EmptyCString(),
+ /* aPort = */ 0,
+ /* aCertFingerprint */ EmptyCString(),
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceIdComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+bool
+MulticastDNSDeviceProvider::FindDeviceByAddress(const nsACString& aAddress,
+ uint32_t& aIndex)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Device> device = new Device(/* aId = */ EmptyCString(),
+ /* aName = */ EmptyCString(),
+ /* aType = */ EmptyCString(),
+ aAddress,
+ /* aPort = */ 0,
+ /* aCertFingerprint */ EmptyCString(),
+ /* aState = */ DeviceState::eUnknown,
+ /* aProvider = */ nullptr);
+ size_t index = mDevices.IndexOf(device, 0, DeviceAddressComparator());
+
+ if (index == mDevices.NoIndex) {
+ return false;
+ }
+
+ aIndex = index;
+ return true;
+}
+
+void
+MulticastDNSDeviceProvider::MarkAllDevicesUnknown()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (auto& device : mDevices) {
+ device->ChangeState(DeviceState::eUnknown);
+ }
+}
+
+void
+MulticastDNSDeviceProvider::ClearUnknownDevices()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ if (mDevices[i]->State() == DeviceState::eUnknown) {
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+ }
+}
+
+void
+MulticastDNSDeviceProvider::ClearDevices()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t i = mDevices.Length();
+ while (i > 0) {
+ --i;
+ Unused << NS_WARN_IF(NS_FAILED(RemoveDevice(i)));
+ }
+}
+
+// nsIPresentationDeviceProvider
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::GetListener(nsIPresentationDeviceListener** aListener)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPresentationDeviceListener> listener =
+ do_QueryReferent(mDeviceListener, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ listener.forget(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::SetListener(nsIPresentationDeviceListener* aListener)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDeviceListener = do_GetWeakReference(aListener);
+
+ nsresult rv;
+ if (mDeviceListener) {
+ if (NS_WARN_IF(NS_FAILED(rv = Init()))) {
+ return rv;
+ }
+ } else {
+ if (NS_WARN_IF(NS_FAILED(rv = Uninit()))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::ForceDiscovery()
+{
+ LOG_I("ForceDiscovery (%d)", mDiscoveryEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDiscoveryEnabled) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mDiscoveryTimer);
+ MOZ_ASSERT(mMulticastDNS);
+
+ // if it's already discovering, extend existing discovery timeout.
+ nsresult rv;
+ if (mIsDiscovering) {
+ Unused << mDiscoveryTimer->Cancel();
+
+ if (NS_WARN_IF(NS_FAILED( rv = mDiscoveryTimer->Init(this,
+ mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ StopDiscovery(NS_OK);
+
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->StartDiscovery(
+ NS_LITERAL_CSTRING(SERVICE_TYPE),
+ mWrappedListener,
+ getter_AddRefs(mDiscoveryRequest))))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// nsIDNSServiceDiscoveryListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnDiscoveryStarted(const nsACString& aServiceType)
+{
+ LOG_I("OnDiscoveryStarted");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDiscoveryTimer);
+
+ MarkAllDevicesUnknown();
+
+ nsresult rv;
+ if (NS_WARN_IF(NS_FAILED(rv = mDiscoveryTimer->Init(this,
+ mDiscoveryTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT)))) {
+ return rv;
+ }
+
+ mIsDiscovering = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnDiscoveryStopped(const nsACString& aServiceType)
+{
+ LOG_I("OnDiscoveryStopped");
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ClearUnknownDevices();
+
+ mIsDiscovering = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceFound(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv ;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceFound: %s", serviceName.get());
+
+ if (mMulticastDNS) {
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService(
+ aServiceInfo, mWrappedListener)))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceLost(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceLost: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ uint32_t index;
+ if (!FindDeviceById(host, index)) {
+ // given device was not found
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv = RemoveDevice(index)))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnStartDiscoveryFailed(const nsACString& aServiceType,
+ int32_t aErrorCode)
+{
+ LOG_E("OnStartDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnStopDiscoveryFailed(const nsACString& aServiceType,
+ int32_t aErrorCode)
+{
+ LOG_E("OnStopDiscoveryFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIDNSRegistrationListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceRegistered(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv;
+
+ nsAutoCString name;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(name)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceRegistered (%s)", name.get());
+ mRegisteredName = name;
+
+ if (mMulticastDNS) {
+ if (NS_WARN_IF(NS_FAILED(rv = mMulticastDNS->ResolveService(
+ aServiceInfo, mWrappedListener)))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceUnregistered(nsIDNSServiceInfo* aServiceInfo)
+{
+ LOG_I("OnServiceUnregistered");
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnRegistrationFailed(nsIDNSServiceInfo* aServiceInfo,
+ int32_t aErrorCode)
+{
+ LOG_E("OnRegistrationFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mRegisterRequest = nullptr;
+
+ if (aErrorCode == nsIDNSRegistrationListener::ERROR_SERVICE_NOT_RUNNING) {
+ return NS_DispatchToMainThread(
+ NewRunnableMethod(this, &MulticastDNSDeviceProvider::RegisterMDNSService));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnUnregistrationFailed(nsIDNSServiceInfo* aServiceInfo,
+ int32_t aErrorCode)
+{
+ LOG_E("OnUnregistrationFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIDNSServiceResolveListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aServiceInfo)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+
+ nsAutoCString serviceName;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceName(serviceName)))) {
+ return rv;
+ }
+
+ LOG_I("OnServiceResolved: %s", serviceName.get());
+
+ nsAutoCString host;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetHost(host)))) {
+ return rv;
+ }
+
+ if (mRegisteredName == serviceName) {
+ LOG_I("ignore self");
+
+ if (NS_WARN_IF(NS_FAILED(rv = mPresentationService->SetId(host)))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ if (!IsCompatibleServer(aServiceInfo)) {
+ LOG_I("ignore incompatible service: %s", serviceName.get());
+ return NS_OK;
+ }
+
+ nsAutoCString address;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetAddress(address)))) {
+ return rv;
+ }
+
+ uint16_t port;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetPort(&port)))) {
+ return rv;
+ }
+
+ nsAutoCString serviceType;
+ if (NS_WARN_IF(NS_FAILED(rv = aServiceInfo->GetServiceType(serviceType)))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ if (NS_WARN_IF(NS_FAILED(
+ aServiceInfo->GetAttributes(getter_AddRefs(propBag)))) || !propBag) {
+ return rv;
+ }
+
+ nsAutoCString certFingerprint;
+ Unused << propBag->GetPropertyAsACString(NS_LITERAL_STRING(CERT_FINGERPRINT_TAG),
+ certFingerprint);
+
+ uint32_t index;
+ if (FindDeviceById(host, index)) {
+ return UpdateDevice(index,
+ serviceName,
+ serviceType,
+ address,
+ port,
+ certFingerprint);
+ } else {
+ return AddDevice(host,
+ serviceName,
+ serviceType,
+ address,
+ port,
+ certFingerprint);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo,
+ int32_t aErrorCode)
+{
+ LOG_E("OnResolveFailed: %d", aErrorCode);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return NS_OK;
+}
+
+// nsIPresentationControlServerListener
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServerReady(uint16_t aPort,
+ const nsACString& aCertFingerprint)
+{
+ LOG_I("OnServerReady: %d, %s", aPort, PromiseFlatCString(aCertFingerprint).get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mDiscoverable) {
+ RegisterMDNSService();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnServerStopped(nsresult aResult)
+{
+ LOG_I("OnServerStopped: (0x%08x)", aResult);
+
+ UnregisterMDNSService(aResult);
+
+ // Try restart server if it is stopped abnormally.
+ if (NS_FAILED(aResult) && mDiscoverable) {
+ mIsServerRetrying = true;
+ mServerRetryTimer->Init(this, mServerRetryMs, nsITimer::TYPE_ONE_SHOT);
+ }
+
+ return NS_OK;
+}
+
+// Create a new device if we were unable to find one with the address.
+already_AddRefed<MulticastDNSDeviceProvider::Device>
+MulticastDNSDeviceProvider::GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo)
+{
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ RefPtr<Device> device;
+ uint32_t index;
+ if (FindDeviceByAddress(address, index)) {
+ device = mDevices[index];
+ } else {
+ // Create a one-time device object for non-discoverable controller.
+ // This device will not be in the list of available devices and cannot
+ // be used for requesting session.
+ nsAutoCString id;
+ Unused << aDeviceInfo->GetId(id);
+ uint16_t port;
+ Unused << aDeviceInfo->GetPort(&port);
+
+ device = new Device(id,
+ /* aName = */ id,
+ /* aType = */ EmptyCString(),
+ address,
+ port,
+ /* aCertFingerprint */ EmptyCString(),
+ DeviceState::eActive,
+ /* aProvider = */ nullptr);
+ }
+
+ return device.forget();
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnSessionRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnSessionRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnSessionRequest(device, aUrl, aPresentationId,
+ aControlChannel);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel,
+ bool aIsFromReceiver)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnTerminateRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnTerminateRequest(device, aPresentationId,
+ aControlChannel, aIsFromReceiver);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnReconnectRequest(nsITCPDeviceInfo* aDeviceInfo,
+ const nsAString& aUrl,
+ const nsAString& aPresentationId,
+ nsIPresentationControlChannel* aControlChannel)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString address;
+ Unused << aDeviceInfo->GetAddress(address);
+
+ LOG_I("OnReconnectRequest: %s", address.get());
+
+ RefPtr<Device> device = GetOrCreateDevice(aDeviceInfo);
+ nsCOMPtr<nsIPresentationDeviceListener> listener;
+ if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+ Unused << listener->OnReconnectRequest(device, aUrl, aPresentationId,
+ aControlChannel);
+ }
+
+ return NS_OK;
+}
+
+// nsIObserver
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ConvertUTF16toUTF8 data(aData);
+ LOG_I("Observe: topic = %s, data = %s", aTopic, data.get());
+
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY)) {
+ OnDiscoveryChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERY));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS)) {
+ OnDiscoveryTimeoutChanged(Preferences::GetUint(PREF_PRESENTATION_DISCOVERY_TIMEOUT_MS));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DISCOVERABLE)) {
+ OnDiscoverableChanged(Preferences::GetBool(PREF_PRESENTATION_DISCOVERABLE));
+ } else if (data.EqualsLiteral(PREF_PRESENTATION_DEVICE_NAME)) {
+ nsAdoptingCString newServiceName = Preferences::GetCString(PREF_PRESENTATION_DEVICE_NAME);
+ if (!mServiceName.Equals(newServiceName)) {
+ OnServiceNameChanged(newServiceName);
+ }
+ }
+ } else if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
+ nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject);
+ if (!timer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (timer == mDiscoveryTimer) {
+ StopDiscovery(NS_OK);
+ } else if (timer == mServerRetryTimer) {
+ mIsServerRetrying = false;
+ StartServer();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::OnDiscoveryChanged(bool aEnabled)
+{
+ LOG_I("DiscoveryEnabled = %d\n", aEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryEnabled = aEnabled;
+
+ if (mDiscoveryEnabled) {
+ return ForceDiscovery();
+ }
+
+ return StopDiscovery(NS_OK);
+}
+
+nsresult
+MulticastDNSDeviceProvider::OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs)
+{
+ LOG_I("OnDiscoveryTimeoutChanged = %d\n", aTimeoutMs);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoveryTimeoutMs = aTimeoutMs;
+
+ return NS_OK;
+}
+
+nsresult
+MulticastDNSDeviceProvider::OnDiscoverableChanged(bool aEnabled)
+{
+ LOG_I("Discoverable = %d\n", aEnabled);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDiscoverable = aEnabled;
+
+ if (mDiscoverable) {
+ return StartServer();
+ }
+
+ return StopServer();
+}
+
+nsresult
+MulticastDNSDeviceProvider::OnServiceNameChanged(const nsACString& aServiceName)
+{
+ LOG_I("serviceName = %s\n", PromiseFlatCString(aServiceName).get());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mServiceName = aServiceName;
+
+ nsresult rv;
+ if (NS_WARN_IF(NS_FAILED(rv = UnregisterMDNSService(NS_OK)))) {
+ return rv;
+ }
+
+ if (mDiscoverable) {
+ return RegisterMDNSService();
+ }
+
+ return NS_OK;
+}
+
+// MulticastDNSDeviceProvider::Device
+NS_IMPL_ISUPPORTS(MulticastDNSDeviceProvider::Device,
+ nsIPresentationDevice)
+
+// nsIPresentationDevice
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetId(nsACString& aId)
+{
+ aId = mId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetName(nsACString& aName)
+{
+ aName = mName;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::GetType(nsACString& aType)
+{
+ aType = mType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::EstablishControlChannel(
+ nsIPresentationControlChannel** aRetVal)
+{
+ if (!mProvider) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return mProvider->Connect(this, aRetVal);
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::Disconnect()
+{
+ // No need to do anything when disconnect.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::Device::IsRequestedUrlSupported(
+ const nsAString& aRequestedUrl,
+ bool* aRetVal)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aRetVal) {
+ return NS_ERROR_INVALID_POINTER;
+ }
+
+ // TV 2.6 also supports presentation Apps and HTTP/HTTPS hosted receiver page.
+ if (DeviceProviderHelpers::IsFxTVSupportedAppUrl(aRequestedUrl) ||
+ DeviceProviderHelpers::IsCommonlySupportedScheme(aRequestedUrl)) {
+ *aRetVal = true;
+ }
+
+ return NS_OK;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/presentation/provider/MulticastDNSDeviceProvider.h b/dom/presentation/provider/MulticastDNSDeviceProvider.h
new file mode 100644
index 000000000..c6a91b3d8
--- /dev/null
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.h
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h
+#define mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsICancelable.h"
+#include "nsIDNSServiceDiscovery.h"
+#include "nsIObserver.h"
+#include "nsIPresentationDevice.h"
+#include "nsIPresentationDeviceProvider.h"
+#include "nsIPresentationControlService.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakPtr.h"
+
+class nsITCPDeviceInfo;
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class DNSServiceWrappedListener;
+class MulticastDNSService;
+
+class MulticastDNSDeviceProvider final
+ : public nsIPresentationDeviceProvider
+ , public nsIDNSServiceDiscoveryListener
+ , public nsIDNSRegistrationListener
+ , public nsIDNSServiceResolveListener
+ , public nsIPresentationControlServerListener
+ , public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICEPROVIDER
+ NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER
+ NS_DECL_NSIDNSREGISTRATIONLISTENER
+ NS_DECL_NSIDNSSERVICERESOLVELISTENER
+ NS_DECL_NSIPRESENTATIONCONTROLSERVERLISTENER
+ NS_DECL_NSIOBSERVER
+
+ explicit MulticastDNSDeviceProvider() = default;
+ nsresult Init();
+ nsresult Uninit();
+
+private:
+ enum class DeviceState : uint32_t {
+ eUnknown,
+ eActive
+ };
+
+ class Device final : public nsIPresentationDevice
+ {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRESENTATIONDEVICE
+
+ explicit Device(const nsACString& aId,
+ const nsACString& aName,
+ const nsACString& aType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint,
+ DeviceState aState,
+ MulticastDNSDeviceProvider* aProvider)
+ : mId(aId)
+ , mName(aName)
+ , mType(aType)
+ , mAddress(aAddress)
+ , mPort(aPort)
+ , mCertFingerprint(aCertFingerprint)
+ , mState(aState)
+ , mProvider(aProvider)
+ {
+ }
+
+ const nsCString& Id() const
+ {
+ return mId;
+ }
+
+ const nsCString& Address() const
+ {
+ return mAddress;
+ }
+
+ uint16_t Port() const
+ {
+ return mPort;
+ }
+
+ const nsCString& CertFingerprint() const
+ {
+ return mCertFingerprint;
+ }
+
+ DeviceState State() const
+ {
+ return mState;
+ }
+
+ void ChangeState(DeviceState aState)
+ {
+ mState = aState;
+ }
+
+ void Update(const nsACString& aName,
+ const nsACString& aType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint)
+ {
+ mName = aName;
+ mType = aType;
+ mAddress = aAddress;
+ mPort = aPort;
+ mCertFingerprint = aCertFingerprint;
+ }
+
+ private:
+ virtual ~Device() = default;
+
+ nsCString mId;
+ nsCString mName;
+ nsCString mType;
+ nsCString mAddress;
+ uint16_t mPort;
+ nsCString mCertFingerprint;
+ DeviceState mState;
+ MulticastDNSDeviceProvider* mProvider;
+ };
+
+ struct DeviceIdComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Id() == aB->Id();
+ }
+ };
+
+ struct DeviceAddressComparator {
+ bool Equals(const RefPtr<Device>& aA, const RefPtr<Device>& aB) const {
+ return aA->Address() == aB->Address();
+ }
+ };
+
+ virtual ~MulticastDNSDeviceProvider();
+ nsresult StartServer();
+ nsresult StopServer();
+ void AbortServerRetry();
+ nsresult RegisterMDNSService();
+ nsresult UnregisterMDNSService(nsresult aReason);
+ nsresult StopDiscovery(nsresult aReason);
+ nsresult Connect(Device* aDevice,
+ nsIPresentationControlChannel** aRetVal);
+ bool IsCompatibleServer(nsIDNSServiceInfo* aServiceInfo);
+
+ // device manipulation
+ nsresult AddDevice(const nsACString& aId,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint);
+ nsresult UpdateDevice(const uint32_t aIndex,
+ const nsACString& aServiceName,
+ const nsACString& aServiceType,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint);
+ nsresult RemoveDevice(const uint32_t aIndex);
+ bool FindDeviceById(const nsACString& aId,
+ uint32_t& aIndex);
+
+ bool FindDeviceByAddress(const nsACString& aAddress,
+ uint32_t& aIndex);
+
+ already_AddRefed<Device>
+ GetOrCreateDevice(nsITCPDeviceInfo* aDeviceInfo);
+
+ void MarkAllDevicesUnknown();
+ void ClearUnknownDevices();
+ void ClearDevices();
+
+ // preferences
+ nsresult OnDiscoveryChanged(bool aEnabled);
+ nsresult OnDiscoveryTimeoutChanged(uint32_t aTimeoutMs);
+ nsresult OnDiscoverableChanged(bool aEnabled);
+ nsresult OnServiceNameChanged(const nsACString& aServiceName);
+
+ bool mInitialized = false;
+ nsWeakPtr mDeviceListener;
+ nsCOMPtr<nsIPresentationControlService> mPresentationService;
+ nsCOMPtr<nsIDNSServiceDiscovery> mMulticastDNS;
+ RefPtr<DNSServiceWrappedListener> mWrappedListener;
+
+ nsCOMPtr<nsICancelable> mDiscoveryRequest;
+ nsCOMPtr<nsICancelable> mRegisterRequest;
+
+ nsTArray<RefPtr<Device>> mDevices;
+
+ bool mDiscoveryEnabled = false;
+ bool mIsDiscovering = false;
+ uint32_t mDiscoveryTimeoutMs;
+ nsCOMPtr<nsITimer> mDiscoveryTimer;
+
+ bool mDiscoverable = false;
+ bool mDiscoverableEncrypted = false;
+ bool mIsServerRetrying = false;
+ uint32_t mServerRetryMs;
+ nsCOMPtr<nsITimer> mServerRetryTimer;
+
+ nsCString mServiceName;
+ nsCString mRegisteredName;
+};
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_presentation_provider_MulticastDNSDeviceProvider_h
diff --git a/dom/presentation/provider/PresentationControlService.js b/dom/presentation/provider/PresentationControlService.js
new file mode 100644
index 000000000..fe61d26d6
--- /dev/null
+++ b/dom/presentation/provider/PresentationControlService.js
@@ -0,0 +1,961 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* globals XPCOMUtils */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+/* globals Services */
+Cu.import("resource://gre/modules/Services.jsm");
+/* globals NetUtil */
+Cu.import("resource://gre/modules/NetUtil.jsm");
+/* globals setTimeout, clearTimeout */
+Cu.import("resource://gre/modules/Timer.jsm");
+
+/* globals ControllerStateMachine */
+XPCOMUtils.defineLazyModuleGetter(this, "ControllerStateMachine", // jshint ignore:line
+ "resource://gre/modules/presentation/ControllerStateMachine.jsm");
+/* global ReceiverStateMachine */
+XPCOMUtils.defineLazyModuleGetter(this, "ReceiverStateMachine", // jshint ignore:line
+ "resource://gre/modules/presentation/ReceiverStateMachine.jsm");
+
+const kProtocolVersion = 1; // need to review isCompatibleServer while fiddling the version number.
+const kLocalCertName = "presentation";
+
+const DEBUG = Services.prefs.getBoolPref("dom.presentation.tcp_server.debug");
+function log(aMsg) {
+ dump("-*- PresentationControlService.js: " + aMsg + "\n");
+}
+
+function TCPDeviceInfo(aAddress, aPort, aId, aCertFingerprint) {
+ this.address = aAddress;
+ this.port = aPort;
+ this.id = aId;
+ this.certFingerprint = aCertFingerprint || "";
+}
+
+function PresentationControlService() {
+ this._id = null;
+ this._port = 0;
+ this._serverSocket = null;
+}
+
+PresentationControlService.prototype = {
+ /**
+ * If a user agent connects to this server, we create a control channel but
+ * hand it to |TCPDevice.listener| when the initial information exchange
+ * finishes. Therefore, we hold the control channels in this period.
+ */
+ _controlChannels: [],
+
+ startServer: function(aEncrypted, aPort) {
+ if (this._isServiceInit()) {
+ DEBUG && log("PresentationControlService - server socket has been initialized"); // jshint ignore:line
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ /**
+ * 0 or undefined indicates opt-out parameter, and a port will be selected
+ * automatically.
+ */
+ let serverSocketPort = (typeof aPort !== "undefined" && aPort !== 0) ? aPort : -1;
+
+ if (aEncrypted) {
+ let self = this;
+ let localCertService = Cc["@mozilla.org/security/local-cert-service;1"]
+ .getService(Ci.nsILocalCertService);
+ localCertService.getOrCreateCert(kLocalCertName, {
+ handleCert: function(aCert, aRv) {
+ DEBUG && log("PresentationControlService - handleCert"); // jshint ignore:line
+ if (aRv) {
+ self._notifyServerStopped(aRv);
+ } else {
+ self._serverSocket = Cc["@mozilla.org/network/tls-server-socket;1"]
+ .createInstance(Ci.nsITLSServerSocket);
+
+ self._serverSocketInit(serverSocketPort, aCert);
+ }
+ }
+ });
+ } else {
+ this._serverSocket = Cc["@mozilla.org/network/server-socket;1"]
+ .createInstance(Ci.nsIServerSocket);
+
+ this._serverSocketInit(serverSocketPort, null);
+ }
+ },
+
+ _serverSocketInit: function(aPort, aCert) {
+ if (!this._serverSocket) {
+ DEBUG && log("PresentationControlService - create server socket fail."); // jshint ignore:line
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ try {
+ this._serverSocket.init(aPort, false, -1);
+
+ if (aCert) {
+ this._serverSocket.serverCert = aCert;
+ this._serverSocket.setSessionCache(false);
+ this._serverSocket.setSessionTickets(false);
+ let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+ this._serverSocket.setRequestClientCertificate(requestCert);
+ }
+
+ this._serverSocket.asyncListen(this);
+ } catch (e) {
+ // NS_ERROR_SOCKET_ADDRESS_IN_USE
+ DEBUG && log("PresentationControlService - init server socket fail: " + e); // jshint ignore:line
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ this._port = this._serverSocket.port;
+
+ DEBUG && log("PresentationControlService - service start on port: " + this._port); // jshint ignore:line
+
+ // Monitor network interface change to restart server socket.
+ Services.obs.addObserver(this, "network:offline-status-changed", false);
+
+ this._notifyServerReady();
+ },
+
+ _notifyServerReady: function() {
+ Services.tm.mainThread.dispatch(() => {
+ if (this._listener) {
+ this._listener.onServerReady(this._port, this.certFingerprint);
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ _notifyServerStopped: function(aRv) {
+ Services.tm.mainThread.dispatch(() => {
+ if (this._listener) {
+ this._listener.onServerStopped(aRv);
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ isCompatibleServer: function(aVersion) {
+ // No compatibility issue for the first version of control protocol
+ return this.version === aVersion;
+ },
+
+ get id() {
+ return this._id;
+ },
+
+ set id(aId) {
+ this._id = aId;
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ get version() {
+ return kProtocolVersion;
+ },
+
+ get certFingerprint() {
+ if (!this._serverSocket.serverCert) {
+ return null;
+ }
+
+ return this._serverSocket.serverCert.sha256Fingerprint;
+ },
+
+ set listener(aListener) {
+ this._listener = aListener;
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ _isServiceInit: function() {
+ return this._serverSocket !== null;
+ },
+
+ connect: function(aDeviceInfo) {
+ if (!this.id) {
+ DEBUG && log("PresentationControlService - Id has not initialized; connect fails"); // jshint ignore:line
+ return null;
+ }
+ DEBUG && log("PresentationControlService - connect to " + aDeviceInfo.id); // jshint ignore:line
+
+ let socketTransport = this._attemptConnect(aDeviceInfo);
+ return new TCPControlChannel(this,
+ socketTransport,
+ aDeviceInfo,
+ "sender");
+ },
+
+ _attemptConnect: function(aDeviceInfo) {
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsISocketTransportService);
+
+ let socketTransport;
+ try {
+ if (aDeviceInfo.certFingerprint) {
+ let overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+ overrideService.rememberTemporaryValidityOverrideUsingFingerprint(
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ aDeviceInfo.certFingerprint,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED | Ci.nsICertOverrideService.ERROR_MISMATCH);
+
+ socketTransport = sts.createTransport(["ssl"],
+ 1,
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ null);
+ } else {
+ socketTransport = sts.createTransport(null,
+ 0,
+ aDeviceInfo.address,
+ aDeviceInfo.port,
+ null);
+ }
+ // Shorten the connection failure procedure.
+ socketTransport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
+ } catch (e) {
+ DEBUG && log("PresentationControlService - createTransport throws: " + e); // jshint ignore:line
+ // Pop the exception to |TCPDevice.establishControlChannel|
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ return socketTransport;
+ },
+
+ responseSession: function(aDeviceInfo, aSocketTransport) {
+ if (!this._isServiceInit()) {
+ DEBUG && log("PresentationControlService - should never receive remote " +
+ "session request before server socket initialization"); // jshint ignore:line
+ return null;
+ }
+ DEBUG && log("PresentationControlService - responseSession to " +
+ JSON.stringify(aDeviceInfo)); // jshint ignore:line
+ return new TCPControlChannel(this,
+ aSocketTransport,
+ aDeviceInfo,
+ "receiver");
+ },
+
+ // Triggered by TCPControlChannel
+ onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
+ DEBUG && log("PresentationControlService - onSessionRequest: " +
+ aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onSessionRequest(aDeviceInfo,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) {
+ DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
+ aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onTerminateRequest(aDeviceInfo,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver);
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ onSessionReconnect: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
+ DEBUG && log("TCPPresentationServer - onSessionReconnect: " +
+ aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+ if (!this.listener) {
+ this.releaseControlChannel(aControlChannel);
+ return;
+ }
+
+ this.listener.onReconnectRequest(aDeviceInfo,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ this.releaseControlChannel(aControlChannel);
+ },
+
+ // nsIServerSocketListener (Triggered by nsIServerSocket.init)
+ onSocketAccepted: function(aServerSocket, aClientSocket) {
+ DEBUG && log("PresentationControlService - onSocketAccepted: " +
+ aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line
+ let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
+ this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket));
+ },
+
+ holdControlChannel: function(aControlChannel) {
+ this._controlChannels.push(aControlChannel);
+ },
+
+ releaseControlChannel: function(aControlChannel) {
+ let index = this._controlChannels.indexOf(aControlChannel);
+ if (index !== -1) {
+ delete this._controlChannels[index];
+ }
+ },
+
+ // nsIServerSocketListener (Triggered by nsIServerSocket.init)
+ onStopListening: function(aServerSocket, aStatus) {
+ DEBUG && log("PresentationControlService - onStopListening: " + aStatus); // jshint ignore:line
+ },
+
+ close: function() {
+ DEBUG && log("PresentationControlService - close"); // jshint ignore:line
+ if (this._isServiceInit()) {
+ DEBUG && log("PresentationControlService - close server socket"); // jshint ignore:line
+ this._serverSocket.close();
+ this._serverSocket = null;
+
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+
+ this._notifyServerStopped(Cr.NS_OK);
+ }
+ this._port = 0;
+ },
+
+ // nsIObserver
+ observe: function(aSubject, aTopic, aData) {
+ DEBUG && log("PresentationControlService - observe: " + aTopic); // jshint ignore:line
+ switch (aTopic) {
+ case "network:offline-status-changed": {
+ if (aData == "offline") {
+ DEBUG && log("network offline"); // jshint ignore:line
+ return;
+ }
+ this._restartServer();
+ break;
+ }
+ }
+ },
+
+ _restartServer: function() {
+ DEBUG && log("PresentationControlService - restart service"); // jshint ignore:line
+
+ // restart server socket
+ if (this._isServiceInit()) {
+ this.close();
+
+ try {
+ this.startServer();
+ } catch (e) {
+ DEBUG && log("PresentationControlService - restart service fail: " + e); // jshint ignore:line
+ }
+ }
+ },
+
+ classID: Components.ID("{f4079b8b-ede5-4b90-a112-5b415a931deb}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIServerSocketListener,
+ Ci.nsIPresentationControlService,
+ Ci.nsIObserver]),
+};
+
+function ChannelDescription(aInit) {
+ this._type = aInit.type;
+ switch (this._type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ this._tcpAddresses = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ for (let address of aInit.tcpAddress) {
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"]
+ .createInstance(Ci.nsISupportsCString);
+ wrapper.data = address;
+ this._tcpAddresses.appendElement(wrapper, false);
+ }
+
+ this._tcpPort = aInit.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ this._dataChannelSDP = aInit.dataChannelSDP;
+ break;
+ }
+}
+
+ChannelDescription.prototype = {
+ _type: 0,
+ _tcpAddresses: null,
+ _tcpPort: 0,
+ _dataChannelSDP: "",
+
+ get type() {
+ return this._type;
+ },
+
+ get tcpAddress() {
+ return this._tcpAddresses;
+ },
+
+ get tcpPort() {
+ return this._tcpPort;
+ },
+
+ get dataChannelSDP() {
+ return this._dataChannelSDP;
+ },
+
+ classID: Components.ID("{82507aea-78a2-487e-904a-858a6c5bf4e1}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+};
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function discriptionAsJson(aDescription) {
+ let json = {};
+ json.type = aDescription.type;
+ switch(aDescription.type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+ json.tcpAddress = [];
+ for (let idx = 0; idx < addresses.length; idx++) {
+ let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+ json.tcpAddress.push(address.data);
+ }
+ json.tcpPort = aDescription.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ json.dataChannelSDP = aDescription.dataChannelSDP;
+ break;
+ }
+ return json;
+}
+
+const kDisconnectTimeout = 5000;
+const kTerminateTimeout = 5000;
+
+function TCPControlChannel(presentationService,
+ transport,
+ deviceInfo,
+ direction) {
+ DEBUG && log("create TCPControlChannel for : " + direction); // jshint ignore:line
+ this._deviceInfo = deviceInfo;
+ this._direction = direction;
+ this._transport = transport;
+
+ this._presentationService = presentationService;
+
+ if (direction === "receiver") {
+ // Need to set security observer before I/O stream operation.
+ this._setSecurityObserver(this);
+ }
+
+ let currentThread = Services.tm.currentThread;
+ transport.setEventSink(this, currentThread);
+
+ this._input = this._transport.openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this._input.asyncWait(this.QueryInterface(Ci.nsIStreamListener),
+ Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY,
+ 0,
+ currentThread);
+
+ this._output = this._transport
+ .openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED, 0, 0)
+ .QueryInterface(Ci.nsIAsyncOutputStream);
+
+ this._outgoingMsgs = [];
+
+
+ this._stateMachine =
+ (direction === "sender") ? new ControllerStateMachine(this, presentationService.id)
+ : new ReceiverStateMachine(this);
+
+ if (direction === "receiver" && !transport.securityInfo) {
+ // Since the transport created by server socket is already CONNECTED_TO.
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ }
+}
+
+TCPControlChannel.prototype = {
+ _outgoingEnabled: false,
+ _incomingEnabled: false,
+ _pendingOpen: false,
+ _pendingOffer: null,
+ _pendingAnswer: null,
+ _pendingClose: null,
+ _pendingCloseReason: null,
+ _pendingReconnect: false,
+
+ sendOffer: function(aOffer) {
+ this._stateMachine.sendOffer(discriptionAsJson(aOffer));
+ },
+
+ sendAnswer: function(aAnswer) {
+ this._stateMachine.sendAnswer(discriptionAsJson(aAnswer));
+ },
+
+ sendIceCandidate: function(aCandidate) {
+ this._stateMachine.updateIceCandidate(aCandidate);
+ },
+
+ launch: function(aPresentationId, aUrl) {
+ this._stateMachine.launch(aPresentationId, aUrl);
+ },
+
+ terminate: function(aPresentationId) {
+ if (!this._terminatingId) {
+ this._terminatingId = aPresentationId;
+ this._stateMachine.terminate(aPresentationId);
+
+ // Start a guard timer to ensure terminateAck is processed.
+ this._terminateTimer = setTimeout(() => {
+ DEBUG && log("TCPControlChannel - terminate timeout: " + aPresentationId); // jshint ignore:line
+ delete this._terminateTimer;
+ if (this._pendingDisconnect) {
+ this._pendingDisconnect();
+ } else {
+ this.disconnect(Cr.NS_OK);
+ }
+ }, kTerminateTimeout);
+ } else {
+ this._stateMachine.terminateAck(aPresentationId);
+ delete this._terminatingId;
+ }
+ },
+
+ _flushOutgoing: function() {
+ if (!this._outgoingEnabled || this._outgoingMsgs.length === 0) {
+ return;
+ }
+
+ this._output.asyncWait(this, 0, 0, Services.tm.currentThread);
+ },
+
+ // may throw an exception
+ _send: function(aMsg) {
+ DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); // jshint ignore:line
+
+ /**
+ * XXX In TCP streaming, it is possible that more than one message in one
+ * TCP packet. We use line delimited JSON to identify where one JSON encoded
+ * object ends and the next begins. Therefore, we do not allow newline
+ * characters whithin the whole message, and add a newline at the end.
+ * Please see the parser code in |onDataAvailable|.
+ */
+ let message = JSON.stringify(aMsg).replace(["\n"], "") + "\n";
+ try {
+ this._output.write(message, message.length);
+ } catch(e) {
+ DEBUG && log("TCPControlChannel - Failed to send message: " + e.name); // jshint ignore:line
+ throw e;
+ }
+ },
+
+ _setSecurityObserver: function(observer) {
+ if (this._transport && this._transport.securityInfo) {
+ DEBUG && log("TCPControlChannel - setSecurityObserver: " + observer); // jshint ignore:line
+ let connectionInfo = this._transport.securityInfo
+ .QueryInterface(Ci.nsITLSServerConnectionInfo);
+ connectionInfo.setSecurityObserver(observer);
+ }
+ },
+
+ // nsITLSServerSecurityObserver
+ onHandshakeDone: function(socket, clientStatus) {
+ log("TCPControlChannel - onHandshakeDone: TLS version: " + clientStatus.tlsVersionUsed.toString(16));
+ this._setSecurityObserver(null);
+
+ // Process input/output after TLS handshake is complete.
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ },
+
+ // nsIAsyncOutputStream
+ onOutputStreamReady: function() {
+ DEBUG && log("TCPControlChannel - onOutputStreamReady"); // jshint ignore:line
+ if (this._outgoingMsgs.length === 0) {
+ return;
+ }
+
+ try {
+ this._send(this._outgoingMsgs[0]);
+ } catch (e) {
+ if (e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ this._output.asyncWait(this, 0, 0, Services.tm.currentThread);
+ return;
+ }
+
+ this._closeTransport();
+ return;
+ }
+ this._outgoingMsgs.shift();
+ this._flushOutgoing();
+ },
+
+ // nsIAsyncInputStream (Triggered by nsIInputStream.asyncWait)
+ // Only used for detecting connection refused
+ onInputStreamReady: function(aStream) {
+ DEBUG && log("TCPControlChannel - onInputStreamReady"); // jshint ignore:line
+ try {
+ aStream.available();
+ } catch (e) {
+ DEBUG && log("TCPControlChannel - onInputStreamReady error: " + e.name); // jshint ignore:line
+ // NS_ERROR_CONNECTION_REFUSED
+ this._notifyDisconnected(e.result);
+ }
+ },
+
+ // nsITransportEventSink (Triggered by nsISocketTransport.setEventSink)
+ onTransportStatus: function(aTransport, aStatus) {
+ DEBUG && log("TCPControlChannel - onTransportStatus: " + aStatus.toString(16) +
+ " with role: " + this._direction); // jshint ignore:line
+ if (aStatus === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ this._outgoingEnabled = true;
+ this._createInputStreamPump();
+ }
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStartRequest: function() {
+ DEBUG && log("TCPControlChannel - onStartRequest with role: " +
+ this._direction); // jshint ignore:line
+ this._incomingEnabled = true;
+ },
+
+ // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
+ onStopRequest: function(aRequest, aContext, aStatus) {
+ DEBUG && log("TCPControlChannel - onStopRequest: " + aStatus +
+ " with role: " + this._direction); // jshint ignore:line
+ this._stateMachine.onChannelClosed(aStatus, true);
+ },
+
+ // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead)
+ onDataAvailable: function(aRequest, aContext, aInputStream) {
+ let data = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ DEBUG && log("TCPControlChannel - onDataAvailable: " + data); // jshint ignore:line
+
+ // Parser of line delimited JSON. Please see |_send| for more informaiton.
+ let jsonArray = data.split("\n");
+ jsonArray.pop();
+ for (let json of jsonArray) {
+ let msg;
+ try {
+ msg = JSON.parse(json);
+ } catch (e) {
+ DEBUG && log("TCPSignalingChannel - error in parsing json: " + e); // jshint ignore:line
+ }
+
+ this._handleMessage(msg);
+ }
+ },
+
+ _createInputStreamPump: function() {
+ if (this._pump) {
+ return;
+ }
+
+ DEBUG && log("TCPControlChannel - create pump with role: " +
+ this._direction); // jshint ignore:line
+ this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].
+ createInstance(Ci.nsIInputStreamPump);
+ this._pump.init(this._input, -1, -1, 0, 0, false);
+ this._pump.asyncRead(this, null);
+ this._stateMachine.onChannelReady();
+ },
+
+ // Handle command from remote side
+ _handleMessage: function(aMsg) {
+ DEBUG && log("TCPControlChannel - handleMessage from " +
+ JSON.stringify(this._deviceInfo) + ": " + JSON.stringify(aMsg)); // jshint ignore:line
+ this._stateMachine.onCommand(aMsg);
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ set listener(aListener) {
+ DEBUG && log("TCPControlChannel - set listener: " + aListener); // jshint ignore:line
+ if (!aListener) {
+ this._listener = null;
+ return;
+ }
+
+ this._listener = aListener;
+ if (this._pendingOpen) {
+ this._pendingOpen = false;
+ DEBUG && log("TCPControlChannel - notify pending opened"); // jshint ignore:line
+ this._listener.notifyConnected();
+ }
+
+ if (this._pendingOffer) {
+ let offer = this._pendingOffer;
+ DEBUG && log("TCPControlChannel - notify pending offer: " +
+ JSON.stringify(offer)); // jshint ignore:line
+ this._listener.onOffer(new ChannelDescription(offer));
+ this._pendingOffer = null;
+ }
+
+ if (this._pendingAnswer) {
+ let answer = this._pendingAnswer;
+ DEBUG && log("TCPControlChannel - notify pending answer: " +
+ JSON.stringify(answer)); // jshint ignore:line
+ this._listener.onAnswer(new ChannelDescription(answer));
+ this._pendingAnswer = null;
+ }
+
+ if (this._pendingClose) {
+ DEBUG && log("TCPControlChannel - notify pending closed"); // jshint ignore:line
+ this._notifyDisconnected(this._pendingCloseReason);
+ this._pendingClose = null;
+ }
+
+ if (this._pendingReconnect) {
+ DEBUG && log("TCPControlChannel - notify pending reconnected"); // jshint ignore:line
+ this._notifyReconnected();
+ this._pendingReconnect = false;
+ }
+ },
+
+ /**
+ * These functions are designed to handle the interaction with listener
+ * appropriately. |_FUNC| is to handle |this._listener.FUNC|.
+ */
+ _onOffer: function(aOffer) {
+ if (!this._incomingEnabled) {
+ return;
+ }
+ if (!this._listener) {
+ this._pendingOffer = aOffer;
+ return;
+ }
+ DEBUG && log("TCPControlChannel - notify offer: " +
+ JSON.stringify(aOffer)); // jshint ignore:line
+ this._listener.onOffer(new ChannelDescription(aOffer));
+ },
+
+ _onAnswer: function(aAnswer) {
+ if (!this._incomingEnabled) {
+ return;
+ }
+ if (!this._listener) {
+ this._pendingAnswer = aAnswer;
+ return;
+ }
+ DEBUG && log("TCPControlChannel - notify answer: " +
+ JSON.stringify(aAnswer)); // jshint ignore:line
+ this._listener.onAnswer(new ChannelDescription(aAnswer));
+ },
+
+ _notifyConnected: function() {
+ this._pendingClose = false;
+ this._pendingCloseReason = Cr.NS_OK;
+
+ if (!this._listener) {
+ this._pendingOpen = true;
+ return;
+ }
+
+ DEBUG && log("TCPControlChannel - notify opened with role: " +
+ this._direction); // jshint ignore:line
+ this._listener.notifyConnected();
+ },
+
+ _notifyDisconnected: function(aReason) {
+ this._pendingOpen = false;
+ this._pendingOffer = null;
+ this._pendingAnswer = null;
+
+ // Remote endpoint closes the control channel with abnormal reason.
+ if (aReason == Cr.NS_OK && this._pendingCloseReason != Cr.NS_OK) {
+ aReason = this._pendingCloseReason;
+ }
+
+ if (!this._listener) {
+ this._pendingClose = true;
+ this._pendingCloseReason = aReason;
+ return;
+ }
+
+ DEBUG && log("TCPControlChannel - notify closed with role: " +
+ this._direction); // jshint ignore:line
+ this._listener.notifyDisconnected(aReason);
+ },
+
+ _notifyReconnected: function() {
+ if (!this._listener) {
+ this._pendingReconnect = true;
+ return;
+ }
+
+ DEBUG && log("TCPControlChannel - notify reconnected with role: " +
+ this._direction); // jshint ignore:line
+ this._listener.notifyReconnected();
+ },
+
+ _closeOutgoing: function() {
+ if (this._outgoingEnabled) {
+ this._output.close();
+ this._outgoingEnabled = false;
+ }
+ },
+ _closeIncoming: function() {
+ if (this._incomingEnabled) {
+ this._pump = null;
+ this._input.close();
+ this._incomingEnabled = false;
+ }
+ },
+ _closeTransport: function() {
+ if (this._disconnectTimer) {
+ clearTimeout(this._disconnectTimer);
+ delete this._disconnectTimer;
+ }
+
+ if (this._terminateTimer) {
+ clearTimeout(this._terminateTimer);
+ delete this._terminateTimer;
+ }
+
+ delete this._pendingDisconnect;
+
+ this._transport.setEventSink(null, null);
+
+ this._closeIncoming();
+ this._closeOutgoing();
+ this._presentationService.releaseControlChannel(this);
+ },
+
+ disconnect: function(aReason) {
+ DEBUG && log("TCPControlChannel - disconnect with reason: " + aReason); // jshint ignore:line
+
+ // Pending disconnect during termination procedure.
+ if (this._terminateTimer) {
+ // Store only the first disconnect action.
+ if (!this._pendingDisconnect) {
+ this._pendingDisconnect = this.disconnect.bind(this, aReason);
+ }
+ return;
+ }
+
+ if (this._outgoingEnabled && !this._disconnectTimer) {
+ // default reason is NS_OK
+ aReason = !aReason ? Cr.NS_OK : aReason;
+
+ this._stateMachine.onChannelClosed(aReason, false);
+
+ // Start a guard timer to ensure the transport will be closed.
+ this._disconnectTimer = setTimeout(() => {
+ DEBUG && log("TCPControlChannel - disconnect timeout"); // jshint ignore:line
+ this._closeTransport();
+ }, kDisconnectTimeout);
+ }
+ },
+
+ reconnect: function(aPresentationId, aUrl) {
+ DEBUG && log("TCPControlChannel - reconnect with role: " +
+ this._direction); // jshint ignore:line
+ if (this._direction != "sender") {
+ return Cr.NS_ERROR_FAILURE;
+ }
+
+ this._stateMachine.reconnect(aPresentationId, aUrl);
+ },
+
+ // callback from state machine
+ sendCommand: function(command) {
+ this._outgoingMsgs.push(command);
+ this._flushOutgoing();
+ },
+
+ notifyDeviceConnected: function(deviceId) {
+ switch (this._direction) {
+ case "receiver":
+ this._deviceInfo.id = deviceId;
+ break;
+ }
+ this._notifyConnected();
+ },
+
+ notifyDisconnected: function(reason) {
+ this._closeTransport();
+ this._notifyDisconnected(reason);
+ },
+
+ notifyLaunch: function(presentationId, url) {
+ switch (this._direction) {
+ case "receiver":
+ this._presentationService.onSessionRequest(this._deviceInfo,
+ url,
+ presentationId,
+ this);
+ break;
+ }
+ },
+
+ notifyTerminate: function(presentationId) {
+ if (!this._terminatingId) {
+ this._terminatingId = presentationId;
+ this._presentationService.onSessionTerminate(this._deviceInfo,
+ presentationId,
+ this,
+ this._direction === "sender");
+ return;
+ }
+
+ // Cancel terminate guard timer after receiving terminate-ack.
+ if (this._terminateTimer) {
+ clearTimeout(this._terminateTimer);
+ delete this._terminateTimer;
+ }
+
+ if (this._terminatingId !== presentationId) {
+ // Requested presentation Id doesn't matched with the one in ACK.
+ // Disconnect the control channel with error.
+ DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line
+ this.disconnect(Cr.NS_ERROR_FAILURE);
+ }
+
+ delete this._terminatingId;
+ if (this._pendingDisconnect) {
+ this._pendingDisconnect();
+ }
+ },
+
+ notifyReconnect: function(presentationId, url) {
+ switch (this._direction) {
+ case "receiver":
+ this._presentationService.onSessionReconnect(this._deviceInfo,
+ url,
+ presentationId,
+ this);
+ break;
+ case "sender":
+ this._notifyReconnected();
+ break;
+ }
+ },
+
+ notifyOffer: function(offer) {
+ this._onOffer(offer);
+ },
+
+ notifyAnswer: function(answer) {
+ this._onAnswer(answer);
+ },
+
+ notifyIceCandidate: function(candidate) {
+ this._listener.onIceCandidate(candidate);
+ },
+
+ classID: Components.ID("{fefb8286-0bdc-488b-98bf-0c11b485c955}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel,
+ Ci.nsIStreamListener]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationControlService]); // jshint ignore:line
diff --git a/dom/presentation/provider/PresentationDeviceProviderModule.cpp b/dom/presentation/provider/PresentationDeviceProviderModule.cpp
new file mode 100644
index 000000000..9100fa49b
--- /dev/null
+++ b/dom/presentation/provider/PresentationDeviceProviderModule.cpp
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DisplayDeviceProvider.h"
+#include "MulticastDNSDeviceProvider.h"
+#include "mozilla/ModuleUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "LegacyMDNSDeviceProvider.h"
+#endif //MOZ_WIDGET_ANDROID
+
+#define MULTICAST_DNS_PROVIDER_CID \
+ {0x814f947a, 0x52f7, 0x41c9, \
+ { 0x94, 0xa1, 0x36, 0x84, 0x79, 0x72, 0x84, 0xac }}
+#define DISPLAY_DEVICE_PROVIDER_CID \
+ { 0x515d9879, 0xfe0b, 0x4d9f, \
+ { 0x89, 0x49, 0x7f, 0xa7, 0x65, 0x6c, 0x01, 0x0e } }
+
+#ifdef MOZ_WIDGET_ANDROID
+#define LEGACY_MDNS_PROVIDER_CID \
+ { 0x6885ff39, 0xd98c, 0x4356, \
+ { 0x9e, 0xb3, 0x56, 0x56, 0x31, 0x63, 0x0a, 0xf6 } }
+#endif //MOZ_WIDGET_ANDROID
+
+#define DISPLAY_DEVICE_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/displaydevice-provider;1"
+#define MULTICAST_DNS_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/multicastdns-provider;1"
+
+#ifdef MOZ_WIDGET_ANDROID
+#define LEGACY_MDNS_PROVIDER_CONTRACT_ID "@mozilla.org/presentation-device/legacy-mdns-provider;1"
+#endif //MOZ_WIDGET_ANDROID
+
+using mozilla::dom::presentation::MulticastDNSDeviceProvider;
+using mozilla::dom::presentation::DisplayDeviceProvider;
+
+#ifdef MOZ_WIDGET_ANDROID
+using mozilla::dom::presentation::legacy::LegacyMDNSDeviceProvider;
+#endif //MOZ_WIDGET_ANDROID
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(MulticastDNSDeviceProvider)
+NS_DEFINE_NAMED_CID(MULTICAST_DNS_PROVIDER_CID);
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(DisplayDeviceProvider)
+NS_DEFINE_NAMED_CID(DISPLAY_DEVICE_PROVIDER_CID);
+
+#ifdef MOZ_WIDGET_ANDROID
+NS_GENERIC_FACTORY_CONSTRUCTOR(LegacyMDNSDeviceProvider)
+NS_DEFINE_NAMED_CID(LEGACY_MDNS_PROVIDER_CID);
+#endif //MOZ_WIDGET_ANDROID
+
+static const mozilla::Module::CIDEntry kPresentationDeviceProviderCIDs[] = {
+ { &kMULTICAST_DNS_PROVIDER_CID, false, nullptr, MulticastDNSDeviceProviderConstructor },
+ { &kDISPLAY_DEVICE_PROVIDER_CID, false, nullptr, DisplayDeviceProviderConstructor },
+#ifdef MOZ_WIDGET_ANDROID
+ { &kLEGACY_MDNS_PROVIDER_CID, false, nullptr, LegacyMDNSDeviceProviderConstructor },
+#endif //MOZ_WIDGET_ANDROID
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kPresentationDeviceProviderContracts[] = {
+ { MULTICAST_DNS_PROVIDER_CONTRACT_ID, &kMULTICAST_DNS_PROVIDER_CID },
+ { DISPLAY_DEVICE_PROVIDER_CONTRACT_ID, &kDISPLAY_DEVICE_PROVIDER_CID },
+#ifdef MOZ_WIDGET_ANDROID
+ { LEGACY_MDNS_PROVIDER_CONTRACT_ID, &kLEGACY_MDNS_PROVIDER_CID },
+#endif //MOZ_WIDGET_ANDROID
+ { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kPresentationDeviceProviderCategories[] = {
+#if defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_ANDROID) || (defined(MOZ_WIDGET_GONK) && ANDROID_VERSION >= 16)
+ { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "MulticastDNSDeviceProvider", MULTICAST_DNS_PROVIDER_CONTRACT_ID },
+#endif
+#if defined(MOZ_WIDGET_GONK)
+ { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "DisplayDeviceProvider", DISPLAY_DEVICE_PROVIDER_CONTRACT_ID },
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ { PRESENTATION_DEVICE_PROVIDER_CATEGORY, "LegacyMDNSDeviceProvider", LEGACY_MDNS_PROVIDER_CONTRACT_ID },
+#endif //MOZ_WIDGET_ANDROID
+ { nullptr }
+};
+
+static const mozilla::Module kPresentationDeviceProviderModule = {
+ mozilla::Module::kVersion,
+ kPresentationDeviceProviderCIDs,
+ kPresentationDeviceProviderContracts,
+ kPresentationDeviceProviderCategories
+};
+
+NSMODULE_DEFN(PresentationDeviceProviderModule) = &kPresentationDeviceProviderModule;
diff --git a/dom/presentation/provider/ReceiverStateMachine.jsm b/dom/presentation/provider/ReceiverStateMachine.jsm
new file mode 100644
index 000000000..23ebb5a4e
--- /dev/null
+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
@@ -0,0 +1,238 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ReceiverStateMachine"]; // jshint ignore:line
+
+const { utils: Cu } = Components;
+
+/* globals State, CommandType */
+Cu.import("resource://gre/modules/presentation/StateMachineHelper.jsm");
+
+const DEBUG = false;
+function debug(str) {
+ dump("-*- ReceiverStateMachine: " + str + "\n");
+}
+
+var handlers = [
+ function _initHandler(stateMachine, command) {
+ // shouldn't receive any command at init state
+ DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+ },
+ function _connectingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.CONNECT:
+ stateMachine._sendCommand({
+ type: CommandType.CONNECT_ACK
+ });
+ stateMachine.state = State.CONNECTED;
+ stateMachine._notifyDeviceConnected(command.deviceId);
+ break;
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _connectedHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ case CommandType.LAUNCH:
+ stateMachine._notifyLaunch(command.presentationId,
+ command.url);
+ stateMachine._sendCommand({
+ type: CommandType.LAUNCH_ACK,
+ presentationId: command.presentationId
+ });
+ break;
+ case CommandType.TERMINATE:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.TERMINATE_ACK:
+ stateMachine._notifyTerminate(command.presentationId);
+ break;
+ case CommandType.OFFER:
+ case CommandType.ICE_CANDIDATE:
+ stateMachine._notifyChannelDescriptor(command);
+ break;
+ case CommandType.RECONNECT:
+ stateMachine._notifyReconnect(command.presentationId,
+ command.url);
+ stateMachine._sendCommand({
+ type: CommandType.RECONNECT_ACK,
+ presentationId: command.presentationId
+ });
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _closingHandler(stateMachine, command) {
+ switch (command.type) {
+ case CommandType.DISCONNECT:
+ stateMachine.state = State.CLOSED;
+ stateMachine._notifyDisconnected(command.reason);
+ break;
+ default:
+ debug("unexpected command: " + JSON.stringify(command));
+ // ignore unexpected command
+ break;
+ }
+ },
+ function _closedHandler(stateMachine, command) {
+ // ignore every command in closed state.
+ DEBUG && debug("unexpected command: " + JSON.stringify(command)); // jshint ignore:line
+ },
+];
+
+function ReceiverStateMachine(channel) {
+ this.state = State.INIT;
+ this._channel = channel;
+}
+
+ReceiverStateMachine.prototype = {
+ launch: function _launch() {
+ // presentation session can only be launched by controlling UA.
+ debug("receiver shouldn't trigger launch");
+ },
+
+ terminate: function _terminate(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE,
+ presentationId: presentationId,
+ });
+ }
+ },
+
+ terminateAck: function _terminateAck(presentationId) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.TERMINATE_ACK,
+ presentationId: presentationId,
+ });
+ }
+ },
+
+ reconnect: function _reconnect() {
+ debug("receiver shouldn't trigger reconnect");
+ },
+
+ sendOffer: function _sendOffer() {
+ // offer can only be sent by controlling UA.
+ debug("receiver shouldn't generate offer");
+ },
+
+ sendAnswer: function _sendAnswer(answer) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.ANSWER,
+ answer: answer,
+ });
+ }
+ },
+
+ updateIceCandidate: function _updateIceCandidate(candidate) {
+ if (this.state === State.CONNECTED) {
+ this._sendCommand({
+ type: CommandType.ICE_CANDIDATE,
+ candidate: candidate,
+ });
+ }
+ },
+
+ onCommand: function _onCommand(command) {
+ handlers[this.state](this, command);
+ },
+
+ onChannelReady: function _onChannelReady() {
+ if (this.state === State.INIT) {
+ this.state = State.CONNECTING;
+ }
+ },
+
+ onChannelClosed: function _onChannelClose(reason, isByRemote) {
+ switch (this.state) {
+ case State.CONNECTED:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ this._notifyDisconnected(reason);
+ } else {
+ this._sendCommand({
+ type: CommandType.DISCONNECT,
+ reason: reason
+ });
+ this.state = State.CLOSING;
+ this._closeReason = reason;
+ }
+ break;
+ case State.CLOSING:
+ if (isByRemote) {
+ this.state = State.CLOSED;
+ if (this._closeReason) {
+ reason = this._closeReason;
+ delete this._closeReason;
+ }
+ this._notifyDisconnected(reason);
+ } else {
+ // do nothing and wait for remote channel closed.
+ }
+ break;
+ default:
+ DEBUG && debug("unexpected channel close: " + reason + ", " + isByRemote); // jshint ignore:line
+ break;
+ }
+ },
+
+ _sendCommand: function _sendCommand(command) {
+ this._channel.sendCommand(command);
+ },
+
+ _notifyDeviceConnected: function _notifyDeviceConnected(deviceName) {
+ this._channel.notifyDeviceConnected(deviceName);
+ },
+
+ _notifyDisconnected: function _notifyDisconnected(reason) {
+ this._channel.notifyDisconnected(reason);
+ },
+
+ _notifyLaunch: function _notifyLaunch(presentationId, url) {
+ this._channel.notifyLaunch(presentationId, url);
+ },
+
+ _notifyTerminate: function _notifyTerminate(presentationId) {
+ this._channel.notifyTerminate(presentationId);
+ },
+
+ _notifyReconnect: function _notifyReconnect(presentationId, url) {
+ this._channel.notifyReconnect(presentationId, url);
+ },
+
+ _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
+ switch (command.type) {
+ case CommandType.OFFER:
+ this._channel.notifyOffer(command.offer);
+ break;
+ case CommandType.ICE_CANDIDATE:
+ this._channel.notifyIceCandidate(command.candidate);
+ break;
+ }
+ },
+};
+
+this.ReceiverStateMachine = ReceiverStateMachine; // jshint ignore:line
diff --git a/dom/presentation/provider/StateMachineHelper.jsm b/dom/presentation/provider/StateMachineHelper.jsm
new file mode 100644
index 000000000..6e07863d4
--- /dev/null
+++ b/dom/presentation/provider/StateMachineHelper.jsm
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["State", "CommandType"]; // jshint ignore:line
+
+const State = Object.freeze({
+ INIT: 0,
+ CONNECTING: 1,
+ CONNECTED: 2,
+ CLOSING: 3,
+ CLOSED: 4,
+});
+
+const CommandType = Object.freeze({
+ // control channel life cycle
+ CONNECT: "connect", // { deviceId: <string> }
+ CONNECT_ACK: "connect-ack", // { presentationId: <string> }
+ DISCONNECT: "disconnect", // { reason: <int> }
+ // presentation session life cycle
+ LAUNCH: "launch", // { presentationId: <string>, url: <string> }
+ LAUNCH_ACK: "launch-ack", // { presentationId: <string> }
+ TERMINATE: "terminate", // { presentationId: <string> }
+ TERMINATE_ACK: "terminate-ack", // { presentationId: <string> }
+ RECONNECT: "reconnect", // { presentationId: <string> }
+ RECONNECT_ACK: "reconnect-ack", // { presentationId: <string> }
+ // session transport establishment
+ OFFER: "offer", // { offer: <json> }
+ ANSWER: "answer", // { answer: <json> }
+ ICE_CANDIDATE: "ice-candidate", // { candidate: <string> }
+});
+
+this.State = State; // jshint ignore:line
+this.CommandType = CommandType; // jshint ignore:line
diff --git a/dom/presentation/provider/moz.build b/dom/presentation/provider/moz.build
new file mode 100644
index 000000000..18428b50e
--- /dev/null
+++ b/dom/presentation/provider/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_COMPONENTS += [
+ 'BuiltinProviders.manifest',
+ 'PresentationControlService.js'
+]
+
+UNIFIED_SOURCES += [
+ 'DeviceProviderHelpers.cpp',
+ 'DisplayDeviceProvider.cpp',
+ 'MulticastDNSDeviceProvider.cpp',
+ 'PresentationDeviceProviderModule.cpp',
+]
+
+EXTRA_JS_MODULES.presentation += [
+ 'ControllerStateMachine.jsm',
+ 'ReceiverStateMachine.jsm',
+ 'StateMachineHelper.jsm',
+]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
+ EXTRA_COMPONENTS += [
+ # For android presentation device
+ 'AndroidCastDeviceProvider.js',
+ 'AndroidCastDeviceProvider.manifest',
+ # for TV 2.5 device backward capability
+ 'LegacyPresentationControlService.js',
+ 'LegacyProviders.manifest',
+ ]
+
+ UNIFIED_SOURCES += [
+ 'LegacyMDNSDeviceProvider.cpp',
+ ]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+FINAL_LIBRARY = 'xul'
diff --git a/dom/presentation/provider/nsTCPDeviceInfo.h b/dom/presentation/provider/nsTCPDeviceInfo.h
new file mode 100644
index 000000000..118f6c8ac
--- /dev/null
+++ b/dom/presentation/provider/nsTCPDeviceInfo.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __TCPDeviceInfo_h__
+#define __TCPDeviceInfo_h__
+
+namespace mozilla {
+namespace dom {
+namespace presentation {
+
+class TCPDeviceInfo final : public nsITCPDeviceInfo
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITCPDEVICEINFO
+
+ explicit TCPDeviceInfo(const nsACString& aId,
+ const nsACString& aAddress,
+ const uint16_t aPort,
+ const nsACString& aCertFingerprint)
+ : mId(aId)
+ , mAddress(aAddress)
+ , mPort(aPort)
+ , mCertFingerprint(aCertFingerprint)
+ {
+ }
+
+private:
+ virtual ~TCPDeviceInfo() {}
+
+ nsCString mId;
+ nsCString mAddress;
+ uint16_t mPort;
+ nsCString mCertFingerprint;
+};
+
+NS_IMPL_ISUPPORTS(TCPDeviceInfo,
+ nsITCPDeviceInfo)
+
+// nsITCPDeviceInfo
+NS_IMETHODIMP
+TCPDeviceInfo::GetId(nsACString& aId)
+{
+ aId = mId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetAddress(nsACString& aAddress)
+{
+ aAddress = mAddress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetPort(uint16_t* aPort)
+{
+ *aPort = mPort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TCPDeviceInfo::GetCertFingerprint(nsACString& aCertFingerprint)
+{
+ aCertFingerprint = mCertFingerprint;
+ return NS_OK;
+}
+
+} // namespace presentation
+} // namespace dom
+} // namespace mozilla
+
+#endif /* !__TCPDeviceInfo_h__ */
+
diff --git a/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
new file mode 100644
index 000000000..2bc069f6b
--- /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';
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/PresentationDeviceInfoManager.jsm');
+
+const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+const manager = Cc['@mozilla.org/presentation-device/manager;1']
+ .getService(Ci.nsIPresentationDeviceManager);
+
+var testProvider = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
+ forceDiscovery: function() {
+ sendAsyncMessage('force-discovery');
+ },
+ listener: null,
+};
+
+var testDevice = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ establishControlChannel: function() {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return true;
+ },
+ id: null,
+ name: null,
+ type: null,
+ listener: null,
+};
+
+var testDevice1 = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'dummyid',
+ name: 'dummyName',
+ type: 'dummyType',
+ establishControlChannel: function(url, presentationId) {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return true;
+ },
+};
+
+var testDevice2 = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'dummyid',
+ name: 'dummyName',
+ type: 'dummyType',
+ establishControlChannel: function(url, presentationId) {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return true;
+ },
+};
+
+var mockedDeviceWithoutSupportedURL = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'dummyid',
+ name: 'dummyName',
+ type: 'dummyType',
+ establishControlChannel: function(url, presentationId) {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return false;
+ },
+};
+
+var mockedDeviceSupportHttpsURL = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'dummyid',
+ name: 'dummyName',
+ type: 'dummyType',
+ establishControlChannel: function(url, presentationId) {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ if (requestedUrl.indexOf("https://") != -1) {
+ 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 000000000..3052bdcb1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -0,0 +1,470 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+'use strict';
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.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;
+ }
+ if (originalFactory) {
+ registrar.unregisterFactory(originalClassId, originalFactory);
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return { contractId: contractId,
+ mockedClassId: mockedClassId,
+ mockedFactory: mockedFactory,
+ originalClassId: originalClassId,
+ originalFactory: originalFactory };
+}
+
+function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ registrar.registerFactory(originalClassId, "", contractId, originalFactory);
+ }
+}
+
+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, false);
+
+const mockedChannelDescription = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: XPCOMUtils.generateQI([Ci.nsIServerSocket,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ get port() {
+ return this._port;
+ },
+ set listener(listener) {
+ this._listener = listener;
+ },
+ init: function(port, loopbackOnly, backLog) {
+ if (port != -1) {
+ this._port = port;
+ } else {
+ this._port = 5678;
+ }
+ },
+ asyncListen: function(listener) {
+ this._listener = listener;
+ },
+ close: function() {
+ this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED);
+ },
+ simulateOnSocketAccepted: function(serverSocket, socketTransport) {
+ this._listener.onSocketAccepted(serverSocket, socketTransport);
+ }
+};
+
+const mockedSocketTransport = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISocketTransport]),
+};
+
+const mockedControlChannel = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+ set listener(listener) {
+ this._listener = listener;
+ },
+ get listener() {
+ return this._listener;
+ },
+ sendOffer: function(offer) {
+ sendAsyncMessage('offer-sent', this._isValidSDP(offer));
+ },
+ sendAnswer: function(answer) {
+ sendAsyncMessage('answer-sent', this._isValidSDP(answer));
+
+ if (answer.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ this._listener.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+ }
+ },
+ _isValidSDP: function(aSDP) {
+ var isValid = false;
+ if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ try {
+ var addresses = aSDP.tcpAddress;
+ if (addresses.length > 0) {
+ for (var i = 0; i < addresses.length; i++) {
+ // Ensure CString addresses are used. Otherwise, an error will be thrown.
+ addresses.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: function(presentationId, url) {
+ sessionId = presentationId;
+ },
+ terminate: function(presentationId) {
+ sendAsyncMessage('sender-terminate', presentationId);
+ },
+ reconnect: function(presentationId, url) {
+ sendAsyncMessage('start-reconnect', url);
+ },
+ notifyReconnected: function() {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ disconnect: function(reason) {
+ sendAsyncMessage('control-channel-closed', reason);
+ this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyDisconnected(reason);
+ },
+ simulateReceiverReady: function() {
+ this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady();
+ },
+ simulateOnOffer: function() {
+ sendAsyncMessage('offer-received');
+ this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onOffer(mockedChannelDescription);
+ },
+ simulateOnAnswer: function() {
+ sendAsyncMessage('answer-received');
+ this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onAnswer(mockedChannelDescription);
+ },
+ simulateNotifyConnected: function() {
+ sendAsyncMessage('control-channel-opened');
+ this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyConnected();
+ },
+};
+
+const mockedDevice = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'id',
+ name: 'name',
+ type: 'type',
+ establishControlChannel: function(url, presentationId) {
+ sendAsyncMessage('control-channel-established');
+ return mockedControlChannel;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return true;
+ },
+};
+
+const mockedDevicePrompt = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection: function(request) {
+ this._request = request;
+ sendAsyncMessage('device-prompt');
+ },
+ simulateSelect: function() {
+ this._request.select(mockedDevice);
+ },
+ simulateCancel: function(result) {
+ this._request.cancel(result);
+ }
+};
+
+const mockedSessionTransport = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport,
+ Ci.nsIPresentationSessionTransportBuilder,
+ Ci.nsIPresentationTCPSessionTransportBuilder,
+ Ci.nsIPresentationDataChannelSessionTransportBuilder,
+ Ci.nsIPresentationControlChannelListener,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.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: function(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: function(description, listener) {
+ this._listener = listener;
+ this._role = Ci.nsIPresentationService.ROLE_RECEIVER;
+
+ var addresses = description.QueryInterface(Ci.nsIPresentationChannelDescription).tcpAddress;
+ this._selfAddress = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINetAddr]),
+ address: (addresses.length > 0) ?
+ addresses.queryElementAt(0, Ci.nsISupportsCString).data : "",
+ port: description.QueryInterface(Ci.nsIPresentationChannelDescription).tcpPort,
+ };
+
+ setTimeout(()=>{
+ this._listener.onSessionTransport(this);
+ this._listener = null;
+ }, 0);
+ },
+ // in-process case
+ buildDataChannelTransport: function(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: function() {
+ sendAsyncMessage('data-transport-notification-enabled');
+ },
+ send: function(data) {
+ sendAsyncMessage('message-sent', data);
+ },
+ close: function(reason) {
+ sendAsyncMessage('data-transport-closed', reason);
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason);
+ },
+ simulateTransportReady: function() {
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+ },
+ simulateIncomingMessage: function(message) {
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyData(message, false);
+ },
+ onOffer: function(aOffer) {
+ },
+ onAnswer: function(aAnswer) {
+ }
+};
+
+const mockedNetworkInfo = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo]),
+ getAddresses: function(ips, prefixLengths) {
+ ips.value = ["127.0.0.1"];
+ prefixLengths.value = [0];
+ return 1;
+ },
+};
+
+const mockedNetworkManager = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkManager,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ get activeNetworkInfo() {
+ return mockedNetworkInfo;
+ },
+};
+
+var requestPromise = null;
+
+const mockedRequestUIGlue = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest: function(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;
+});
+
+var obs = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+obs.addObserver(function observer(aSubject, aTopic, aData) {
+ obs.removeObserver(observer, aTopic);
+
+ requestPromise = aSubject;
+}, 'setup-request-promise', false);
diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
new file mode 100644
index 000000000..82d7362b2
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -0,0 +1,366 @@
+/* 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';
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.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: XPCOMUtils.generateQI([Ci.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: function() {
+ // send offer after notifyConnected immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ notifyReconnected: function() {
+ // send offer after notifyOpened immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ sendOffer: function(offer) {
+ Services.tm.mainThread.dispatch(() => {
+ mockControlChannelOfReceiver.onOffer(offer);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+ onAnswer: function(answer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onAnswer(answer);
+ },
+ launch: function(presentationId, url) {
+ sessionId = presentationId;
+ sendAsyncMessage('sender-launch', url);
+ },
+ disconnect: function(reason) {
+ if (!this._listener) {
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ mockControlChannelOfReceiver.disconnect();
+ },
+ terminate: function(presentationId) {
+ sendAsyncMessage('sender-terminate');
+ },
+ reconnect: function(presentationId, url) {
+ sendAsyncMessage('start-reconnect', url);
+ },
+ sendIceCandidate: function(candidate) {
+ mockControlChannelOfReceiver.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate: function(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+// control channel of receiver
+const mockControlChannelOfReceiver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+ set listener(listener) {
+ // PresentationPresentingInfo::SetControlChannel
+ if (listener) {
+ debug('set listener for mockControlChannelOfReceiver without null');
+ } else {
+ debug('set listener for mockControlChannelOfReceiver with null');
+ }
+ this._listener = listener;
+
+ if (this._pendingOpened) {
+ this._pendingOpened = false;
+ this.notifyConnected();
+ }
+ },
+ get listener() {
+ return this._listener;
+ },
+ notifyConnected: function() {
+ // do nothing
+ if (!this._listener) {
+ this._pendingOpened = true;
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ onOffer: function(offer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onOffer(offer);
+ },
+ sendAnswer: function(answer) {
+ Services.tm.mainThread.dispatch(() => {
+ mockControlChannelOfSender.onAnswer(answer);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+ disconnect: function(reason) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ sendAsyncMessage('control-channel-receiver-closed', reason);
+ },
+ terminate: function(presentaionId) {
+ },
+ sendIceCandidate: function(candidate) {
+ mockControlChannelOfReceiver.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate: function(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+const mockDevice = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'id',
+ name: 'name',
+ type: 'type',
+ establishControlChannel: function(url, presentationId) {
+ if (triggerControlChannelError) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ sendAsyncMessage('control-channel-established');
+ return mockControlChannelOfSender;
+ },
+ disconnect: function() {
+ sendAsyncMessage('device-disconnected');
+ },
+ isRequestedUrlSupported: function(requestedUrl) {
+ return true;
+ },
+};
+
+const mockDevicePrompt = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt,
+ Ci.nsIFactory]),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection: function(request) {
+ this._request = request;
+ sendAsyncMessage('device-prompt');
+ },
+ simulateSelect: function() {
+ this._request.select(mockDevice);
+ },
+ simulateCancel: function() {
+ this._request.cancel();
+ }
+};
+
+const mockRequestUIGlue = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue,
+ Ci.nsIFactory]),
+ set promise(aPromise) {
+ this._promise = aPromise
+ },
+ get promise() {
+ return this._promise;
+ },
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest: function(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: contractId,
+ mockClassId: mockClassId,
+ mockFactory: mockFactory,
+ originalClassId: originalClassId,
+ originalFactory: originalFactory };
+ }
+ // Register mock factories.
+ const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ 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);
+
+ var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+ obs.addObserver(function setupRequestPromiseHandler(aSubject, aTopic, aData) {
+ debug('Got observer: setup-request-promise');
+ obs.removeObserver(setupRequestPromiseHandler, aTopic);
+ mockRequestUIGlue.promise = aSubject;
+ sendAsyncMessage('promise-setup-ready');
+ }, 'setup-request-promise', false);
+}
+
+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 000000000..77240ab5f
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js
@@ -0,0 +1,258 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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': data});
+ } else {
+ port.postMessage({'type': type,
+ 'data': 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 = {};
+ addMessageListener = function(message, handler) {
+ if (handlers.hasOwnProperty(message)) {
+ handlers[message].push(handler);
+ } else {
+ handlers[message] = [handler];
+ }
+ };
+ 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 { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+ const mockedChannelDescription = {
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIPresentationChannelDescription];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+ get type() {
+ 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 = {
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIPresentationSessionTransport,
+ Ci.nsIPresentationDataChannelSessionTransportBuilder,
+ Ci.nsIFactory];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(aIID);
+ },
+ set callback(callback) {
+ this._callback = callback;
+ },
+ get callback() {
+ return this._callback;
+ },
+ /* OOP case */
+ buildDataChannelTransport: function(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: function() {
+ sendMessage('data-transport-notification-enabled');
+ },
+ send: function(data) {
+ sendMessage('message-sent', data);
+ },
+ close: function(reason) {
+ sendMessage('data-transport-closed', reason);
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason);
+ this._callback = null;
+ },
+ simulateTransportReady: function() {
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+ },
+ simulateIncomingMessage: function(message) {
+ this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyData(message, false);
+ },
+ onOffer: function(aOffer) {
+ this._listener.sendAnswer(mockedChannelDescription);
+ this._onSessionTransport();
+ },
+ onAnswer: function(aAnswer) {
+ this._onSessionTransport();
+ },
+ _onSessionTransport: function() {
+ 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;
+ }
+ if (originalFactory) {
+ registrar.unregisterFactory(originalClassId, originalFactory);
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return { contractId: contractId,
+ mockedClassId: mockedClassId,
+ mockedFactory: mockedFactory,
+ originalClassId: originalClassId,
+ originalFactory: originalFactory };
+ }
+
+ function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ registrar.registerFactory(originalClassId, "", contractId, originalFactory);
+ }
+ }
+
+ /* 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: function(message, handler) {
+ if (this.handlers.hasOwnProperty(message)) {
+ this.handlers[message].push(handler);
+ } else {
+ this.handlers[message] = [handler];
+ }
+ },
+ removeMessageListener: function(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: function(message, data) {
+ port.postMessage({'type': message,
+ 'data': data
+ });
+ }
+}
+
+if (!SpecialPowers.isMainProcess()) {
+ var port;
+ try {
+ port = SpecialPowers.loadPrivilegedScript(loadPrivilegedScriptTest.toSource());
+ } catch (e) {
+ ok(false, "loadPrivilegedScript shoulde 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 000000000..83841f4f8
--- /dev/null
+++ b/dom/presentation/tests/mochitest/chrome.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files =
+ PresentationDeviceInfoChromeScript.js
+ PresentationSessionChromeScript.js
+
+[test_presentation_datachannel_sessiontransport.html]
+skip-if = os == 'android'
+[test_presentation_device_info.html]
+[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 000000000..cf02d2b2c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -0,0 +1,220 @@
+<!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;version=1.7">
+
+"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: name, data: 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 messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ 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();
+ });
+ 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 messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ let recvData= String.fromCharCode.apply(null, new Uint8Array(evt.data));
+ is(recvData, "Hello World", 'expected same string data');
+ aResolve();
+ });
+ });
+}
+
+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 messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ 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);
+ });
+ });
+}
+
+function testIncomingArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info('Receiver: --- testIncomingArrayBufferView ---');
+ connection.binaryType = "arraybuffer";
+ connection.send('testIncomingArrayBufferView');
+ connection.addEventListener('message', function messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ ok(is_same_buffer(evt.data, TYPED_DATA_ARRAY), "expected same buffer data");
+ aResolve();
+ });
+ });
+}
+
+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 000000000..370cb92e1
--- /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;version=1.7">
+
+"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: name, data: 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_mixed_security_contexts.html b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html
new file mode 100644
index 000000000..f042d2994
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html
@@ -0,0 +1,159 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript;version=1.8">
+
+"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 onMessage(evt) {
+ window.removeEventListener("message", onMessage);
+ if (evt.data === "start") {
+ runTest();
+ }
+}, false);
+
+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 000000000..1203523ac
--- /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;version=1.7">
+
+"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 000000000..c95eddf57
--- /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;version=1.7">
+
+"use strict";
+
+function ok(a, msg) {
+ alert((a ? '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 000000000..46a330b5f
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver.html
@@ -0,0 +1,140 @@
+<!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;version=1.7">
+
+"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.");
+ var promise = 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 messageHandler(aEvent) {
+ connection.removeEventListener('message', messageHandler);
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ });
+
+ 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 000000000..3a6060310
--- /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;version=1.7">
+
+"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 000000000..6b1f2152f
--- /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;version=1.7">
+
+"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: name, data: 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 000000000..3bd5ac4b1
--- /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;version=1.7">
+
+"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 000000000..174ccd3f3
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_reconnect.html
@@ -0,0 +1,102 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript;version=1.8">
+
+"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);
+ teardown();
+ 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 });
+ }
+ );
+ }
+}, false);
+
+</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 000000000..369621cee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html
@@ -0,0 +1,114 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript;version=1.8">
+
+"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 onMessage(evt) {
+ window.removeEventListener("message", onMessage);
+ if (evt.data === "start") {
+ runTest();
+ }
+}, false);
+
+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 000000000..a26a44b90
--- /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;version=1.7'>
+
+'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: name, data: 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 000000000..d8df8a1a6
--- /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;version=1.7'>
+
+'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: name, data: 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 000000000..8b1378917
--- /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 000000000..fc044e3c4
--- /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 000000000..f96e07f1e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -0,0 +1,77 @@
+[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]
+[test_presentation_dc_receiver.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_dc_receiver_oop.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_inproc.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_oop.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_1ua_connection_wentaway_inproc.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_1ua_connection_wentaway_oop.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_device_info_permission.html]
+[test_presentation_tcp_sender_disconnect.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_tcp_sender_establish_connection_error.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_tcp_receiver_establish_connection_error.html]
+skip-if = (e10s || toolkit == 'android' || os == 'mac' || os == 'win') # Bug 1129785, Bug 1204709
+[test_presentation_tcp_receiver_establish_connection_timeout.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html]
+skip-if = (e10s || toolkit == 'android')
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html]
+skip-if = (e10s || toolkit == 'android')
+[test_presentation_tcp_receiver.html]
+skip-if = (e10s || toolkit == 'android') # Bug 1129785
+[test_presentation_tcp_receiver_oop.html]
+skip-if = (e10s || toolkit == 'android') # 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 || toolkit == 'android')
+[test_presentation_terminate_oop.html]
+skip-if = (e10s || toolkit == 'android')
+[test_presentation_terminate_establish_connection_error_inproc.html]
+skip-if = (e10s || toolkit == 'android')
+[test_presentation_terminate_establish_connection_error_oop.html]
+skip-if = (e10s || toolkit == 'android')
+[test_presentation_sender_on_terminate_request.html]
+skip-if = toolkit == '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]
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 000000000..dbeb4ffcc
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -0,0 +1,175 @@
+'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.indexOf('_inproc') == -1;
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute('src', receiverUrl);
+ receiverIframe.addEventListener("mozbrowserloadend", function mozbrowserloadendHander() {
+ receiverIframe.removeEventListener("mozbrowserloadend", mozbrowserloadendHander);
+ info("Receiver loaded.");
+ });
+
+ // 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();
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+ });
+
+ 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.mozBrowserFramesEnabled", true],
+ ["dom.ipc.tabs.disabled", false],
+ ["network.disable.ipc.security", true],
+ ["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 000000000..68491d81b
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..68491d81b
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..8a7787b40
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js
@@ -0,0 +1,370 @@
+/* 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.indexOf('_inproc') == -1;
+ 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);
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+ });
+
+ 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 messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ let msg = evt.data;
+ is(msg, "msg-receiver-to-sender", "Sender: Sender should receive message from Receiver");
+ postMessageToIframe('message-from-receiver-received');
+ aResolve();
+ });
+ postMessageToIframe('trigger-message-from-receiver');
+ });
+}
+
+function testSendBlobMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info('Sender: --- testSendBlobMessage ---');
+ connection.addEventListener('message', function messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ 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();
+ });
+ });
+}
+
+function testSendArrayBuffer() {
+ return new Promise(function(aResolve, aReject) {
+ info('Sender: --- testSendArrayBuffer ---');
+ connection.addEventListener('message', function messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ let msg = evt.data;
+ is(msg, "testIncomingArrayBuffer", "Sender: Sender should receive message from Receiver");
+ connection.send(DATA_ARRAY_BUFFER);
+ aResolve();
+ });
+ });
+}
+
+function testSendArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info('Sender: --- testSendArrayBufferView ---');
+ connection.addEventListener('message', function messageHandler(evt) {
+ connection.removeEventListener('message', messageHandler);
+ let msg = evt.data;
+ is(msg, "testIncomingArrayBufferView", "Sender: Sender should receive message from Receiver");
+ connection.send(TYPED_DATA_ARRAY);
+ aResolve();
+ });
+ });
+}
+
+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],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", true],
+ ["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 000000000..520b1a98c
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..e744e6802
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..89f1ad1b7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_availability.html
@@ -0,0 +1,236 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+"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(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;
+ gScript.sendAsyncMessage('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 request = new PresentationRequest("https://example.org");
+ let firstAvailabilityResolved = false;
+ return Promise.all([
+ request.getAvailability().then(function() {
+ firstAvailabilityResolved = true;
+ }),
+ request.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 request = new PresentationRequest("https://test.com");
+ request.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();
+}
+
+function runTests() {
+ 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);
+}
+
+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]]},
+ runTests);
+});
+
+</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 000000000..89a51afb7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
@@ -0,0 +1,245 @@
+<!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 type="application/javascript" 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const { Services } = Cu.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: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+ notifyTransportReady: function () {
+ info("Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed: function (aReason) {
+ info("Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData: function(aData) {
+ is(aData, serverMessage, "Client transport receives data.");
+ gResolve();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+ notifyTransportReady: function () {
+ info("Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed: function (aReason) {
+ info("Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData: function(aData) {
+ is(aData, clientMessage, "Server transport receives data.");
+ gResolve()
+ },
+};
+
+const clientListener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+ onSessionTransport: function(aTransport) {
+ info("Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+ },
+ onError: function(aError) {
+ ok(false, "client's builder reports error " + aError);
+ },
+ sendOffer: function(aOffer) {
+ setTimeout(()=>this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer: function(aAnswer) {
+ setTimeout(()=>this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate: function(aCandidate) {
+ setTimeout(()=>this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect: function(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: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+ onSessionTransport: function(aTransport) {
+ info("Server Transport is built.");
+ serverTransport = aTransport;
+ serverTransport.callback = serverCallback;
+ serverTransport.enableDataNotification();
+ },
+ onError: function(aError) {
+ ok(false, "server's builder reports error " + aError);
+ },
+ sendOffer: function(aOffer) {
+ setTimeout(()=>this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer: function(aAnswer) {
+ setTimeout(()=>this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate: function(aCandidate) {
+ setTimeout(()=>this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect: function(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 000000000..a42489bdb
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html
@@ -0,0 +1,141 @@
+<!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 type="application/javascript" 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.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+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();
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ 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],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", 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 000000000..b289b0be6
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
@@ -0,0 +1,213 @@
+<!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 type="application/javascript" 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.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+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();
+ }
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ receiverIframe.addEventListener("mozbrowserloadstart", function onLoadEnd() {
+ receiverIframe.removeEventListener("mozbrowserloadstart", onLoadEnd);
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.loadFrameScript("data:,(" + loadPrivilegedScriptTest.toString() + ")();", false);
+ });
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ // 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();
+ }
+ }
+ }, false);
+
+ 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(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(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.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", 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 000000000..97e252e84
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
@@ -0,0 +1,291 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+'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 messageHandler(aEvent) {
+ connection.removeEventListener('message', messageHandler);
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ });
+
+ 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_device_info.html b/dom/presentation/tests/mochitest/test_presentation_device_info.html
new file mode 100644
index 000000000..77253e41d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info.html
@@ -0,0 +1,144 @@
+<!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 Device Info API</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1080474">Test for B2G Presentation Device Info API</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+var testDevice = {
+ id: 'id',
+ name: 'name',
+ type: 'type',
+};
+
+var gUrl = SimpleTest.getTestFileURL('PresentationDeviceInfoChromeScript.js');
+var gScript = SpecialPowers.loadChromeScript(gUrl);
+
+function testSetup() {
+ return new Promise(function(resolve, reject) {
+ gScript.addMessageListener('setup-complete', function() {
+ resolve();
+ });
+ gScript.sendAsyncMessage('setup');
+ });
+}
+
+function testForceDiscovery() {
+ info('test force discovery');
+ return new Promise(function(resolve, reject) {
+ gScript.addMessageListener('force-discovery', function() {
+ ok(true, 'nsIPresentationDeviceProvider.forceDiscovery is invoked');
+ resolve();
+ });
+ navigator.mozPresentationDeviceInfo.forceDiscovery();
+ });
+}
+
+function testDeviceAdd() {
+ info('test device add');
+ return new Promise(function(resolve, reject) {
+ navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+ navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+ let detail = e.detail;
+ is(detail.type, 'add', 'expected update type');
+ is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+ is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+ is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+ navigator.mozPresentationDeviceInfo.getAll()
+ .then(function(devices) {
+ is(devices.length, 1, 'expected 1 available device');
+ is(devices[0].id, testDevice.id, 'expected device id');
+ is(devices[0].name, testDevice.name, 'expected device name');
+ is(devices[0].type, testDevice.type, 'expected device type');
+ resolve();
+ });
+ });
+ gScript.sendAsyncMessage('trigger-device-add', testDevice);
+ });
+}
+
+function testDeviceUpdate() {
+ info('test device update');
+ return new Promise(function(resolve, reject) {
+ testDevice.name = 'name-update';
+
+ navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+ navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+ let detail = e.detail;
+ is(detail.type, 'update', 'expected update type');
+ is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+ is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+ is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+ navigator.mozPresentationDeviceInfo.getAll()
+ .then(function(devices) {
+ is(devices.length, 1, 'expected 1 available device');
+ is(devices[0].id, testDevice.id, 'expected device id');
+ is(devices[0].name, testDevice.name, 'expected device name');
+ is(devices[0].type, testDevice.type, 'expected device type');
+ resolve();
+ });
+ });
+ gScript.sendAsyncMessage('trigger-device-update', testDevice);
+ });
+}
+
+function testDeviceRemove() {
+ info('test device remove');
+ return new Promise(function(resolve, reject) {
+ navigator.mozPresentationDeviceInfo.addEventListener('devicechange', function deviceChangeHandler(e) {
+ navigator.mozPresentationDeviceInfo.removeEventListener('devicechange', deviceChangeHandler);
+ let detail = e.detail;
+ is(detail.type, 'remove', 'expected update type');
+ is(detail.deviceInfo.id, testDevice.id, 'expected device id');
+ is(detail.deviceInfo.name, testDevice.name, 'expected device name');
+ is(detail.deviceInfo.type, testDevice.type, 'expected device type');
+
+ navigator.mozPresentationDeviceInfo.getAll()
+ .then(function(devices) {
+ is(devices.length, 0, 'expected 0 available device');
+ resolve();
+ });
+ });
+ gScript.sendAsyncMessage('trigger-device-remove');
+ });
+}
+
+function runTests() {
+ testSetup()
+ .then(testForceDiscovery)
+ .then(testDeviceAdd)
+ .then(testDeviceUpdate)
+ .then(testDeviceRemove)
+ .then(function() {
+ info('test finished, teardown');
+ gScript.sendAsyncMessage('teardown', '');
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+}
+
+window.addEventListener('load', function() {
+ SpecialPowers.pushPrefEnv({
+ 'set': [
+ ['dom.presentation.enabled', true],
+ ]
+ }, runTests);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html b/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html
new file mode 100644
index 000000000..c7f7ac96d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info_permission.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 B2G Presentation Device Info API Permission</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=1080474">Test for B2G Presentation Device Info API Permission</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ is(navigator.mozPresentationDeviceInfo, undefined, 'navigator.mozPresentationDeviceInfo is undefined');
+ SimpleTest.finish();
+}
+
+window.addEventListener('load', function() {
+ SpecialPowers.pushPrefEnv({
+ 'set': [
+ ['dom.presentation.enabled', true],
+ ]
+ }, runTests);
+});
+
+</script>
+</pre>
+</body>
+</html>
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 000000000..31918a2c4
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
@@ -0,0 +1,81 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+"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: {
+ appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
+ 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 000000000..0647bff3a
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
@@ -0,0 +1,77 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver_auxiliary_navigation.html");
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+ var oop = location.pathname.indexOf('_inproc') == -1;
+ 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();
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise", null);
+
+ aResolve();
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup().then();
+}
+
+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.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", 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 000000000..f873fa3da
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..f873fa3da
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..079b7f5c5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
@@ -0,0 +1,379 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+'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 ---');
+ ok(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 messageHandler(aEvent) {
+ aConnection.removeEventListener('message', messageHandler);
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ });
+
+ 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 000000000..dc17209c5
--- /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 type="application/javascript" 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;version=1.8">
+
+"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 000000000..d0c8af0ad
--- /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 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 onTerminateRequest at sender side</a>
+<script type="application/javascript;version=1.8">
+
+'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 000000000..27b17bb32
--- /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 type="application/javascript" 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;version=1.8">
+
+'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 000000000..f26184f0b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
@@ -0,0 +1,137 @@
+<!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 type="application/javascript" 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.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ 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();
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ 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.");
+ });
+
+ 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.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", 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 000000000..0935aaaf9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
@@ -0,0 +1,110 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_receiver_establish_connection_error.html');
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+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();
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ 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.");
+ });
+
+ 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);
+ });
+}
+
+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.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", true]]},
+ 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 000000000..1dc002644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
@@ -0,0 +1,81 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ 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', null);
+
+ aResolve();
+ });
+}
+
+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().
+ then(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 000000000..d73f84cf8
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
@@ -0,0 +1,88 @@
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_unknown_content_type.test');
+
+var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+var receiverIframe;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ 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.indexOf('_inproc') == -1;
+ receiverIframe.setAttribute("remote", oop);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ aResolve();
+ });
+}
+
+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().
+ then(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.mozBrowserFramesEnabled', true],
+ ["network.disable.ipc.security", true],
+ ['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 000000000..8ade1d72d
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..b2d2d3c6e
--- /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 type="application/javascript" 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;version=1.8" 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 000000000..bfbc7947a
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
@@ -0,0 +1,178 @@
+<!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 type="application/javascript" 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.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ 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();
+ }
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+
+ // 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();
+ }
+ }
+ }, false);
+
+ 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.");
+ });
+
+ 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);
+ });
+}
+
+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", false],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["network.disable.ipc.security", true],
+ ["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 000000000..8df34c884
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
@@ -0,0 +1,260 @@
+<!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 type="application/javascript" 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;version=1.8">
+
+'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 messageHandler(aEvent) {
+ connection.removeEventListener('message', messageHandler);
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ });
+
+ 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 000000000..60247ec98
--- /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 type="application/javascript" 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;version=1.8">
+
+'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 000000000..a95da104f
--- /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 type="application/javascript" 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;version=1.8">
+
+'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 000000000..557ae71a6
--- /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 type="application/javascript" 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;version=1.8">
+
+'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 000000000..8ebfd9d64
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,243 @@
+'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.indexOf('_inproc') == -1;
+ receiverIframe.setAttribute('remote', oop);
+
+ receiverIframe.setAttribute('src', receiverUrl);
+ receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+ receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+ info('Receiver loaded.');
+ });
+
+ // 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);
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+ });
+
+ 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.mozBrowserFramesEnabled', true],
+ ["network.disable.ipc.security", 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 000000000..a1d477aab
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
@@ -0,0 +1,197 @@
+'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.indexOf('_inproc') == -1;
+ receiverIframe.setAttribute('remote', oop);
+
+ receiverIframe.setAttribute('src', receiverUrl);
+ receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+ receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+ info('Receiver loaded.');
+ });
+
+ // 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);
+ }
+ }, false);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obs.notifyObservers(promise, 'setup-request-promise', null);
+ });
+
+ 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.mozBrowserFramesEnabled', true],
+ ["network.disable.ipc.security", 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 000000000..ccf0767f1
--- /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;version=1.8' 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 000000000..ccf0767f1
--- /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;version=1.8' 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 000000000..33bbcda57
--- /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;version=1.8' 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 000000000..33bbcda57
--- /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;version=1.8' 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 000000000..137a5609a
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
@@ -0,0 +1,1318 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global Services, do_register_cleanup, do_test_pending */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.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) {
+ let deferred = Promise.defer();
+
+ let timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+
+ timer.initWithCallback({
+ notify: function () {
+ deferred.resolve();
+ },
+ }, aMs, timer.TYPE_ONE_SHOT);
+
+ return deferred.promise;
+}
+
+function MockFactory(aClass) {
+ this._cls = aClass;
+}
+MockFactory.prototype = {
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw 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: function(aLock) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: function() {
+ this.reset();
+
+ let oldContract = this.unregister();
+ this.hookedMap.get(this._contractID).push(oldContract);
+ registrar.registerFactory(this.classID,
+ "",
+ this._contractID,
+ this._newFactory);
+
+ do_register_cleanup(() => { this.cleanup.apply(this); });
+ },
+
+ reset: function() {},
+
+ cleanup: function() {
+ this.reset();
+
+ this.unregister();
+ let prevContract = this.hookedMap.get(this._contractID).pop();
+
+ if (prevContract.factory) {
+ registrar.registerFactory(prevContract.classID,
+ "",
+ this._contractID,
+ prevContract.factory);
+ }
+ },
+
+ unregister: function() {
+ var classID, factory;
+
+ try {
+ classID = registrar.contractIDToCID(this._contractID);
+ factory = Cm.getClassObject(Cc[this._contractID], Ci.nsIFactory);
+ } catch (ex) {
+ classID = "";
+ factory = null;
+ }
+
+ if (factory) {
+ registrar.unregisterFactory(classID, factory);
+ }
+
+ return { classID: classID, factory: factory };
+ }
+};
+
+function MockDNSServiceInfo() {}
+MockDNSServiceInfo.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+
+ addDevice: function(device) { this.devices[device.id] = device; },
+ removeDevice: function(device) { delete this.devices[device.id]; },
+ updateDevice: function(device) { this.devices[device.id] = device; },
+ onSessionRequest: function(device, url, presentationId, controlChannel) {},
+
+ count: function() {
+ 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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {},
+ registerService: function(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {
+ this.serviceUnregistered++;
+ }.bind(this)
+ };
+ },
+ resolveService: function(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0
+ };
+ let contractHook = 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: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {},
+ registerService: function(serviceInfo, listener) {
+ deferred.resolve();
+ Assert.ok(false, "should not register service if not discoverable");
+ },
+ resolveService: function(serviceInfo, listener) {},
+ };
+
+ let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+
+ // Try register
+ provider.listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {},
+ registerService: function(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {
+ this.serviceUnregistered++;
+ }.bind(this)
+ };
+ },
+ resolveService: function(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0
+ };
+ let contractHook = 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: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(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 contractHook = 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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(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 contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(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;
+ run_next_test();
+ },
+ updateDevice: function() {},
+ removeDevice: function() {},
+ onSessionRequest: function() {},
+ };
+
+ provider.listener = listener;
+}
+
+function handleSessionRequest() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testDeviceName = "test-device-name";
+
+ Services.prefs.setCharPref(PREF_DEVICENAME, testDeviceName);
+
+ let mockDevice = createDevice("device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE);
+ let mockSDObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(serviceInfo, listener) {
+ listener.onServiceResolved(createDevice(mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ }
+ };
+
+ let mockServerObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ connect: function(deviceInfo) {
+ this.request = {
+ deviceInfo: deviceInfo,
+ };
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+ };
+ },
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer: function(version) {
+ return this.version === version;
+ }
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {
+ this.device = device;
+ },
+ };
+
+ provider.listener = listener;
+
+ let controlChannel = listener.device.establishControlChannel();
+
+ Assert.equal(mockServerObj.request.deviceInfo.id, mockDevice.host);
+ Assert.equal(mockServerObj.request.deviceInfo.address, mockDevice.host);
+ Assert.equal(mockServerObj.request.deviceInfo.port, mockDevice.port);
+ Assert.equal(mockServerObj.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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(serviceInfo, listener) {
+ listener.onServiceResolved(createDevice(mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ }
+ };
+
+ let mockServerObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ startServer: function() {},
+ sessionRequest: function() {},
+ close: function() {},
+ id: '',
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(device) {},
+ onSessionRequest: function(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url: url,
+ presentationId: presentationId,
+ };
+ }
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+ id: mockDevice.host,
+ address: mockDevice.host,
+ port: 54321,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {},
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(serviceInfo, listener) {}
+ };
+
+ let mockServerObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ startServer: function() {},
+ sessionRequest: function() {},
+ close: function() {},
+ id: '',
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {
+ Assert.ok(false, "shouldn't create any new device");
+ },
+ removeDevice: function(device) {
+ Assert.ok(false, "shouldn't remote any device");
+ },
+ updateDevice: function(device) {
+ Assert.ok(false, "shouldn't update any device");
+ },
+ onSessionRequest: function(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url: url,
+ presentationId: presentationId,
+ };
+ }
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+ id: "unknown-device.local",
+ address: "unknown-device.local",
+ port: 12345,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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 mockDevice = createDevice("device.local", 12345, "service.name", SERVICE_TYPE);
+ let mockObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ Assert.ok(false, "shouldn't perform any device discovery");
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(serviceInfo, listener) {
+ }
+ };
+ let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(createDevice("",
+ 54321,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ resolveService: function(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: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ startServer: function() {
+ Services.tm.currentThread.dispatch(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+ sessionRequest: function() {},
+ close: function() {},
+ id: '',
+ version: LATEST_VERSION,
+ isCompatibleServer: function(version) {
+ return false;
+ },
+ port: 54321,
+ certFingerprint: 'mock-cert-fingerprint',
+ listener: null,
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = 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;
+
+ 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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ resolveService: function(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: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ startServer: function() {
+ Services.tm.currentThread.dispatch(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+ sessionRequest: function() {},
+ close: function() {},
+ id: '',
+ version: LATEST_VERSION,
+ isCompatibleServer: function(version) {
+ return this.version === version;
+ },
+ port: 54321,
+ certFingerprint: 'mock-cert-fingerprint',
+ listener: null,
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = 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;
+
+ run_next_test();
+ });
+}
+
+function addDeviceDynamically() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+
+ let mockDevice = createDevice("device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE);
+ let mockObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(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 contractHook = 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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+
+ if (!this.discovered) {
+ listener.onServiceFound(mockDevice1);
+ } else {
+ listener.onServiceFound(mockDevice2);
+ }
+ this.discovered = true;
+
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {
+ listener.onDiscoveryStopped(serviceType);
+ }
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(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);
+ }
+ }
+ };
+
+ let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+
+ addDevice: function(device) {
+ Assert.ok(!this.isDeviceAdded);
+ Assert.equal(device.id, mockDevice1.host);
+ Assert.equal(device.name, mockDevice1.serviceName);
+ this.isDeviceAdded = true;
+ },
+ removeDevice: function(device) { Assert.ok(false); },
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(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: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {
+ listener.onDiscoveryStopped(serviceType);
+ }
+ };
+ },
+ registerService: function(serviceInfo, listener) {},
+ resolveService: function(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);
+ }
+ }
+ };
+
+ let contractHook = 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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(createDevice("",
+ 0,
+ mockDevice.serviceName,
+ mockDevice.serviceType));
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {}
+ };
+ },
+ registerService: function(serviceInfo, listener) {
+ this.serviceRegistered++;
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ cancel: function() {
+ this.serviceUnregistered++;
+ }.bind(this)
+ };
+ },
+ resolveService: function(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
+ };
+ let contractHook = 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: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) { this.devices.push(device); },
+ removeDevice: function(device) {},
+ updateDevice: function(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: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+ startDiscovery: function(serviceType, listener) {},
+ registerService: function(serviceInfo, listener) {
+ Assert.ok(isRetrying, "register service after retrying startServer");
+ provider.listener = null;
+ run_next_test();
+ },
+ resolveService: function(serviceInfo, listener) {}
+ };
+
+ let mockServerObj = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+ startServer: function(encrypted, port) {
+ if (!isRetrying) {
+ isRetrying = true;
+ Services.tm.currentThread.dispatch(() => {
+ this.listener.onServerStopped(Cr.NS_ERROR_FAILURE);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ } else {
+ this.port = 54321;
+ Services.tm.currentThread.dispatch(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+ sessionRequest: function() {},
+ close: function() {},
+ id: '',
+ version: LATEST_VERSION,
+ port: 0,
+ certFingerprint: 'mock-cert-fingerprint',
+ listener: null,
+ };
+
+ let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+ Ci.nsISupportsWeakReference]),
+ addDevice: function(device) {},
+ removeDevice: function(device) {},
+ updateDevice: function(device) {},
+ onSessionRequest: function(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);
+
+ let infoHook = new ContractHook(INFO_CONTRACT_ID, MockDNSServiceInfo);
+
+ do_register_cleanup(() => {
+ 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 000000000..68f07df65
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -0,0 +1,244 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.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: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+ sendOffer: function(offer) {},
+ sendAnswer: function(answer) {},
+ disconnect: function() {},
+ launch: function() {},
+ terminate: function() {},
+ reconnect: function() {},
+ set listener(listener) {},
+ get listener() {},
+};
+
+var testProvider = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
+
+ forceDiscovery: function() {
+ },
+ set listener(listener) {
+ },
+ get listener() {
+ },
+};
+
+const forbiddenRequestedUrl = 'http://example.com';
+var testDevice = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+ id: 'id',
+ name: 'name',
+ type: 'type',
+ establishControlChannel: function(url, presentationId) {
+ return null;
+ },
+ disconnect: function() {},
+ isRequestedUrlSupported: function(requestedUrl) {
+ return forbiddenRequestedUrl !== requestedUrl;
+ },
+};
+
+function addProvider() {
+ Object.defineProperty(testProvider, 'listener', {
+ configurable: true,
+ set: function(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', false);
+ 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', false);
+ 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, false);
+ 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', false);
+ manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel);
+}
+
+function terminateRequest() {
+ let testUrl = 'http://www.example.org/';
+ let testPresentationId = 'test-presentation-id';
+ let testControlChannel = new TestPresentationControlChannel();
+ let testIsFromReceiver = true;
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, topic);
+
+ let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest);
+
+ Assert.equal(request.device.id, testDevice.id, 'expected device');
+ Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+ Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver');
+
+ run_next_test();
+ }, 'presentation-terminate-request', false);
+ manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onTerminateRequest(testDevice, testPresentationId,
+ testControlChannel, testIsFromReceiver);
+}
+
+function 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', false);
+ 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', false);
+ manager.QueryInterface(Ci.nsIPresentationDeviceListener).removeDevice(testDevice);
+}
+
+function removeProvider() {
+ Object.defineProperty(testProvider, 'listener', {
+ configurable: true,
+ set: function(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);
+
+function run_test() {
+ run_next_test();
+}
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 000000000..8e207bc22
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
@@ -0,0 +1,198 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC } = Components;
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init");
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+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, false);
+
+const serverChannelDescription = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+ type: 1,
+ tcpAddress: addresses,
+};
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+const clientCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+ notifyTransportReady: function () {
+ Assert.ok(true, "Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed: function (aReason) {
+ Assert.ok(true, "Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData: function(aData) {
+ Assert.equal(aData, serverMessage, "Client transport receives data.");
+ run_next_test();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+ notifyTransportReady: function () {
+ Assert.ok(true, "Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed: function (aReason) {
+ Assert.ok(true, "Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData: function(aData) {
+ Assert.equal(aData, clientMessage, "Server transport receives data.");
+ run_next_test();
+ },
+};
+
+const clientListener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+ onSessionTransport(aTransport) {
+ Assert.ok(true, "Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+
+ if (serverTransport) {
+ run_next_test();
+ }
+ }
+}
+
+const serverListener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: function(aSocket, aTransport) {
+ print("Test server gets a client connection.");
+ serverBuilder = Cc["@mozilla.org/presentation/presentationtcpsessiontransport;1"]
+ .createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+ serverBuilder.buildTCPSenderTransport(aTransport, serverListener);
+ },
+ onStopListening: function(aSocket) {
+ print("Test server stops listening.");
+ },
+ close: function() {
+ 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 000000000..fcaf34da6
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -0,0 +1,236 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components,Assert,run_next_test,add_test,do_execute_soon */
+
+'use strict';
+
+const { utils: Cu, results: Cr } = Components;
+
+/* globals ControllerStateMachine */
+Cu.import('resource://gre/modules/presentation/ControllerStateMachine.jsm');
+/* globals ReceiverStateMachine */
+Cu.import('resource://gre/modules/presentation/ReceiverStateMachine.jsm');
+/* globals State */
+Cu.import('resource://gre/modules/presentation/StateMachineHelper.jsm');
+
+const testControllerId = 'test-controller-id';
+const testPresentationId = 'test-presentation-id';
+const testUrl = 'http://example.org';
+
+let mockControllerChannel = {};
+let mockReceiverChannel = {};
+
+let controllerState = new ControllerStateMachine(mockControllerChannel, testControllerId);
+let receiverState = new ReceiverStateMachine(mockReceiverChannel);
+
+mockControllerChannel.sendCommand = function(command) {
+ do_execute_soon(function() {
+ receiverState.onCommand(command);
+ });
+};
+
+mockReceiverChannel.sendCommand = function(command) {
+ do_execute_soon(function() {
+ controllerState.onCommand(command);
+ });
+};
+
+function connect() {
+ Assert.equal(controllerState.state, State.INIT, 'controller in init state');
+ Assert.equal(receiverState.state, State.INIT, 'receiver in init state');
+ // step 1: underlying connection is ready
+ controllerState.onChannelReady();
+ Assert.equal(controllerState.state, State.CONNECTING, 'controller in connecting state');
+ receiverState.onChannelReady();
+ Assert.equal(receiverState.state, State.CONNECTING, 'receiver in connecting state');
+
+ // step 2: receiver reply to connect command
+ mockReceiverChannel.notifyDeviceConnected = function(deviceId) {
+ Assert.equal(deviceId, testControllerId, 'receiver connect to mock controller');
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+ // step 3: controller receive connect-ack command
+ mockControllerChannel.notifyDeviceConnected = function() {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ run_next_test();
+ };
+ };
+}
+
+function launch() {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+ controllerState.launch(testPresentationId, testUrl);
+ mockReceiverChannel.notifyLaunch = function(presentationId, url) {
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+ Assert.equal(url, testUrl, 'expected url received');
+
+ mockControllerChannel.notifyLaunch = function(presentationId) {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+ run_next_test();
+ };
+ };
+}
+
+function terminateByController() {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+ controllerState.terminate(testPresentationId);
+ mockReceiverChannel.notifyTerminate = function(presentationId) {
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+ mockControllerChannel.notifyTerminate = function(presentationId) {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+ run_next_test();
+ };
+
+ receiverState.terminateAck(presentationId);
+ };
+}
+
+function terminateByReceiver() {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+ receiverState.terminate(testPresentationId);
+ mockControllerChannel.notifyTerminate = function(presentationId) {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+ mockReceiverChannel.notifyTerminate = function(presentationId) {
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+ Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+ run_next_test();
+ };
+
+ controllerState.terminateAck(presentationId);
+ };
+}
+
+function exchangeSDP() {
+ Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+ Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+ const testOffer = 'test-offer';
+ const testAnswer = 'test-answer';
+ const testIceCandidate = 'test-ice-candidate';
+ controllerState.sendOffer(testOffer);
+ mockReceiverChannel.notifyOffer = function(offer) {
+ Assert.equal(offer, testOffer, 'expected offer received');
+
+ receiverState.sendAnswer(testAnswer);
+ mockControllerChannel.notifyAnswer = function(answer) {
+ Assert.equal(answer, testAnswer, 'expected answer received');
+
+ controllerState.updateIceCandidate(testIceCandidate);
+ mockReceiverChannel.notifyIceCandidate = function(candidate) {
+ Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in receiver');
+
+ receiverState.updateIceCandidate(testIceCandidate);
+ mockControllerChannel.notifyIceCandidate = function(candidate) {
+ Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in controller');
+
+ run_next_test();
+ };
+ };
+ };
+ };
+}
+
+function disconnect() {
+ // step 1: controller send disconnect command
+ controllerState.onChannelClosed(Cr.NS_OK, false);
+ Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+ mockReceiverChannel.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(reason) {
+ Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+ Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+ run_next_test();
+ };
+ controllerState.onChannelClosed(Cr.NS_OK, true);
+ };
+}
+
+function receiverDisconnect() {
+ // initial state: controller and receiver are connected
+ controllerState.state = State.CONNECTED;
+ receiverState.state = State.CONNECTED;
+
+ // step 1: controller send disconnect command
+ receiverState.onChannelClosed(Cr.NS_OK, false);
+ Assert.equal(receiverState.state, State.CLOSING, 'receiver in closing state');
+
+ mockControllerChannel.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(reason) {
+ Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+ Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+ run_next_test();
+ };
+ receiverState.onChannelClosed(Cr.NS_OK, true);
+ };
+}
+
+function abnormalDisconnect() {
+ // initial state: controller and receiver are connected
+ controllerState.state = State.CONNECTED;
+ receiverState.state = State.CONNECTED;
+
+ const testErrorReason = Cr.NS_ERROR_FAILURE;
+ // step 1: controller send disconnect command
+ controllerState.onChannelClosed(testErrorReason, false);
+ Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+ mockReceiverChannel.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(reason) {
+ Assert.equal(reason, testErrorReason, 'receive abnormal close reason');
+ Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+ run_next_test();
+ };
+ controllerState.onChannelClosed(Cr.NS_OK, true);
+ };
+}
+
+add_test(connect);
+add_test(launch);
+add_test(terminateByController);
+add_test(terminateByReceiver);
+add_test(exchangeSDP);
+add_test(disconnect);
+add_test(receiverDisconnect);
+add_test(abnormalDisconnect);
+
+function run_test() { // jshint ignore:line
+ run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
new file mode 100644
index 000000000..5f3df584d
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -0,0 +1,398 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+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() {
+ do_print('got expected: ' + name);
+ if (++successCount === names.length)
+ run_next_test();
+ };
+ });
+ return funcs;
+}
+
+function TestDescription(aType, aTcpAddress, aTcpPort) {
+ this.type = aType;
+ this.tcpAddress = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ for (let address of aTcpAddress) {
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"]
+ .createInstance(Ci.nsISupportsCString);
+ wrapper.data = address;
+ this.tcpAddress.appendElement(wrapper, false);
+ }
+ this.tcpPort = aTcpPort;
+}
+
+TestDescription.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.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: function() {
+ 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: function(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: function(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: function(aAnswer) {
+ Assert.ok(false, 'get answer');
+ },
+ onIceCandidate: function(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: function() {
+ Assert.equal(this.status, 'created', '0. presenterControlChannel: opened');
+ this.status = 'opened';
+ },
+ notifyDisconnected: function(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: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+ },
+ onReconnectRequest: function(deviceInfo, url, presentationId, controlChannel) {
+ Assert.equal(url, 'http://example.com', 'expected url');
+ Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+ yayFuncs.presenterControlChannelReconnect();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlServerListener]),
+ };
+
+ let presenterDeviceInfo = {
+ id: 'presentatorID',
+ address: '127.0.0.1',
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+ };
+
+ let controllerControlChannel = pcs.connect(presenterDeviceInfo);
+
+ controllerControlChannel.listener = {
+ status: 'created',
+ onOffer: function(offer) {
+ Assert.ok(false, 'get offer');
+ },
+ onAnswer: function(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: function(aCandidate) {
+ Assert.ok(false, 'get ICE candidate');
+ },
+ notifyConnected: function() {
+ 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: function(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: function() {
+ reconnectControllerControlChannel.reconnect('testPresentationId', 'http://example.com');
+ },
+ notifyReconnected: function() {
+ yayFuncs.controllerControlChannelReconnect();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+}
+
+function terminateRequest() {
+ let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+ 'controllerControlChannelDisconnected',
+ 'presenterControlChannelDisconnected',
+ 'terminatedByController',
+ 'terminatedByReceiver']);
+ let controllerControlChannel;
+ let terminatePhase = 'controller';
+
+ pcs.listener = {
+ onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+ Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+ Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+ controlChannel.terminate(presentationId); // Reply terminate ack.
+
+ if (terminatePhase === 'controller') {
+ controllerControlChannel = controlChannel;
+ Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+ Assert.equal(isFromReceiver, false, 'expected request from controller');
+ yayFuncs.terminatedByController();
+
+ controllerControlChannel.listener = {
+ notifyConnected: function() {
+ Assert.ok(true, 'control channel notify connected');
+ yayFuncs.controllerControlChannelConnected();
+
+ terminatePhase = 'receiver';
+ controllerControlChannel.terminate('testPresentationId');
+ },
+ notifyDisconnected: function(aReason) {
+ Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+ } else {
+ Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id');
+ Assert.equal(isFromReceiver, true, 'expected request from receiver');
+ yayFuncs.terminatedByReceiver();
+ presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+ };
+
+ let presenterDeviceInfo = {
+ id: 'presentatorID',
+ address: '127.0.0.1',
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected: function() {
+ presenterControlChannel.terminate('testPresentationId');
+ },
+ notifyDisconnected: function(aReason) {
+ Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+}
+
+function terminateRequestAbnormal() {
+ let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+ 'controllerControlChannelDisconnected',
+ 'presenterControlChannelDisconnected']);
+ let controllerControlChannel;
+
+ pcs.listener = {
+ onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+ Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+ Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+ Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+ Assert.equal(isFromReceiver, false, 'expected request from controller');
+ controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack.
+
+ controllerControlChannel = controlChannel;
+
+ controllerControlChannel.listener = {
+ notifyConnected: function() {
+ Assert.ok(true, 'control channel notify connected');
+ yayFuncs.controllerControlChannelConnected();
+ },
+ notifyDisconnected: function(aReason) {
+ Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error');
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+ };
+
+ let presenterDeviceInfo = {
+ id: 'presentatorID',
+ address: '127.0.0.1',
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected: function() {
+ presenterControlChannel.terminate('testPresentationId');
+ },
+ notifyDisconnected: function(aReason) {
+ Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error');
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+ };
+}
+
+function setOffline() {
+ pcs.listener = {
+ onServerReady: function(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: function() {
+ 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: function(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);
+
+ do_register_cleanup(() => {
+ 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 000000000..8a9c305a0
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+tail =
+
+[test_multicast_dns_device_provider.js]
+[test_presentation_device_manager.js]
+[test_presentation_session_transport.js]
+[test_tcp_control_channel.js]
+[test_presentation_state_machine.js]