Bug 1589554 - Part 2: Implement Screen Wake Lock API. r=dom-core,edgar

Depends on D189508

Differential Revision: https://phabricator.services.mozilla.com/D189509
This commit is contained in:
Vincent Hilla 2023-12-05 23:58:06 +00:00
parent 0c8b54eb5e
commit 248841bc9c
9 changed files with 351 additions and 12 deletions

View file

@ -233,6 +233,7 @@
#include "mozilla/dom/URL.h" #include "mozilla/dom/URL.h"
#include "mozilla/dom/UseCounterMetrics.h" #include "mozilla/dom/UseCounterMetrics.h"
#include "mozilla/dom/UserActivation.h" #include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/WakeLockJS.h"
#include "mozilla/dom/WakeLockSentinel.h" #include "mozilla/dom/WakeLockSentinel.h"
#include "mozilla/dom/WindowBinding.h" #include "mozilla/dom/WindowBinding.h"
#include "mozilla/dom/WindowContext.h" #include "mozilla/dom/WindowContext.h"
@ -18260,6 +18261,44 @@ nsTHashSet<RefPtr<WakeLockSentinel>>& Document::ActiveWakeLocks(
return mActiveLocks.LookupOrInsert(aType); return mActiveLocks.LookupOrInsert(aType);
} }
class UnlockAllWakeLockRunnable final : public Runnable {
public:
UnlockAllWakeLockRunnable(WakeLockType aType, Document* aDoc)
: Runnable("UnlockAllWakeLocks"), mType(aType), mDoc(aDoc) {}
// MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
// bug 1535398.
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Run() override {
// Move, as ReleaseWakeLock will try to remove from and possibly allow
// scripts via onrelease to add to document.[[ActiveLocks]]["screen"]
nsCOMPtr<Document> doc = mDoc;
nsTHashSet<RefPtr<WakeLockSentinel>> locks =
std::move(doc->ActiveWakeLocks(mType));
for (const auto& lock : locks) {
// ReleaseWakeLock runs script, which could release other locks
if (!lock->Released()) {
ReleaseWakeLock(doc, MOZ_KnownLive(lock), mType);
}
}
return NS_OK;
}
protected:
~UnlockAllWakeLockRunnable() = default;
private:
WakeLockType mType;
nsCOMPtr<Document> mDoc;
};
void Document::UnlockAllWakeLocks(WakeLockType aType) {
// Perform unlock in a runnable to prevent UnlockAll being MOZ_CAN_RUN_SCRIPT
RefPtr<UnlockAllWakeLockRunnable> runnable =
MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this);
NS_DispatchToMainThread(runnable);
}
RefPtr<Document::AutomaticStorageAccessPermissionGrantPromise> RefPtr<Document::AutomaticStorageAccessPermissionGrantPromise>
Document::AutomaticStorageAccessPermissionCanBeGranted(bool hasUserActivation) { Document::AutomaticStorageAccessPermissionCanBeGranted(bool hasUserActivation) {
// requestStorageAccessForOrigin may not require user activation. If we don't // requestStorageAccessForOrigin may not require user activation. If we don't

View file

@ -3540,6 +3540,8 @@ class Document : public nsINode,
nsTHashSet<RefPtr<WakeLockSentinel>>& ActiveWakeLocks(WakeLockType aType); nsTHashSet<RefPtr<WakeLockSentinel>>& ActiveWakeLocks(WakeLockType aType);
void UnlockAllWakeLocks(WakeLockType aType);
// ParentNode // ParentNode
nsIHTMLCollection* Children(); nsIHTMLCollection* Children();
uint32_t ChildElementCount(); uint32_t ChildElementCount();

View file

@ -4,11 +4,85 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ErrorList.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Event.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 "nsCOMPtr.h"
#include "nsError.h"
#include "nsIGlobalObject.h"
#include "nsISupports.h"
#include "nsPIDOMWindow.h" #include "nsPIDOMWindow.h"
#include "WakeLockJS.h" #include "WakeLockJS.h"
#include "WakeLockSentinel.h"
namespace mozilla::dom { namespace mozilla::dom {
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;
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 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) {
// 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();
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WakeLockJS, mWindow) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WakeLockJS, mWindow)
NS_IMPL_CYCLE_COLLECTING_ADDREF(WakeLockJS) NS_IMPL_CYCLE_COLLECTING_ADDREF(WakeLockJS)
@ -20,7 +94,11 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WakeLockJS)
NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
NS_INTERFACE_MAP_END NS_INTERFACE_MAP_END
WakeLockJS::WakeLockJS(nsPIDOMWindowInner* aWindow) : mWindow(aWindow) {} WakeLockJS::WakeLockJS(nsPIDOMWindowInner* aWindow) : mWindow(aWindow) {
AttachListeners();
}
WakeLockJS::~WakeLockJS() { DetachListeners(); }
nsISupports* WakeLockJS::GetParentObject() const { return mWindow; } nsISupports* WakeLockJS::GetParentObject() const { return mWindow; }
@ -29,12 +107,102 @@ JSObject* WakeLockJS::WrapObject(JSContext* aCx,
return WakeLock_Binding::Wrap(aCx, this, aGivenProto); return WakeLock_Binding::Wrap(aCx, this, aGivenProto);
} }
already_AddRefed<Promise> WakeLockJS::Request(WakeLockType aType) { // https://w3c.github.io/screen-wake-lock/#the-request-method Step 7.3
return nullptr; 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., 7.3.5. Append lock to locks and resolve promise with lock
doc->ActiveWakeLocks(aType).Insert(lock);
return lock.forget();
} }
void WakeLockJS::NotifyOwnerDocumentActivityChanged() {} // https://w3c.github.io/screen-wake-lock/#the-request-method
already_AddRefed<Promise> WakeLockJS::Request(WakeLockType aType,
ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = mWindow->AsGlobal();
NS_IMETHODIMP WakeLockJS::HandleEvent(Event* aEvent) { return NS_OK; } 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();
}
// Step 7.1. Requesting permission to use screen-wake-lock
// Note: implemented in a future part, for now always try to obtain
auto lockOrErr = Obtain(aType);
if (lockOrErr.isOk()) {
RefPtr<WakeLockSentinel> lock = lockOrErr.unwrap();
promise->MaybeResolve(lock);
} 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));
}
void WakeLockJS::DetachListeners() {
if (mWindow) {
if (nsCOMPtr<Document> doc = mWindow->GetExtantDoc()) {
doc->RemoveSystemEventListener(u"visibilitychange"_ns, this, true);
doc->UnregisterActivityObserver(ToSupports(this));
}
}
}
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;
}
} // namespace mozilla::dom } // namespace mozilla::dom

