diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3f89df1..ffe4740 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -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 diff --git a/.gitignore b/.gitignore index 88efa4b..eeb418a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ .buildlog/ .history .svn/ -.metadata \ No newline at end of file +.metadata +CLAUDE.md diff --git a/wakelock_plus/CHANGELOG.md b/wakelock_plus/CHANGELOG.md index da5027c..5713e1d 100644 --- a/wakelock_plus/CHANGELOG.md +++ b/wakelock_plus/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.6.2] +* 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. diff --git a/wakelock_plus/android/build.gradle b/wakelock_plus/android/build.gradle index d2a1ffe..e060d37 100644 --- a/wakelock_plus/android/build.gradle +++ b/wakelock_plus/android/build.gradle @@ -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 { diff --git a/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt b/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt index 8ba445a..36e5027 100644 --- a/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt +++ b/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt @@ -6,34 +6,35 @@ import android.app.Activity import android.view.WindowManager internal class Wakelock { - 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) +} diff --git a/wakelock_plus/android/src/test/kotlin/dev/fluttercommunity/plus/wakelock/WakelockTest.kt b/wakelock_plus/android/src/test/kotlin/dev/fluttercommunity/plus/wakelock/WakelockTest.kt new file mode 100644 index 0000000..07f2a9a --- /dev/null +++ b/wakelock_plus/android/src/test/kotlin/dev/fluttercommunity/plus/wakelock/WakelockTest.kt @@ -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) + } +} diff --git a/wakelock_plus/pubspec.yaml b/wakelock_plus/pubspec.yaml index a555046..7a4ea90 100644 --- a/wakelock_plus/pubspec.yaml +++ b/wakelock_plus/pubspec.yaml @@ -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 repository: https://github.com/fluttercommunity/wakelock_plus/tree/main/wakelock_plus environment: