Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,25 @@ internal object WebWakeLockManager {

private val requests = mutableSetOf<Any>()

@VisibleForTesting
internal var requestWakeLock: () -> Promise<WakeLockSentinel> = ::requestScreenWakeLock

init {
document.addEventListener("visibilitychange") {
if (documentIsVisible() && enoughRequestsForLock() && webWakeLockSupported) {
requestWakeLock()
retryWakeLockRequest()
}

// Safari requires Screen Wake Lock requests to run during transient user activation.
listOf(
"click",
"keydown",
"mousedown",
"pointerdown",
"pointerup",
"touchend"
).forEach { eventName ->
document.addEventListener(eventName) {
retryWakeLockRequest()
}
}
}
Expand All @@ -68,21 +83,26 @@ internal object WebWakeLockManager {
requests.remove(client)
}
if (enoughRequestsForLock()) {
requestWakeLock()
acquireWakeLock()
} else {
releaseWakeLock()
}
}

private fun retryWakeLockRequest() {
if (documentIsVisible() && enoughRequestsForLock() && webWakeLockSupported) {
acquireWakeLock()
}
}

private fun requestWakeLock() {
private fun acquireWakeLock() {
if (wakeLockSentinel != null || requestingLock) {
//A lock is already active or a request is in progress
return
}

requestingLock = true
requestScreenWakeLock()
requestWakeLock()
.then { sentinel ->
//Prevents race condition where a release requestLock could come in before the lock is granted
if (requestingLock) {
Expand Down Expand Up @@ -134,6 +154,7 @@ internal object WebWakeLockManager {
requestingLock = false
requests.clear()
releaseWakeLock()
requestWakeLock = ::requestScreenWakeLock
}

}
Expand All @@ -156,7 +177,7 @@ internal fun isFullWakeLockApiSupported(): Boolean =
"""
)

private external interface WakeLockSentinel : JsAny {
internal external interface WakeLockSentinel : JsAny {
@Suppress("unused")
val released: Boolean

Expand All @@ -165,6 +186,3 @@ private external interface WakeLockSentinel : JsAny {
fun release(): Promise<JsAny?>
fun addEventListener(type: String, listener: () -> Unit)
}



Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.OnCanvasTests
import androidx.compose.ui.keepScreenOn
import kotlin.js.JsAny
import kotlin.js.Promise
import kotlin.js.js
import kotlin.js.unsafeCast
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.suspendCancellableCoroutine
import org.w3c.dom.pointerevents.PointerEvent
import org.w3c.dom.pointerevents.PointerEventInit

class WebWakeLockManagerTests : OnCanvasTests {

Expand Down Expand Up @@ -75,6 +76,52 @@ class WebWakeLockManagerTests : OnCanvasTests {
manager.reset()
}

@Test
fun testWakeLockRequestRetriedOnUserActivationAfterInitialFailure() = runApplicationTest {
val manager = WebWakeLockManager
manager.reset()

var allowRequest = false
val sentinel = fakeWakeLockSentinel()
manager.requestWakeLock = {
if (allowRequest) {
resolvedWakeLockPromise(sentinel)
} else {
rejectedWakeLockPromise()
}
}

try {
createComposeWindow {
Box(Modifier.keepScreenOn())
}

awaitIdle()

assertFalse(
manager.isWakeLockActive(),
"Wake lock should not be active after the initial request fails"
)
assertFalse(manager.requestingLock, "Failed request should clear requesting state")

allowRequest = true
document.dispatchEvent(
PointerEvent(
"pointerdown",
PointerEventInit(button = 0, buttons = 1, pointerType = "mouse")
)
)
awaitIdle()

assertTrue(
manager.isWakeLockActive(),
"Wake lock should be retried on a user activation event"
)
} finally {
manager.reset()
}
}

@Test
fun testWakeLockRemainsWhenOneOfTwoRemoved() = runApplicationTest {
var attachFirst by mutableStateOf(true)
Expand Down Expand Up @@ -264,4 +311,31 @@ class WebWakeLockManagerTests : OnCanvasTests {
assertFalse(manager.requestingLock, "Should not be requesting lock after release")
manager.reset()
}
}
}

private fun fakeWakeLockSentinel(): WakeLockSentinel = js(
"""({
released: false,
type: "screen",
release: function() {
this.released = true;
if (this.releaseListener) {
this.releaseListener();
}
return Promise.resolve(null);
},
addEventListener: function(type, listener) {
if (type === "release") {
this.releaseListener = listener;
}
}
})"""
).unsafeCast<WakeLockSentinel>()

private fun resolvedWakeLockPromise(sentinel: WakeLockSentinel): Promise<WakeLockSentinel> = js(
"Promise.resolve(sentinel)"
)

private fun rejectedWakeLockPromise(): Promise<WakeLockSentinel> = js(
"Promise.reject(new Error('Transient activation is required'))"
)
Loading