Bug 1228508 - Part 2, maintain the set of availability objects. r=smaug
authorShih-Chiang Chien <schien@mozilla.com>
Wed, 17 Aug 2016 16:15:32 +0800
changeset 353326 e413ced9ff12901067ac568391f085518b69f9ac
parent 353325 ca07578f0c6443f10aa4e94a090b2cc55f25588a
child 353327 7e1f25f59298f7dbed0862f0e24cdf6cca8d9234
push id6570
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:26:13 +0000
treeherdermozilla-beta@f455459b2ae5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1228508
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1228508 - Part 2, maintain the set of availability objects. r=smaug MozReview-Commit-ID: 8JNWvnfsMU7
dom/presentation/AvailabilityCollection.cpp
dom/presentation/AvailabilityCollection.h
dom/presentation/PresentationAvailability.cpp
dom/presentation/PresentationAvailability.h
dom/presentation/PresentationRequest.cpp
dom/presentation/PresentationRequest.h
dom/presentation/moz.build
dom/presentation/tests/mochitest/test_presentation_availability.html
new file mode 100644
--- /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 nsAString& aUrl)
+{
+  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, aUrl)) {
+      RefPtr<PresentationAvailability> matchedAvailability = availability.get();
+      return matchedAvailability.forget();
+    }
+  }
+
+
+  return nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /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 nsAString& aUrl);
+
+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
--- a/dom/presentation/PresentationAvailability.cpp
+++ b/dom/presentation/PresentationAvailability.cpp
@@ -7,51 +7,55 @@
 #include "PresentationAvailability.h"
 
 #include "mozilla/dom/PresentationAvailabilityBinding.h"
 #include "mozilla/dom/Promise.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(mPromise)
+  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(mPromise);
+  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 nsAString& aUrl,
                                  RefPtr<Promise>& aPromise)
 {
   RefPtr<PresentationAvailability> availability =
-    new PresentationAvailability(aWindow);
+    new PresentationAvailability(aWindow, aUrl);
   return NS_WARN_IF(!availability->Init(aPromise)) ? nullptr
                                                    : availability.forget();
 }
 
-PresentationAvailability::PresentationAvailability(nsPIDOMWindowInner* aWindow)
+PresentationAvailability::PresentationAvailability(nsPIDOMWindowInner* aWindow,
+                                                   const nsAString& aUrl)
   : DOMEventTargetHelper(aWindow)
   , mIsAvailable(false)
+  , mUrl(aUrl)
 {
 }
 
 PresentationAvailability::~PresentationAvailability()
 {
   Shutdown();
 }
 
@@ -68,23 +72,33 @@ PresentationAvailability::Init(RefPtr<Pr
   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;
   }
 
-  mPromise = aPromise;
+  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;
   }
 
   nsresult rv = service->UnregisterAvailabilityListener(this);
   NS_WARN_IF(NS_FAILED(rv));
@@ -100,16 +114,42 @@ PresentationAvailability::DisconnectFrom
 /* virtual */ JSObject*
 PresentationAvailability::WrapObject(JSContext* aCx,
                                      JS::Handle<JSObject*> aGivenProto)
 {
   return PresentationAvailabilityBinding::Wrap(aCx, this, aGivenProto);
 }
 
 bool
