diff options
Diffstat (limited to 'dom/power')
-rw-r--r-- | dom/power/PowerManagerService.cpp | 138 | ||||
-rw-r--r-- | dom/power/PowerManagerService.h | 57 | ||||
-rw-r--r-- | dom/power/WakeLock.cpp | 181 | ||||
-rw-r--r-- | dom/power/WakeLock.h | 74 | ||||
-rw-r--r-- | dom/power/WakeLockJS.cpp | 288 | ||||
-rw-r--r-- | dom/power/WakeLockJS.h | 103 | ||||
-rw-r--r-- | dom/power/WakeLockSentinel.cpp | 100 | ||||
-rw-r--r-- | dom/power/WakeLockSentinel.h | 79 | ||||
-rw-r--r-- | dom/power/components.conf | 17 | ||||
-rw-r--r-- | dom/power/moz.build | 39 | ||||
-rw-r--r-- | dom/power/nsIDOMWakeLockListener.idl | 29 | ||||
-rw-r--r-- | dom/power/nsIPowerManagerService.idl | 32 | ||||
-rw-r--r-- | dom/power/nsIWakeLock.idl | 12 | ||||
-rw-r--r-- | dom/power/tests/mochitest.toml | 9 | ||||
-rw-r--r-- | dom/power/tests/test_dynamic_pref_change.html | 34 | ||||
-rw-r--r-- | dom/power/tests/test_wakelock_default_permission.html | 20 |
16 files changed, 1212 insertions, 0 deletions
diff --git a/dom/power/PowerManagerService.cpp b/dom/power/PowerManagerService.cpp new file mode 100644 index 0000000000..ad3eadce8e --- /dev/null +++ b/dom/power/PowerManagerService.cpp @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Hal.h" +#include "mozilla/HalWakeLock.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/Preferences.h" +#include "nsIDOMWakeLockListener.h" +#include "PowerManagerService.h" +#include "WakeLock.h" + +// For _exit(). +#ifdef XP_WIN +# include <process.h> +#else +# include <unistd.h> +#endif + +namespace mozilla::dom::power { + +using namespace hal; + +NS_IMPL_ISUPPORTS(PowerManagerService, nsIPowerManagerService) + +/* static */ +StaticRefPtr<PowerManagerService> PowerManagerService::sSingleton; + +/* static */ +already_AddRefed<PowerManagerService> PowerManagerService::GetInstance() { + if (!sSingleton) { + sSingleton = new PowerManagerService(); + sSingleton->Init(); + ClearOnShutdown(&sSingleton); + } + + RefPtr<PowerManagerService> service = sSingleton.get(); + return service.forget(); +} + +void PowerManagerService::Init() { RegisterWakeLockObserver(this); } + +PowerManagerService::~PowerManagerService() { + UnregisterWakeLockObserver(this); +} + +void PowerManagerService::ComputeWakeLockState( + const WakeLockInformation& aWakeLockInfo, nsAString& aState) { + WakeLockState state = hal::ComputeWakeLockState(aWakeLockInfo.numLocks(), + aWakeLockInfo.numHidden()); + switch (state) { + case WAKE_LOCK_STATE_UNLOCKED: + aState.AssignLiteral("unlocked"); + break; + case WAKE_LOCK_STATE_HIDDEN: + aState.AssignLiteral("locked-background"); + break; + case WAKE_LOCK_STATE_VISIBLE: + aState.AssignLiteral("locked-foreground"); + break; + } +} + +void PowerManagerService::Notify(const WakeLockInformation& aWakeLockInfo) { + nsAutoString state; + ComputeWakeLockState(aWakeLockInfo, state); + + /** + * Copy the listeners list before we walk through the callbacks + * because the callbacks may install new listeners. We expect no + * more than one listener per window, so it shouldn't be too long. + */ + const CopyableAutoTArray<nsCOMPtr<nsIDOMMozWakeLockListener>, 2> listeners = + mWakeLockListeners; + + for (uint32_t i = 0; i < listeners.Length(); ++i) { + listeners[i]->Callback(aWakeLockInfo.topic(), state); + } +} + +NS_IMETHODIMP +PowerManagerService::AddWakeLockListener(nsIDOMMozWakeLockListener* aListener) { + if (mWakeLockListeners.Contains(aListener)) return NS_OK; + + mWakeLockListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +PowerManagerService::RemoveWakeLockListener( + nsIDOMMozWakeLockListener* aListener) { + mWakeLockListeners.RemoveElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +PowerManagerService::GetWakeLockState(const nsAString& aTopic, + nsAString& aState) { + WakeLockInformation info; + GetWakeLockInfo(aTopic, &info); + + ComputeWakeLockState(info, aState); + + return NS_OK; +} + +already_AddRefed<WakeLock> PowerManagerService::NewWakeLock( + const nsAString& aTopic, nsPIDOMWindowInner* aWindow, + mozilla::ErrorResult& aRv) { + RefPtr<WakeLock> wakelock = new WakeLock(); + aRv = wakelock->Init(aTopic, aWindow); + if (aRv.Failed()) { + return nullptr; + } + + return wakelock.forget(); +} + +NS_IMETHODIMP +PowerManagerService::NewWakeLock(const nsAString& aTopic, + mozIDOMWindow* aWindow, + nsIWakeLock** aWakeLock) { + mozilla::ErrorResult rv; + RefPtr<WakeLock> wakelock = + NewWakeLock(aTopic, nsPIDOMWindowInner::From(aWindow), rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + wakelock.forget(aWakeLock); + return NS_OK; +} + +} // namespace mozilla::dom::power diff --git a/dom/power/PowerManagerService.h b/dom/power/PowerManagerService.h new file mode 100644 index 0000000000..1522342201 --- /dev/null +++ b/dom/power/PowerManagerService.h @@ -0,0 +1,57 @@ +/* -*- 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_power_PowerManagerService_h +#define mozilla_dom_power_PowerManagerService_h + +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "nsIDOMWakeLockListener.h" +#include "nsIPowerManagerService.h" +#include "mozilla/HalWakeLockInformation.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/WakeLock.h" + +namespace mozilla::dom { + +class ContentParent; + +namespace power { + +class PowerManagerService : public nsIPowerManagerService, + public hal::WakeLockObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPOWERMANAGERSERVICE + + PowerManagerService() = default; + + static already_AddRefed<PowerManagerService> GetInstance(); + + void Init(); + + // Implement WakeLockObserver + void Notify(const hal::WakeLockInformation& aWakeLockInfo) override; + + already_AddRefed<WakeLock> NewWakeLock(const nsAString& aTopic, + nsPIDOMWindowInner* aWindow, + mozilla::ErrorResult& aRv); + + private: + ~PowerManagerService(); + + void ComputeWakeLockState(const hal::WakeLockInformation& aWakeLockInfo, + nsAString& aState); + + static StaticRefPtr<PowerManagerService> sSingleton; + + nsTArray<nsCOMPtr<nsIDOMMozWakeLockListener>> mWakeLockListeners; +}; + +} // namespace power +} // namespace mozilla::dom + +#endif // mozilla_dom_power_PowerManagerService_h diff --git a/dom/power/WakeLock.cpp b/dom/power/WakeLock.cpp new file mode 100644 index 0000000000..b381b3c627 --- /dev/null +++ b/dom/power/WakeLock.cpp @@ -0,0 +1,181 @@ +/* -*- 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 "WakeLock.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Event.h" // for Event +#include "mozilla/Hal.h" +#include "mozilla/HalWakeLock.h" +#include "nsError.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIPropertyBag2.h" + +using namespace mozilla::hal; + +namespace mozilla::dom { + +NS_INTERFACE_MAP_BEGIN(WakeLock) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIWakeLock) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(WakeLock) +NS_IMPL_RELEASE(WakeLock) + +WakeLock::~WakeLock() { + DoUnlock(); + DetachEventListener(); +} + +nsresult WakeLock::Init(const nsAString& aTopic, nsPIDOMWindowInner* aWindow) { + // Don't Init() a WakeLock twice. + MOZ_ASSERT(mTopic.IsEmpty()); + + if (aTopic.IsEmpty()) { + return NS_ERROR_INVALID_ARG; + } + + mTopic.Assign(aTopic); + + mWindow = do_GetWeakReference(aWindow); + + /** + * Null windows are allowed. A wake lock without associated window + * is always considered invisible. + */ + if (aWindow) { + nsCOMPtr<Document> doc = aWindow->GetExtantDoc(); + NS_ENSURE_STATE(doc); + mHidden = doc->Hidden(); + } + + AttachEventListener(); + DoLock(); + + return NS_OK; +} + +void WakeLock::DoLock() { + if (!mLocked) { + // Change the flag immediately to prevent recursive reentering + mLocked = true; + + hal::ModifyWakeLock( + mTopic, hal::WAKE_LOCK_ADD_ONE, + mHidden ? hal::WAKE_LOCK_ADD_ONE : hal::WAKE_LOCK_NO_CHANGE); + } +} + +void WakeLock::DoUnlock() { + if (mLocked) { + // Change the flag immediately to prevent recursive reentering + mLocked = false; + + hal::ModifyWakeLock( + mTopic, hal::WAKE_LOCK_REMOVE_ONE, + mHidden ? hal::WAKE_LOCK_REMOVE_ONE : hal::WAKE_LOCK_NO_CHANGE); + } +} + +void WakeLock::AttachEventListener() { + if (nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow)) { + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (doc) { + doc->AddSystemEventListener(u"visibilitychange"_ns, this, + /* useCapture = */ true, + /* wantsUntrusted = */ false); + + nsCOMPtr<EventTarget> target = do_QueryInterface(window); + target->AddSystemEventListener(u"pagehide"_ns, this, + /* useCapture = */ true, + /* wantsUntrusted = */ false); + target->AddSystemEventListener(u"pageshow"_ns, this, + /* useCapture = */ true, + /* wantsUntrusted = */ false); + } + } +} + +void WakeLock::DetachEventListener() { + if (nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow)) { + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (doc) { + doc->RemoveSystemEventListener(u"visibilitychange"_ns, this, + /* useCapture = */ true); + nsCOMPtr<EventTarget> target = do_QueryInterface(window); + target->RemoveSystemEventListener(u"pagehide"_ns, this, + /* useCapture = */ true); + target->RemoveSystemEventListener(u"pageshow"_ns, this, + /* useCapture = */ true); + } + } +} + +void WakeLock::Unlock(ErrorResult& aRv) { + /* + * We throw NS_ERROR_DOM_INVALID_STATE_ERR on double unlock. + */ + if (!mLocked) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + DoUnlock(); + DetachEventListener(); +} + +void WakeLock::GetTopic(nsAString& aTopic) { aTopic.Assign(mTopic); } + +NS_IMETHODIMP +WakeLock::HandleEvent(Event* aEvent) { + nsAutoString type; + aEvent->GetType(type); + + if (type.EqualsLiteral("visibilitychange")) { + nsCOMPtr<Document> doc = do_QueryInterface(aEvent->GetTarget()); + NS_ENSURE_STATE(doc); + + bool oldHidden = mHidden; + mHidden = doc->Hidden(); + + if (mLocked && oldHidden != mHidden) { + hal::ModifyWakeLock( + mTopic, hal::WAKE_LOCK_NO_CHANGE, + mHidden ? hal::WAKE_LOCK_ADD_ONE : hal::WAKE_LOCK_REMOVE_ONE); + } + + return NS_OK; + } + + if (type.EqualsLiteral("pagehide")) { + DoUnlock(); + return NS_OK; + } + + if (type.EqualsLiteral("pageshow")) { + DoLock(); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +WakeLock::Unlock() { + ErrorResult error; + Unlock(error); + return error.StealNSResult(); +} + +nsPIDOMWindowInner* WakeLock::GetParentObject() const { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mWindow); + return window; +} + +} // namespace mozilla::dom diff --git a/dom/power/WakeLock.h b/dom/power/WakeLock.h new file mode 100644 index 0000000000..0f14c3d7d4 --- /dev/null +++ b/dom/power/WakeLock.h @@ -0,0 +1,74 @@ +/* -*- 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_power_WakeLock_h +#define mozilla_dom_power_WakeLock_h + +#include "nsCOMPtr.h" +#include "nsIDOMEventListener.h" +#include "nsIWakeLock.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class Document; + +class WakeLock final : public nsIDOMEventListener, + public nsSupportsWeakReference, + public nsIWakeLock { + public: + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_NSIWAKELOCK + + NS_DECL_ISUPPORTS + + // Note: WakeLock lives for the lifetime of the document in order to avoid + // exposing GC behavior to pages. This means that + // |var foo = navigator.requestWakeLock('cpu'); foo = null;| + // doesn't unlock the 'cpu' resource. + + WakeLock() = default; + + // Initialize this wake lock on behalf of the given window. Null windows are + // allowed; a lock without an associated window is always considered + // invisible. + nsresult Init(const nsAString& aTopic, nsPIDOMWindowInner* aWindow); + + // WebIDL methods + + nsPIDOMWindowInner* GetParentObject() const; + + void GetTopic(nsAString& aTopic); + + void Unlock(ErrorResult& aRv); + + private: + virtual ~WakeLock(); + + void DoUnlock(); + void DoLock(); + void AttachEventListener(); + void DetachEventListener(); + + bool mLocked = false; + bool mHidden = true; + + nsString mTopic; + + // window that this was created for. Weak reference. + nsWeakPtr mWindow; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_power_WakeLock_h diff --git a/dom/power/WakeLockJS.cpp b/dom/power/WakeLockJS.cpp new file mode 100644 index 0000000000..50e4a716c0 --- /dev/null +++ b/dom/power/WakeLockJS.cpp @@ -0,0 +1,288 @@ +/* -*- 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 "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WakeLockBinding.h" +#include "mozilla/Hal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsError.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" +#include "nsPIDOMWindow.h" +#include "nsContentPermissionHelper.h" +#include "nsServiceManagerUtils.h" +#include "nscore.h" +#include "WakeLock.h" +#include "WakeLockJS.h" +#include "WakeLockSentinel.h" + +namespace mozilla::dom { + +static mozilla::LazyLogModule sLogger("ScreenWakeLock"); + +#define MIN_BATTERY_LEVEL 0.05 + +nsLiteralCString WakeLockJS::GetRequestErrorMessage(RequestError aRv) { + switch (aRv) { + case RequestError::DocInactive: + return "The requesting document is inactive."_ns; + case RequestError::DocHidden: + return "The requesting document is hidden."_ns; + case RequestError::PolicyDisallowed: + return "A permissions policy does not allow screen-wake-lock for the requesting document."_ns; + case RequestError::PrefDisabled: + return "The pref dom.screenwakelock.enabled is disabled."_ns; + case RequestError::InternalFailure: + return "A browser-internal error occured."_ns; + case RequestError::PermissionDenied: + return "Permission to request screen-wake-lock was denied."_ns; + default: + MOZ_ASSERT_UNREACHABLE("Unknown error reason"); + return "Unknown error"_ns; + } +} + +// https://w3c.github.io/screen-wake-lock/#the-request-method steps 2-5 +WakeLockJS::RequestError WakeLockJS::WakeLockAllowedForDocument( + Document* aDoc) { + if (!aDoc) { + return RequestError::InternalFailure; + } + + // Step 2. check policy-controlled feature screen-wake-lock + if (!FeaturePolicyUtils::IsFeatureAllowed(aDoc, u"screen-wake-lock"_ns)) { + return RequestError::PolicyDisallowed; + } + + // Step 3. Deny wake lock for user agent specific reasons + if (!StaticPrefs::dom_screenwakelock_enabled()) { + return RequestError::PrefDisabled; + } + + // Step 4 check doc active + if (!aDoc->IsActive()) { + return RequestError::DocInactive; + } + + // Step 5. check doc visible + if (aDoc->Hidden()) { + return RequestError::DocHidden; + } + + return RequestError::Success; +} + +// https://w3c.github.io/screen-wake-lock/#dfn-applicable-wake-lock +static bool IsWakeLockApplicable(WakeLockType aType) { + hal::BatteryInformation batteryInfo; + hal::GetCurrentBatteryInformation(&batteryInfo); + if (batteryInfo.level() <= MIN_BATTERY_LEVEL && !batteryInfo.charging()) { + return false; + } + + // only currently supported wake lock type + return aType == WakeLockType::Screen; +} + +// https://w3c.github.io/screen-wake-lock/#dfn-release-a-wake-lock +void ReleaseWakeLock(Document* aDoc, WakeLockSentinel* aLock, + WakeLockType aType) { + MOZ_ASSERT(aLock); + MOZ_ASSERT(aDoc); + + RefPtr<WakeLockSentinel> kungFuDeathGrip = aLock; + aDoc->ActiveWakeLocks(aType).Remove(aLock); + aLock->NotifyLockReleased(); + MOZ_LOG(sLogger, LogLevel::Debug, ("Released wake lock sentinel")); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WakeLockJS) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WakeLockJS) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WakeLockJS) + tmp->DetachListeners(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WakeLockJS) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WakeLockJS) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WakeLockJS) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +WakeLockJS::WakeLockJS(nsPIDOMWindowInner* aWindow) : mWindow(aWindow) { + AttachListeners(); +} + +WakeLockJS::~WakeLockJS() { DetachListeners(); } + +nsISupports* WakeLockJS::GetParentObject() const { return mWindow; } + +JSObject* WakeLockJS::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return WakeLock_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://w3c.github.io/screen-wake-lock/#the-request-method Step 7.3 +Result<already_AddRefed<WakeLockSentinel>, WakeLockJS::RequestError> +WakeLockJS::Obtain(WakeLockType aType) { + // Step 7.3.1. check visibility again + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + if (!doc) { + return Err(RequestError::InternalFailure); + } + if (doc->Hidden()) { + return Err(RequestError::DocHidden); + } + // Step 7.3.3. let lock be a new WakeLockSentinel + RefPtr<WakeLockSentinel> lock = + MakeRefPtr<WakeLockSentinel>(mWindow->AsGlobal(), aType); + // Step 7.3.2. acquire a wake lock + if (IsWakeLockApplicable(aType)) { + lock->AcquireActualLock(); + } + + // Steps 7.3.4. append lock to locks + doc->ActiveWakeLocks(aType).Insert(lock); + + return lock.forget(); +} + +// https://w3c.github.io/screen-wake-lock/#the-request-method +already_AddRefed<Promise> WakeLockJS::Request(WakeLockType aType, + ErrorResult& aRv) { + MOZ_LOG(sLogger, LogLevel::Debug, ("Received request for wake lock")); + nsCOMPtr<nsIGlobalObject> global = mWindow->AsGlobal(); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + NS_ENSURE_FALSE(aRv.Failed(), nullptr); + + // Steps 1-5 + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + RequestError rv = WakeLockAllowedForDocument(doc); + if (rv != RequestError::Success) { + promise->MaybeRejectWithNotAllowedError(GetRequestErrorMessage(rv)); + return promise.forget(); + } + + // For now, we don't check the permission as we always grant the lock + // Step 7.3. Queue a task + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ObtainWakeLock", [aType, promise, self = RefPtr<WakeLockJS>(this)]() { + auto lockOrErr = self->Obtain(aType); + if (lockOrErr.isOk()) { + RefPtr<WakeLockSentinel> lock = lockOrErr.unwrap(); + promise->MaybeResolve(lock); + MOZ_LOG(sLogger, LogLevel::Debug, + ("Resolved promise with wake lock sentinel")); + } else { + promise->MaybeRejectWithNotAllowedError( + GetRequestErrorMessage(lockOrErr.unwrapErr())); + } + })); + + return promise.forget(); +} + +void WakeLockJS::AttachListeners() { + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + DebugOnly<nsresult> rv = + doc->AddSystemEventListener(u"visibilitychange"_ns, this, true, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + doc->RegisterActivityObserver(ToSupports(this)); + + hal::RegisterBatteryObserver(this); + + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + MOZ_ASSERT(prefBranch); + rv = prefBranch->AddObserver("dom.screenwakelock.enabled", this, true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void WakeLockJS::DetachListeners() { + if (mWindow) { + if (nsCOMPtr<Document> doc = mWindow->GetExtantDoc()) { + doc->RemoveSystemEventListener(u"visibilitychange"_ns, this, true); + + doc->UnregisterActivityObserver(ToSupports(this)); + } + } + + hal::UnregisterBatteryObserver(this); + + if (nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID)) { + prefBranch->RemoveObserver("dom.screenwakelock.enabled", this); + } +} + +NS_IMETHODIMP WakeLockJS::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (nsCRT::strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) { + if (!StaticPrefs::dom_screenwakelock_enabled()) { + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + doc->UnlockAllWakeLocks(WakeLockType::Screen); + } + } + return NS_OK; +} + +void WakeLockJS::NotifyOwnerDocumentActivityChanged() { + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + if (!doc->IsActive()) { + doc->UnlockAllWakeLocks(WakeLockType::Screen); + } +} + +NS_IMETHODIMP WakeLockJS::HandleEvent(Event* aEvent) { + nsAutoString type; + aEvent->GetType(type); + + if (type.EqualsLiteral("visibilitychange")) { + nsCOMPtr<Document> doc = do_QueryInterface(aEvent->GetTarget()); + NS_ENSURE_STATE(doc); + + if (doc->Hidden()) { + doc->UnlockAllWakeLocks(WakeLockType::Screen); + } + } + + return NS_OK; +} + +void WakeLockJS::Notify(const hal::BatteryInformation& aBatteryInfo) { + if (aBatteryInfo.level() > MIN_BATTERY_LEVEL || aBatteryInfo.charging()) { + return; + } + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + doc->UnlockAllWakeLocks(WakeLockType::Screen); +} + +} // namespace mozilla::dom diff --git a/dom/power/WakeLockJS.h b/dom/power/WakeLockJS.h new file mode 100644 index 0000000000..c6858fdd22 --- /dev/null +++ b/dom/power/WakeLockJS.h @@ -0,0 +1,103 @@ +/* -*- 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 DOM_WAKELOCKJS_H_ +#define DOM_WAKELOCKJS_H_ + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/HalBatteryInformation.h" +#include "mozilla/dom/WakeLockBinding.h" +#include "nsIDOMEventListener.h" +#include "nsIDocumentActivity.h" +#include "nsIObserver.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWeakReference.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla::dom { + +class Promise; +class Document; +class WakeLockSentinel; + +} // namespace mozilla::dom + +namespace mozilla::dom { + +/** + * Management class for wake locks held from client scripts. + * Instances of this class have two purposes: + * - Implement navigator.wakeLock.request which creates a WakeLockSentinel + * - Listen for state changes that require all WakeLockSentinel to be released + * The WakeLockSentinel objects are held in document.mActiveLocks. + * + * https://www.w3.org/TR/screen-wake-lock/#the-wakelock-interface + */ +class WakeLockJS final : public nsIDOMEventListener, + public nsWrapperCache, + public hal::BatteryObserver, + public nsIDocumentActivity, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_NSIDOCUMENTACTIVITY + NS_DECL_NSIOBSERVER + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(WakeLockJS, + nsIDOMEventListener) + + public: + explicit WakeLockJS(nsPIDOMWindowInner* aWindow); + + protected: + ~WakeLockJS(); + + public: + nsISupports* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void Notify(const hal::BatteryInformation& aBatteryInfo) override; + + already_AddRefed<Promise> Request(WakeLockType aType, ErrorResult& aRv); + + private: + enum class RequestError { + Success, + DocInactive, + DocHidden, + PolicyDisallowed, + PrefDisabled, + InternalFailure, + PermissionDenied + }; + + static nsLiteralCString GetRequestErrorMessage(RequestError aRv); + + static RequestError WakeLockAllowedForDocument(Document* aDoc); + + void AttachListeners(); + void DetachListeners(); + + Result<already_AddRefed<WakeLockSentinel>, RequestError> Obtain( + WakeLockType aType); + + RefPtr<nsPIDOMWindowInner> mWindow; +}; + +MOZ_CAN_RUN_SCRIPT +void ReleaseWakeLock(Document* aDoc, WakeLockSentinel* aLock, + WakeLockType aType); + +} // namespace mozilla::dom + +#endif // DOM_WAKELOCKJS_H_ diff --git a/dom/power/WakeLockSentinel.cpp b/dom/power/WakeLockSentinel.cpp new file mode 100644 index 0000000000..1a0739dbd0 --- /dev/null +++ b/dom/power/WakeLockSentinel.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WakeLockSentinelBinding.h" +#include "mozilla/Hal.h" +#include "WakeLockJS.h" +#include "WakeLockSentinel.h" + +namespace mozilla::dom { + +JSObject* WakeLockSentinel::WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return WakeLockSentinel_Binding::Wrap(cx, this, aGivenProto); +} + +bool WakeLockSentinel::Released() const { return mReleased; } + +void WakeLockSentinel::NotifyLockReleased() { + MOZ_ASSERT(!mReleased); + mReleased = true; + + Telemetry::AccumulateTimeDelta(Telemetry::SCREENWAKELOCK_HELD_DURATION_MS, + mCreationTime); + + hal::BatteryInformation batteryInfo; + hal::GetCurrentBatteryInformation(&batteryInfo); + if (!batteryInfo.charging()) { + uint32_t level = static_cast<uint32_t>(100 * batteryInfo.level()); + Telemetry::Accumulate( + Telemetry::SCREENWAKELOCK_RELEASE_BATTERY_LEVEL_DISCHARGING, level); + } + + if (mHoldsActualLock) { + MOZ_ASSERT(mType == WakeLockType::Screen); + NS_DispatchToMainThread(NS_NewRunnableFunction("ReleaseWakeLock", []() { + hal::ModifyWakeLock(u"screen"_ns, hal::WAKE_LOCK_REMOVE_ONE, + hal::WAKE_LOCK_NO_CHANGE); + })); + mHoldsActualLock = false; + } + + EventInit init; + init.mBubbles = false; + init.mCancelable = false; + RefPtr<Event> event = Event::Constructor(this, u"release"_ns, init); + DispatchTrustedEvent(event); +} + +void WakeLockSentinel::AcquireActualLock() { + MOZ_ASSERT(mType == WakeLockType::Screen); + MOZ_ASSERT(!mHoldsActualLock); + mHoldsActualLock = true; + NS_DispatchToMainThread(NS_NewRunnableFunction("AcquireWakeLock", []() { + hal::ModifyWakeLock(u"screen"_ns, hal::WAKE_LOCK_ADD_ONE, + hal::WAKE_LOCK_NO_CHANGE); + })); +} + +// https://w3c.github.io/screen-wake-lock/#the-release-method +already_AddRefed<Promise> WakeLockSentinel::ReleaseLock(ErrorResult& aRv) { + // ReleaseWakeLock will remove this from document.[[ActiveLocks]] + RefPtr<WakeLockSentinel> kungFuDeathGrip(this); + + if (!mReleased) { + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + if (!global) { + aRv.Throw(NS_ERROR_NULL_POINTER); + return nullptr; + } + nsCOMPtr<nsPIDOMWindowInner> window = global->GetAsInnerWindow(); + if (!window) { + aRv.Throw(NS_ERROR_NULL_POINTER); + return nullptr; + } + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_NULL_POINTER); + return nullptr; + } + ReleaseWakeLock(doc, this, mType); + } + + if (RefPtr<Promise> p = + Promise::CreateResolvedWithUndefined(GetOwnerGlobal(), aRv)) { + return p.forget(); + } + return nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/power/WakeLockSentinel.h b/dom/power/WakeLockSentinel.h new file mode 100644 index 0000000000..f9e79e2c65 --- /dev/null +++ b/dom/power/WakeLockSentinel.h @@ -0,0 +1,79 @@ +/* -*- 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 DOM_WAKELOCKSENTINEL_H_ +#define DOM_WAKELOCKSENTINEL_H_ + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/WakeLockBinding.h" + +namespace mozilla::dom { + +class Promise; + +} // namespace mozilla::dom + +namespace mozilla::dom { + +class WakeLockSentinel final : public DOMEventTargetHelper { + public: + WakeLockSentinel(nsIGlobalObject* aOwnerWindow, WakeLockType aType) + : DOMEventTargetHelper(aOwnerWindow), + mType(aType), + mCreationTime(TimeStamp::Now()) {} + + protected: + ~WakeLockSentinel() { + MOZ_DIAGNOSTIC_ASSERT(mReleased); + MOZ_DIAGNOSTIC_ASSERT(!mHoldsActualLock); + } + + public: + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool Released() const; + + WakeLockType Type() const { return mType; } + + MOZ_CAN_RUN_SCRIPT + already_AddRefed<Promise> ReleaseLock(ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + void NotifyLockReleased(); + + IMPL_EVENT_HANDLER(release); + + // Acquire underlying system wake lock by modifying the HAL wake lock counter + void AcquireActualLock(); + + private: + WakeLockType mType; + + bool mReleased = false; + + /** + * To avoid user fingerprinting, WakeLockJS::Request will provide a + * WakeLockSentinel even if the lock type is not applicable or cannot be + * obtained. + * But when releasing this sentinel, we have to know whether + * AcquireActualLock was called. + * + * https://w3c.github.io/screen-wake-lock/#dfn-applicable-wake-lock + * https://w3c.github.io/screen-wake-lock/#the-request-method + */ + bool mHoldsActualLock = false; + + // Time when this object was created + TimeStamp mCreationTime; +}; + +} // namespace mozilla::dom + +#endif // DOM_WAKELOCKSENTINEL_H_ diff --git a/dom/power/components.conf b/dom/power/components.conf new file mode 100644 index 0000000000..0d09757f8d --- /dev/null +++ b/dom/power/components.conf @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{18c2e238-3a0a-4153-89fc-166b3b1465a1}', + 'contract_ids': ['@mozilla.org/power/powermanagerservice;1'], + 'type': 'mozilla::dom::power::PowerManagerService', + 'constructor': 'mozilla::dom::power::PowerManagerService::GetInstance', + 'headers': ['mozilla/dom/power/PowerManagerService.h'], + 'singleton': True, + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_AND_SOCKET_PROCESS, + }, +] diff --git a/dom/power/moz.build b/dom/power/moz.build new file mode 100644 index 0000000000..5f04a3247d --- /dev/null +++ b/dom/power/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIDOMWakeLockListener.idl", + "nsIPowerManagerService.idl", + "nsIWakeLock.idl", +] + +XPIDL_MODULE = "dom_power" + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS.mozilla.dom += ["WakeLock.h", "WakeLockJS.h", "WakeLockSentinel.h"] + +EXPORTS.mozilla.dom.power += [ + "PowerManagerService.h", +] + +UNIFIED_SOURCES += [ + "PowerManagerService.cpp", + "WakeLock.cpp", + "WakeLockJS.cpp", + "WakeLockSentinel.cpp", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/power/nsIDOMWakeLockListener.idl b/dom/power/nsIDOMWakeLockListener.idl new file mode 100644 index 0000000000..366e9cf594 --- /dev/null +++ b/dom/power/nsIDOMWakeLockListener.idl @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 40; 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 "nsISupports.idl" + +[scriptable, function, uuid(4e258af8-cffb-47bc-b16d-e8241243426e)] +interface nsIDOMMozWakeLockListener : nsISupports +{ + /** + * The callback will be called when a lock topic changes its lock + * state. + * + * Possible states are: + * + * - "unlocked" - nobody holds the wake lock. + * + * - "locked-foreground" - at least one window holds the wake lock, + * and it is visible. + * + * - "locked-background" - at least one window holds the wake lock, + * but all of them are hidden. + * + * @param aTopic The resource name related to the wake lock. + * @param aState The wake lock state + */ + void callback(in AString aTopic, in AString aState); +}; diff --git a/dom/power/nsIPowerManagerService.idl b/dom/power/nsIPowerManagerService.idl new file mode 100644 index 0000000000..3fef3de9a3 --- /dev/null +++ b/dom/power/nsIPowerManagerService.idl @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 POWERMANAGERSERVICE_CONTRACTID "@mozilla.org/power/powermanagerservice;1" +%} + +interface nsIDOMMozWakeLockListener; +interface mozIDOMWindow; +interface nsIWakeLock; + +/** + * For use with non-content code. + */ +[scriptable, builtinclass, uuid(ba7ca4c1-9d92-4425-a83b-85dd7fa953f7)] +interface nsIPowerManagerService : nsISupports +{ + void addWakeLockListener(in nsIDOMMozWakeLockListener aListener); + void removeWakeLockListener(in nsIDOMMozWakeLockListener aListener); + AString getWakeLockState(in AString aTopic); + + /** + * Return a wake lock (MozWakeLock) object of aTopic associated with aWindow. + * A wake lock without associated window, e.g. used in chrome, is + * always considered invisible. + */ + nsIWakeLock newWakeLock(in AString aTopic, [optional] in mozIDOMWindow aWindow); +}; diff --git a/dom/power/nsIWakeLock.idl b/dom/power/nsIWakeLock.idl new file mode 100644 index 0000000000..5ebc158109 --- /dev/null +++ b/dom/power/nsIWakeLock.idl @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 40; 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 "nsISupports.idl" + +[scriptable, builtinclass, uuid(e27e57ce-fa63-4035-b9ef-27c5dc0cc3ae)] +interface nsIWakeLock : nsISupports +{ + void unlock(); +}; diff --git a/dom/power/tests/mochitest.toml b/dom/power/tests/mochitest.toml new file mode 100644 index 0000000000..0b57119a8a --- /dev/null +++ b/dom/power/tests/mochitest.toml @@ -0,0 +1,9 @@ +[DEFAULT] +prefs = ["dom.screenwakelock.enabled=true"] +scheme = "https" + +["test_dynamic_pref_change.html"] +fail-if = ["xorigin"] # cross-origin use requires permissions policy + +["test_wakelock_default_permission.html"] +fail-if = ["xorigin"] # cross-origin use requires permissions policy diff --git a/dom/power/tests/test_dynamic_pref_change.html b/dom/power/tests/test_dynamic_pref_change.html new file mode 100644 index 0000000000..c6aba76246 --- /dev/null +++ b/dom/power/tests/test_dynamic_pref_change.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test Screen Wake Lock responds to dynamic pref changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + /* globals SpecialPowers */ + 'use strict'; + + function spinEventLoop() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); + } + + add_task(async function my_test() { + const lock = await navigator.wakeLock.request("screen"); + await SpecialPowers.pushPrefEnv({ + set: [["dom.screenwakelock.enabled", false]], + }); + await spinEventLoop(); + ok(lock.released, "Lock was released once pref was deactivated"); + await SpecialPowers.popPrefEnv(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/power/tests/test_wakelock_default_permission.html b/dom/power/tests/test_wakelock_default_permission.html new file mode 100644 index 0000000000..78065f6eeb --- /dev/null +++ b/dom/power/tests/test_wakelock_default_permission.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test Screen Wake Lock Permission Granted by Default</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + add_task(async function my_test() { + const status = await navigator.permissions.query({name:'screen-wake-lock'}); + is(status.state, "granted", "Permission is granted by default"); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> |