diff options
Diffstat (limited to 'dom/presentation')
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] |