Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ jobs:
cd wakelock_plus/example
flutter pub get
flutter build apk --debug --target=./lib/main.dart
- name: Android plugin unit tests (Robolectric)
# The APK build above generates the (gitignored) Gradle wrapper that this
# step reuses to run the plugin's JVM/Robolectric unit tests.
run: |
cd wakelock_plus/example/android
./gradlew :wakelock_plus:testDebugUnitTest

android_integration_test:
needs: setup_matrix
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
.buildlog/
.history
.svn/
.metadata
.metadata
CLAUDE.md
3 changes: 3 additions & 0 deletions wakelock_plus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [1.6.2]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not eagerly increment the changelog.

I'll handle deploying the app when the PR is merged.

* Android: Fixed `NoActivityException` ("wakelock requires a foreground activity") being thrown when `toggle`/`enabled` were called with no foreground activity attached (e.g. while the app is backgrounded or during a lifecycle transition). The requested wakelock state is now remembered and re-applied once an activity (re)attaches instead of throwing.

## [1.6.1]
* [#133](https://github.com/fluttercommunity/wakelock_plus/pull/133): wakelock_plus Flutter 3.38 downgrade. Thanks [diegotori](https://github.com/diegotori).
- Library now requires Dart version `3.10` or higher, restoring previous compatibility.
Expand Down
7 changes: 7 additions & 0 deletions wakelock_plus/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ android {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
// Robolectric runs the JVM unit tests against a simulated Android
// framework so the wakelock window flags can be asserted without a
// device. It is JUnit4-based, so the vintage engine bridges it onto the
// JUnit Platform configured below (useJUnitPlatform()).
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.14.1'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.11.4'
}

testOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,35 @@ import android.app.Activity
import android.view.WindowManager

internal class Wakelock {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a way to create native tests for this change, that would be ideal.

Given that this requires destroying an activity in order to simulate going to the background, you'll probably need something like robolectric for this.

var activity: Activity? = null

private val enabled
get() = activity!!.window.attributes.flags and
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON != 0
// The desired wakelock state. Tracked independently of [activity] so that a
// toggle requested while no activity is attached (e.g. the app is in the
// background or mid lifecycle transition) is remembered and re-applied once an
// activity (re)attaches, instead of throwing a NoActivityException.
private var enableWakelock = false

fun toggle(message: ToggleMessage) {
if (activity == null) {
throw NoActivityException()
var activity: Activity? = null
set(value) {
field = value
// Re-assert the wakelock on the newly attached activity's window, but only
// when it was actually requested. If the user never enabled it (or last
// disabled it), leave the flag alone — the activity may keep the screen on
// for its own reasons.
if (enableWakelock) applyWakelock()
}

val activity = this.activity!!
val enabled = this.enabled

if (message.enable!!) {
if (!enabled) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else if (enabled) {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
private fun applyWakelock() {
val window = activity?.window ?: return
if (enableWakelock) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}

fun isEnabled(): IsEnabledMessage {
if (activity == null) {
throw NoActivityException()
}

return IsEnabledMessage(enabled = enabled)
fun toggle(message: ToggleMessage) {
enableWakelock = message.enable!!
applyWakelock()
}
}

class NoActivityException : Exception("wakelock requires a foreground activity")
fun isEnabled(): IsEnabledMessage = IsEnabledMessage(enabled = enableWakelock)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package dev.fluttercommunity.plus.wakelock

import ToggleMessage
import android.app.Activity
import android.os.Build
import android.view.WindowManager
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* Unit tests for [Wakelock] covering the "no foreground activity attached"
* behaviour introduced to stop [toggle]/[isEnabled] from throwing when the app
* is backgrounded or mid lifecycle transition. The desired state is tracked in
* Kotlin and (re)applied to whichever activity window is attached.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
class WakelockTest {

private fun buildActivity(): Activity =
Robolectric.buildActivity(Activity::class.java).setup().get()

private val Activity.keepScreenOn: Boolean
get() = window.attributes.flags and
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON != 0

@Test
fun `toggle enable with no activity attached does not throw`() {
val wakelock = Wakelock()

wakelock.toggle(ToggleMessage(enable = true))

assertTrue(wakelock.isEnabled().enabled == true)
}

@Test
fun `isEnabled with no activity attached does not throw and defaults to false`() {
assertFalse(Wakelock().isEnabled().enabled == true)
}

@Test
fun `enabling before an activity attaches applies the flag once it does`() {
val wakelock = Wakelock()

wakelock.toggle(ToggleMessage(enable = true))
val activity = buildActivity()
wakelock.activity = activity

assertTrue(activity.keepScreenOn)
}

@Test
fun `attaching an activity while disabled leaves the flag untouched`() {
val wakelock = Wakelock()
val activity = buildActivity()

wakelock.activity = activity

assertFalse(activity.keepScreenOn)
}

@Test
fun `disabling clears the flag on the attached activity`() {
val wakelock = Wakelock()
val activity = buildActivity()
wakelock.activity = activity

wakelock.toggle(ToggleMessage(enable = true))
assertTrue(activity.keepScreenOn)

wakelock.toggle(ToggleMessage(enable = false))
assertFalse(activity.keepScreenOn)
}

@Test
fun `enabled state is re-applied to a new activity after detach`() {
val wakelock = Wakelock()
val first = buildActivity()
wakelock.activity = first
wakelock.toggle(ToggleMessage(enable = true))
assertTrue(first.keepScreenOn)

// The activity goes away (e.g. the app is backgrounded). This used to throw.
wakelock.activity = null
assertTrue(wakelock.isEnabled().enabled == true)

// A fresh activity attaches; the requested state must be re-asserted on it.
val second = buildActivity()
wakelock.activity = second
assertTrue(second.keepScreenOn)
}
}
2 changes: 1 addition & 1 deletion wakelock_plus/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: wakelock_plus
description: >-2
Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on
Android, iOS, macOS, Windows, Linux, and web.
version: 1.6.1
version: 1.6.2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not eagerly increment the pubspec.

I'll handle deploying the app when the PR is merged.

repository: https://github.com/fluttercommunity/wakelock_plus/tree/main/wakelock_plus

environment:
Expand Down