View file

@ -8,6 +8,7 @@
#define DOM_WAKELOCKJS_H_ #define DOM_WAKELOCKJS_H_
#include "js/TypeDecls.h" #include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/dom/WakeLockBinding.h" #include "mozilla/dom/WakeLockBinding.h"
#include "nsIDOMEventListener.h" #include "nsIDOMEventListener.h"
#include "nsIDocumentActivity.h" #include "nsIDocumentActivity.h"
@ -26,6 +27,15 @@ 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, class WakeLockJS final : public nsIDOMEventListener,
public nsWrapperCache, public nsWrapperCache,
public nsIDocumentActivity { public nsIDocumentActivity {
@ -41,7 +51,7 @@ class WakeLockJS final : public nsIDOMEventListener,
explicit WakeLockJS(nsPIDOMWindowInner* aWindow); explicit WakeLockJS(nsPIDOMWindowInner* aWindow);
protected: protected:
~WakeLockJS() = default; ~WakeLockJS();
public: public:
nsISupports* GetParentObject() const; nsISupports* GetParentObject() const;
@ -49,12 +59,37 @@ class WakeLockJS final : public nsIDOMEventListener,
JSObject* WrapObject(JSContext* aCx, JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override; JS::Handle<JSObject*> aGivenProto) override;
already_AddRefed<Promise> Request(WakeLockType aType); already_AddRefed<Promise> Request(WakeLockType aType, ErrorResult& aRv);
private: private:
enum class RequestError {
Success,
DocInactive,
DocHidden,
PolicyDisallowed,
PrefDisabled,
InternalFailure
};
static nsLiteralCString GetRequestErrorMessage(RequestError aRv);
static RequestError WakeLockAllowedForDocument(Document* aDoc);
void AttachListeners();
void DetachListeners();
Result<already_AddRefed<WakeLockSentinel>, RequestError> Obtain(
WakeLockType aType);
void UnlockAll(WakeLockType aType);
RefPtr<nsPIDOMWindowInner> mWindow; RefPtr<nsPIDOMWindowInner> mWindow;
}; };
MOZ_CAN_RUN_SCRIPT
void ReleaseWakeLock(Document* aDoc, WakeLockSentinel* aLock,
WakeLockType aType);
} // namespace mozilla::dom } // namespace mozilla::dom
#endif // DOM_WAKELOCKJS_H_ #endif // DOM_WAKELOCKJS_H_

