-
Notifications
You must be signed in to change notification settings - Fork 203
feat: Screen Wake Lock API on Page #24324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Artur-
wants to merge
2
commits into
main
Choose a base branch
from
feature/wake-lock
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /* | ||
| * Copyright 2000-2026 Vaadin Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. You may obtain a copy of | ||
| * the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
|
|
||
| type VaadinWakeLockState = 'ACTIVE' | 'RELEASED'; | ||
|
|
||
| // Whether the server-side has asked us to hold the lock. The browser releases | ||
| // the lock whenever the tab is hidden; this flag is what lets the | ||
| // visibilitychange handler re-acquire silently when the tab returns. | ||
| let wanted = false; | ||
| let sentinel: WakeLockSentinel | null = null; | ||
| let visibilityListenerInstalled = false; | ||
|
|
||
| function dispatch(element: HTMLElement, state: VaadinWakeLockState): void { | ||
| element.dispatchEvent(new CustomEvent('vaadin-wake-lock-change', { detail: state })); | ||
| } | ||
|
|
||
| async function acquire(element: HTMLElement): Promise<void> { | ||
| if (sentinel) { | ||
| return; | ||
| } | ||
| if (!('wakeLock' in navigator)) { | ||
| return; | ||
| } | ||
| try { | ||
| const next = await (navigator as any).wakeLock.request('screen'); | ||
| // The user (or the browser) may have released the lock or the tab may have | ||
| // been hidden again while the request was in flight. | ||
| if (!wanted || document.visibilityState !== 'visible') { | ||
| try { | ||
| await next.release(); | ||
| } catch (_e) { | ||
| // Ignore; releasing an already-released sentinel throws on some | ||
| // browsers and there is nothing meaningful to do here. | ||
| } | ||
| return; | ||
| } | ||
| sentinel = next; | ||
| next.addEventListener('release', () => { | ||
| sentinel = null; | ||
| dispatch(element, 'RELEASED'); | ||
| }); | ||
| dispatch(element, 'ACTIVE'); | ||
| } catch (_e) { | ||
| // The browser refused (insecure context, feature unavailable, low battery, | ||
| // user revoked, …). The signal stays in 'RELEASED'; no need to surface | ||
| // the error — applications observe activeSignal() to know the truth. | ||
| } | ||
| } | ||
|
|
||
| function installVisibilityListener(element: HTMLElement): void { | ||
| if (visibilityListenerInstalled) { | ||
| return; | ||
| } | ||
| visibilityListenerInstalled = true; | ||
| document.addEventListener('visibilitychange', () => { | ||
| if (wanted && !sentinel && document.visibilityState === 'visible') { | ||
| acquire(element); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const $wnd = window as any; | ||
| $wnd.Vaadin ??= {}; | ||
| $wnd.Vaadin.Flow ??= {}; | ||
| $wnd.Vaadin.Flow.wakeLock = { | ||
| request(element: HTMLElement): Promise<void> { | ||
| wanted = true; | ||
| installVisibilityListener(element); | ||
| if (document.visibilityState !== 'visible') { | ||
| // The browser will not grant a lock while the page is hidden; the | ||
| // visibilitychange listener will pick it up on the next 'visible'. | ||
| return Promise.resolve(); | ||
| } | ||
| return acquire(element); | ||
| }, | ||
|
|
||
| async release(element: HTMLElement): Promise<void> { | ||
| wanted = false; | ||
| if (!sentinel) { | ||
| return; | ||
| } | ||
| const current = sentinel; | ||
| sentinel = null; | ||
| try { | ||
| await current.release(); | ||
| } catch (_e) { | ||
| // Ignore; the 'release' event listener installed in acquire() also | ||
| // dispatches RELEASED, so the state still reaches the server even when | ||
| // the explicit release() call rejects. | ||
| } | ||
| dispatch(element, 'RELEASED'); | ||
| } | ||
| }; | ||
|
|
||
| // Empty export to ensure TypeScript emits this as an ES module, which is | ||
| // required for Vite to load it via import. | ||
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
flow-server/src/main/java/com/vaadin/flow/component/page/WakeLock.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| /* | ||
| * Copyright 2000-2026 Vaadin Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. You may obtain a copy of | ||
| * the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
| package com.vaadin.flow.component.page; | ||
|
|
||
| import java.io.Serializable; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import com.vaadin.flow.component.UI; | ||
| import com.vaadin.flow.signals.Signal; | ||
| import com.vaadin.flow.signals.local.ValueSignal; | ||
|
|
||
| /** | ||
| * Browser Screen Wake Lock API for Flow applications. Keeps the screen from | ||
| * dimming or locking while the lock is held — useful for dashboards, kiosks, | ||
| * presentations, and recipe / workout screens. | ||
| * <p> | ||
| * Reach the per-UI instance through {@link Page#getWakeLock()}. | ||
| * | ||
| * <p> | ||
| * <b>Example:</b> | ||
| * | ||
| * <pre> | ||
| * WakeLock wakeLock = UI.getCurrent().getPage().getWakeLock(); | ||
| * wakeLock.request(); | ||
| * | ||
| * Signal.effect(this, () -> { | ||
| * statusLabel.setText( | ||
| * wakeLock.activeSignal().get() ? "Screen will stay on" : "Idle"); | ||
| * }); | ||
| * </pre> | ||
| * | ||
| * <p> | ||
| * <b>Lifecycle.</b> {@link #request()} fires the browser request asynchronously | ||
| * and {@link #activeSignal()} flips to {@code true} once the browser confirms. | ||
| * The browser releases the lock automatically when the tab is hidden; the | ||
| * client transparently re-acquires it when the tab becomes visible again, so a | ||
| * single {@link #request()} is enough for the lifetime of the view. Call | ||
| * {@link #release()} to stop re-acquiring and drop the current lock. | ||
| * | ||
| * <p> | ||
| * <b>Reliability caveats.</b> | ||
| * <ul> | ||
| * <li>Safari only ships the Screen Wake Lock API from version 16.4.</li> | ||
| * <li>The browser requires a secure context (HTTPS or {@code localhost}); on an | ||
| * insecure origin the call fails silently and {@link #activeSignal()} stays | ||
| * {@code false}.</li> | ||
| * <li>Some browsers release the lock on low battery or when the device enters | ||
| * power-saving mode; {@link #activeSignal()} reflects this immediately.</li> | ||
| * </ul> | ||
| * | ||
| * @see Page#getWakeLock() | ||
| */ | ||
| public final class WakeLock implements Serializable { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory | ||
| .getLogger(WakeLock.class); | ||
|
|
||
| static final String STATE_CHANGE_EVENT = "vaadin-wake-lock-change"; | ||
|
|
||
| private final UI ui; | ||
| private final ValueSignal<Boolean> activeSignal = new ValueSignal<>( | ||
| Boolean.FALSE); | ||
| private final Signal<Boolean> activeSignalReadOnly = activeSignal | ||
| .asReadonly(); | ||
|
|
||
| WakeLock(UI ui) { | ||
| this.ui = ui; | ||
| ui.getElement() | ||
| .addEventListener(STATE_CHANGE_EVENT, | ||
| e -> setActive(e.getEventDetail(String.class))) | ||
| .addEventDetail().allowInert(); | ||
| } | ||
|
|
||
| /** | ||
| * Asks the browser to acquire a screen wake lock and keep re-acquiring it | ||
| * across tab visibility changes. | ||
| * <p> | ||
| * The call is asynchronous and fire-and-forget: by the time this method | ||
| * returns the browser has not necessarily granted the lock yet. Observe | ||
| * {@link #activeSignal()} to react to the actual state. Calling | ||
| * {@code request()} when a lock is already held is a no-op. | ||
| */ | ||
| public void request() { | ||
| ui.getElement().executeJs("window.Vaadin.Flow.wakeLock.request(this)") | ||
| .then(ignored -> { | ||
| }, err -> LOGGER | ||
| .debug("Client-side wakeLock.request failed: {}", err)); | ||
| } | ||
|
|
||
| /** | ||
| * Releases the screen wake lock and stops re-acquiring it on subsequent | ||
| * visibility changes. Idempotent — calling it when no lock is held is a | ||
| * no-op. | ||
| */ | ||
| public void release() { | ||
| ui.getElement().executeJs("window.Vaadin.Flow.wakeLock.release(this)") | ||
| .then(ignored -> { | ||
| }, err -> LOGGER | ||
| .debug("Client-side wakeLock.release failed: {}", err)); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a read-only signal that reflects whether the browser is currently | ||
| * holding the wake lock on behalf of this UI. | ||
| * <p> | ||
| * Starts as {@code false}, flips to {@code true} when the browser confirms | ||
| * a request, and flips back to {@code false} whenever the browser releases | ||
| * the lock — explicitly through {@link #release()}, automatically when the | ||
| * tab becomes hidden, or because the browser dropped it (power saving, low | ||
| * battery). When the tab is shown again the client re-requests the lock if | ||
| * {@link #release()} has not been called, so the signal flips back to | ||
| * {@code true} shortly after. | ||
| * <p> | ||
| * Use {@code Signal.effect(owner, ...)} to react to changes and | ||
| * {@code .peek()} for a snapshot outside a reactive context. | ||
| * | ||
| * @return the read-only active-state signal | ||
| */ | ||
| public Signal<Boolean> activeSignal() { | ||
| return activeSignalReadOnly; | ||
| } | ||
|
|
||
| /** | ||
| * Updates the signal from a raw state value reported by the client. Unknown | ||
| * values are logged at debug level so a forward-compatible client value | ||
| * does not silently disappear. | ||
| * | ||
| * @param value | ||
| * the raw value, or {@code null} | ||
| */ | ||
| void setActive(String value) { | ||
| if (value == null) { | ||
| return; | ||
| } | ||
| switch (value) { | ||
| case "ACTIVE" -> activeSignal.set(Boolean.TRUE); | ||
| case "RELEASED" -> activeSignal.set(Boolean.FALSE); | ||
| default -> | ||
| LOGGER.debug("Unknown wake lock state from client: {}", value); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other browser feature APIs have their own static methods instead of being accessed throug
Page.Should we make this consistent?