Bug 1228508 - Part 2, maintain the set of availability objects. r=smaug draft
authorShih-Chiang Chien <schien@mozilla.com>
Wed, 17 Aug 2016 16:15:32 +0800
changeset 407881 004360c49a899273cc582ad4a2e886be167e9cac
parent 407880 076e26f256faed64f44c035d2a1b8e317052af63
child 529975 5b09036ebdc2231ae444883847908211e87cb240
push id28071
push userschien@mozilla.com
push dateWed, 31 Aug 2016 06:02:21 +0000
reviewerssmaug
bugs1228508
milestone51.0a1
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},