View file

@ -4,7 +4,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/Assertions.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/dom/WakeLockSentinelBinding.h"
#include "mozilla/Hal.h"
#include "WakeLockJS.h"
#include "WakeLockSentinel.h" #include "WakeLockSentinel.h"
namespace mozilla::dom { namespace mozilla::dom {
@ -14,8 +21,67 @@ JSObject* WakeLockSentinel::WrapObject(JSContext* cx,
return WakeLockSentinel_Binding::Wrap(cx, this, aGivenProto); return WakeLockSentinel_Binding::Wrap(cx, this, aGivenProto);
} }
bool WakeLockSentinel::Released() const { return false; } bool WakeLockSentinel::Released() const { return mReleased; }
already_AddRefed<Promise> WakeLockSentinel::ReleaseLock() { return nullptr; } void WakeLockSentinel::NotifyLockReleased() {
MOZ_ASSERT(!mReleased);
mReleased = true;
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 } // namespace mozilla::dom

View file

@ -8,6 +8,7 @@
#define DOM_WAKELOCKSENTINEL_H_ #define DOM_WAKELOCKSENTINEL_H_
#include "js/TypeDecls.h" #include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/DOMEventTargetHelper.h" #include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/dom/WakeLockBinding.h" #include "mozilla/dom/WakeLockBinding.h"
@ -25,7 +26,10 @@ class WakeLockSentinel final : public DOMEventTargetHelper {
: DOMEventTargetHelper(aOwnerWindow), mType(aType) {} : DOMEventTargetHelper(aOwnerWindow), mType(aType) {}
protected: protected:
~WakeLockSentinel() = default; ~WakeLockSentinel() {
MOZ_DIAGNOSTIC_ASSERT(mReleased);
MOZ_DIAGNOSTIC_ASSERT(!mHoldsActualLock);
}
public: public:
JSObject* WrapObject(JSContext* aCx, JSObject* WrapObject(JSContext* aCx,
@ -36,12 +40,35 @@ class WakeLockSentinel final : public DOMEventTargetHelper {
WakeLockType Type() const { return mType; } WakeLockType Type() const { return mType; }
MOZ_CAN_RUN_SCRIPT MOZ_CAN_RUN_SCRIPT
already_AddRefed<Promise> ReleaseLock(); already_AddRefed<Promise> ReleaseLock(ErrorResult& aRv);
MOZ_CAN_RUN_SCRIPT
void NotifyLockReleased();
IMPL_EVENT_HANDLER(release); IMPL_EVENT_HANDLER(release);
// Acquire underlying system wake lock by modifying the HAL wake lock counter
void AcquireActualLock();
private: private:
WakeLockType mType; 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 / the wake lock acquired.
TimeStamp mAcquireTime;
}; };
} // namespace mozilla::dom } // namespace mozilla::dom

View file

@ -40,6 +40,7 @@ static FeatureMap sSupportedFeatures[] = {
FeaturePolicyUtils::FeaturePolicyValue::eSelf}, FeaturePolicyUtils::FeaturePolicyValue::eSelf},
{"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, {"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
{"storage-access", FeaturePolicyUtils::FeaturePolicyValue::eAll}, {"storage-access", FeaturePolicyUtils::FeaturePolicyValue::eAll},
{"screen-wake-lock", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
}; };
/* /*

View file

@ -9,6 +9,7 @@
[SecureContext, Exposed=(Window)] [SecureContext, Exposed=(Window)]
interface WakeLock { interface WakeLock {
[Throws]
Promise<WakeLockSentinel> request(optional WakeLockType type = "screen"); Promise<WakeLockSentinel> request(optional WakeLockType type = "screen");
}; };

View file

@ -11,7 +11,7 @@
interface WakeLockSentinel : EventTarget { interface WakeLockSentinel : EventTarget {
readonly attribute boolean released; readonly attribute boolean released;
readonly attribute WakeLockType type; readonly attribute WakeLockType type;
[BinaryName="releaseLock"] [BinaryName="releaseLock", Throws]
Promise<undefined> release(); Promise<undefined> release();
attribute EventHandler onrelease; attribute EventHandler onrelease;
}; };