+PresentationAvailability::Equals(const uint64_t aWindowID,
+                                 const nsAString& aUrl) const
+{
+  if (GetOwner() && GetOwner()->WindowID() == aWindowID &&
+      mUrl.Equals(aUrl)) {
+    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(bool aIsAvailable)
 {
@@ -117,22 +157,31 @@ PresentationAvailability::NotifyAvailabl
                                     <bool>(this,
                                            &PresentationAvailability::UpdateAvailabilityAndDispatchEvent,
                                            aIsAvailable));
 }
 
 void
 PresentationAvailability::UpdateAvailabilityAndDispatchEvent(bool aIsAvailable)
 {
+  PRES_DEBUG("%s:id[%s]\n", __func__,
+             NS_ConvertUTF16toUTF8(mUrl).get());
   bool isChanged = (aIsAvailable != mIsAvailable);
 
   mIsAvailable = aIsAvailable;
 
-  if (mPromise) {
-    mPromise->MaybeResolve(this);
-    mPromise = nullptr;
+  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) {
     NS_WARN_IF(NS_FAILED(DispatchTrustedEvent(NS_LITERAL_STRING("change"))));
   }
 }
--- a/dom/presentation/PresentationAvailability.h
+++ b/dom/presentation/PresentationAvailability.h
@@ -13,49 +13,61 @@
 
 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 nsAString& aUrl,
          RefPtr<Promise>& aPromise);
 
   virtual void DisconnectFromOwner() override;
 
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
+  bool Equals(const uint64_t aWindowID, const nsAString& aUrl) const;
+
+  bool IsCachedValueReady();
+
+  void EnqueuePromise(RefPtr<Promise>& aPromise);
+
   // WebIDL (public APIs)
   bool Value() const;
 
   IMPL_EVENT_HANDLER(change);
 
 private:
-  explicit PresentationAvailability(nsPIDOMWindowInner* aWindow);
+  explicit PresentationAvailability(nsPIDOMWindowInner* aWindow,
+                                    const nsAString& aUrl);
 
   virtual ~PresentationAvailability();
 
   bool Init(RefPtr<Promise>& aPromise);
 
   void Shutdown();
 
   void UpdateAvailabilityAndDispatchEvent(bool aIsAvailable);
 
   bool mIsAvailable;
 
-  RefPtr<Promise> mPromise;
+  nsTArray<RefPtr<Promise>> mPromises;
+
+  nsString mUrl;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_PresentationAvailability_h
--- a/dom/presentation/PresentationRequest.cpp
+++ b/dom/presentation/PresentationRequest.cpp
@@ -1,16 +1,17 @@
 /* -*- 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/PresentationRequestBinding.h"
 #include "mozilla/dom/PresentationConnectionAvailableEvent.h"
 #include "mozilla/dom/Promise.h"
 #include "mozIThirdPartyUtil.h"
 #include "nsContentSecurityManager.h"
 #include "nsCycleCollectionParticipant.h"
@@ -335,29 +336,60 @@ PresentationRequest::GetAvailability(Err
     return promise.forget();
   }
 
   if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
     promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
     return promise.forget();
   }
 
-  // TODO
-  // Search the set of availability object and resolve
-  // promise with the one had same presentation URLs.
+  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 =
-    PresentationAvailability::Create(GetOwner(), promise);
+    collection->Find(GetOwner()->WindowID(), mUrl);
 
   if (!availability) {
-    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-    return promise.forget();
+    availability = PresentationAvailability::Create(GetOwner(), mUrl, aPromise);
+  } else {
+    PRES_DEBUG(">resolve with same object:id[%s]\n",
+               NS_ConvertUTF16toUTF8(mUrl).get());
+
+    // 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);
   }
 
-  return promise.forget();
+  if (!availability) {
+    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
 }
 
 nsresult
 PresentationRequest::DispatchConnectionAvailableEvent(PresentationConnection* aConnection)
 {
   PresentationConnectionAvailableEventInit init;
   init.mConnection = aConnection;
 
--- a/dom/presentation/PresentationRequest.h
+++ b/dom/presentation/PresentationRequest.h
@@ -51,16 +51,18 @@ private:
 
   ~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);
 
   nsString mUrl;
 };
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -27,16 +27,17 @@ EXPORTS.mozilla.dom += [
     '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',
--- a/dom/presentation/tests/mochitest/test_presentation_availability.html
+++ b/dom/presentation/tests/mochitest/test_presentation_availability.html
@@ -56,16 +56,26 @@ function testInitialAvailable() {
     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');
@@ -95,16 +105,17 @@ function 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(teardown);
 }
 
 SimpleTest.waitForExplicitFinish();
 SpecialPowers.pushPermissions([
   {type: "presentation-device-manage", allow: false, context: document},