Add command_screen_off to turn the display off while the device stays awake - #7162
Add command_screen_off to turn the display off while the device stays awake#7162markfrancisonly wants to merge 11 commits into
Conversation
Turn the screen off from the server while the device stays awake, unlocked and connected, so that dashboards keep receiving commands and their WebSocket connection is not interrupted. Cutting the display power is only possible through a device admin, so ScreenOffAdminReceiver is added with the force lock policy alone. It is inert until the user activates it, and the app opens the system activation screen when the command is used before that. A partial wake lock keeps the CPU and the network alive while the display is off, and command_screen_on releases it. The command is ignored on devices with a secure keyguard since turning the screen off would lock them behind their credentials. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Some manufacturers, like Samsung, refuse to open the device admin activation screen when it is started as a new task, which is the only way the notification handling can start an activity. Route the request through a transparent activity that opens the activation screen from its own context instead, so the screen off command can guide the user to activate the device admin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
Adds support for a new command_screen_off notification command to cut display power while keeping the device awake (via a partial wake lock + DevicePolicyManager.lockNow()), and extends the existing command_screen_on flow to release that wake lock again. This fits into the notification-command routing in MessagingManager and introduces the required device-admin plumbing (receiver + activation activity) plus Robolectric coverage.
Changes:
- Add
command_screen_offhandling inMessagingManager, including permission prompting and wake-lock release oncommand_screen_on - Introduce
ScreenOffHelper+ device-admin receiver + activation activity + admin policy XML - Add unit tests covering helper behavior, admin flows, and command routing
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| common/src/main/res/values/strings.xml | Adds device-admin label/description strings used in activation UI |
| automotive/src/main/AndroidManifest.xml | Registers the device-admin receiver and transparent activation activity for automotive build |
| app/src/test/kotlin/io/homeassistant/companion/android/util/ScreenOffHelperTest.kt | Robolectric tests for wake-lock behavior, keyguard refusal, and SecurityException handling |
| app/src/test/kotlin/io/homeassistant/companion/android/util/ScreenOffAdminRequestActivityTest.kt | Verifies admin-activation intent is launched with correct extras |
| app/src/test/kotlin/io/homeassistant/companion/android/util/ScreenOffAdminReceiverTest.kt | Ensures wake lock is released when admin is disabled |
| app/src/test/kotlin/io/homeassistant/companion/android/notifications/MessagingManagerScreenCommandsTest.kt | Tests routing for screen on/off commands and foreground permission prompting |
| app/src/main/res/xml/screen_off_device_admin.xml | Declares minimal device-admin policy (force-lock) |
| app/src/main/res/xml/changelog_master.xml | Documents new command_screen_off feature in changelog |
| app/src/main/kotlin/io/homeassistant/companion/android/util/ScreenOffHelper.kt | Implements screen-off behavior using device admin + partial wake lock |
| app/src/main/kotlin/io/homeassistant/companion/android/util/ScreenOffAdminRequestActivity.kt | Transparent activity that launches the system admin-activation screen |
| app/src/main/kotlin/io/homeassistant/companion/android/util/ScreenOffAdminReceiver.kt | DeviceAdminReceiver that releases wake lock if admin is disabled |
| app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt | Adds command constant, routing, permission request hook, and wake-lock release on screen-on |
| app/src/main/AndroidManifest.xml | Registers the device-admin receiver and transparent activation activity for the main app |
A recreated instance has no guarantee of a pending activation result, so returning early could leave the transparent activity covering the task. The changelog entries also read awkwardly with the duplicated word command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
requires device admin, but works inside google's boundaries |
|
Hi, thanks for contributing. Did you see #6973?
Is this correct? Based on what I can find there is no official Android 17 for any Samsung device yet. The Tab A9 (your device, from 2023) isn't even on lists of Samsung devices getting Android 17. |
|
Hello. I believe you are correct regarding android 17, and I'm unsure why Fable chose that descriptor, it had ADB access to the device, my SM-X110 devices are running 16 and are too old and slow for another major os revision. Unfortunately I agree with your thoughts about Device Admin, but I also am desperate. Only Android devices are available sub-$200 for multiple rooms wallpanel mounting. Since I have a wallpanel in my bedroom, screen dimming is not good enough, and running Fully is worse than running a forked android companion app. I would understand your decision either way to merge or close this PR. Home Assistant has undoubtedly countless android phone users who I would say should be the priority. Wallpanel power users are the only users who need this feature, and the need is not going away if you close a second or third similar PR. I have an earlier version of this feature that paints a black overlay instead of locking the display. The overlay implementation has the advantage of being quicker and keeping the camera/mic active, I use the tablets' camera in a WHIP WebRTC stream for person detection, and screen lock cuts the camera. Screen off is missing, but I would be happy to modify this PR to only include the black overlay implementation? Since it's in my Claude memory, it would be trivial to upload it. Do you want it? |
|
btw "blacking" the screen could be seen as more of a platform policy violation than adding Device Admin permissions to turn off the screen. Samsung/google cut the cameras when the screen is locked and appears off by design. |
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertNotNull | ||
| import org.junit.Assert.assertNull | ||
| import org.junit.Assert.assertTrue |
There was a problem hiding this comment.
Use the Jupiter API like in the rest of the project.
There was a problem hiding this comment.
This one drives the real activity lifecycle through ActivityScenario/Robolectric.buildActivity, so it needs Robolectric, and Robolectric requires the JUnit 4 runner — matching the project guidance to use JUnit 4 only where Robolectric is required. Happy to convert if there is a Jupiter-compatible setup I missed.
There was a problem hiding this comment.
The guidance is for the runner not the assert API.
| import io.mockk.every | ||
| import io.mockk.mockk | ||
| import io.mockk.verify | ||
| import org.junit.Assert.assertEquals |
There was a problem hiding this comment.
Kept on Robolectric/JUnit 4: the test relies on idling the main looper, the started-activity capture, and the foreground-process shadow, which have no mock-context equivalent. The two test classes that don't need Robolectric (helper and receiver) are plain Jupiter in be39869.
|
@jpelgrom what about gating this on minimal only? It's coming back quite often. |
…off-command # Conflicts: # app/src/main/res/xml/changelog_master.xml
Apply review feedback: - ScreenOffHelper now also wakes the screen up, so the whole wake lock flow lives in one tested place; MessagingManager only routes the commands - ScreenOffAdminRequestActivity provides its Intent through newInstance() - a device without a device admin activation screen shows a toast instead of failing silently, and the command tells the user it is not supported on Automotive - the device admin receiver and activity are no longer declared on Automotive, where device admin is unavailable - ScreenOffHelperTest and ScreenOffAdminReceiverTest run as plain JUnit Jupiter tests with mocks instead of Robolectric Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review! Addressed in be39869 (after merging
On the admin API question — happy to hear @jpelgrom's take; the device admin force-lock policy is the only non-root way we found to actually power the display off while the device stays awake. |
Turning the display off with the screen off command stops the activity, which paused the WebView and, as the only started WebView screen, froze the global WebView timers. The open dashboard's JavaScript stopped for the whole commanded off period — a server restart during it left the frontend's own reconnect countdown frozen mid-flight, so after screen on it kept waiting out the remaining interval instead of reconnecting (observed on a wall tablet: "Retrying in 41 seconds" right after wake). While the screen is commanded off the frontend now keeps running: the lifecycle effect skips pausing the WebView and the global timers, while still unregistering from the started count, which therefore stays exact. The next first started screen resumes timers, a no-op when they kept running, and the next regular stop pauses them again. The commanded off state now also ends when the screen turns on by any means: a screen on broadcast receiver, registered only while the wake lock is held, releases it — so a power button wake can no longer leave the wake lock held nor the WebView permanently unpaused. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field testing showed the keep-running bypass keeps nothing meaningful alive: the camera pipeline stops when the page is hidden regardless of JavaScript timers, so it only ran an invisible dashboard. The WebView returns to the platform's documented pause behavior. The unbounded partial wake lock is removed with it. Background partial wake locks held for hours are flagged by Android vitals, and Doze can suspend networking regardless of wake locks — reachability while the screen is off is the job of the persistent connection setting and its foreground worker, not this command. The screen on broadcast receiver leaves with the wake lock it existed to release, making the helper stateless. The changelog no longer promises that the device stays awake and connected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The command refuses to lock a device behind credentials, yet a user with a secure lock screen was still walked through granting the device admin — an invasive permission that could then never be used. The keyguard is now checked before the activation is requested and a toast explains why the command is unavailable instead. Also removes the onDisabled wake-up left behind by the wake lock removal: it existed to release the lock, and deactivating the admin requires the screen to be on anyway. The receiver is back to a plain declaration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I don't think the request is that common - it just happened to show up twice in one month. My main objection is with 1. the uncertainty it creates in store policies and 2. the risk of device admin if we do not lock it down properly. 1 would be mitigated if we limit it to minimal (I don't believe any of the stores we submit minimal to have strong policies on this), 2 would be harder to mitigate but the current iteration appears to be more locked down than earlier ones. However, in the latest commits there is suddenly a new limitation where the command won't run if the device has a PIN etc. set which doesn't make sense to me. A lock is very common. If the user wants to turn off the screen on their device and they have a lock set that's something they should be aware of, not refused. We shouldn't promote insecure behavior by forcing no lock. |
Summary
Adds a
command_screen_offnotification command that turns the display off, and extendscommand_screen_onto turn it back on. This is aimed at wall-mounted dashboards where the server decides when the panel should be dark (presence, night mode). Only the display is affected: whether the app stays reachable while the screen is off follows its connection settings, like the persistent connection and its foreground worker.The display power is cut with
DevicePolicyManager.lockNow(). This requires the user to activate a new, minimal device admin that holds only the force-lock policy. Activation is a documented two-step flow: when the command is used with the app in the foreground, the system's activation screen is opened (routed through a small transparent activity because some manufacturers, like Samsung, refuse to open that screen from a new task); in the background a missing-permission notification is posted instead, and the command has to be sent again after activating. The command is refused when a secure keyguard is set — and the device admin activation is never requested in that case — since turning the screen off would otherwise lock the device behind credentials. No wake lock is held: background partial wake locks are an Android vitals liability and Doze can suspend networking regardless, so reachability stays the job of the existing connection settings.Checklist
Link to pull request in documentation repositories
User Documentation: home-assistant/companion.home-assistant#1374
Any other notes
Tested on a Samsung SM-X110 (Android 16): screen turns off and back on via the commands, and with the persistent connection enabled, notifications keep arriving while the screen is off. Covered by unit tests for the helper (including the keyguard refusal and a
SecurityExceptionwhen the admin is deactivated mid-call), the admin receiver, the activation activity, and the command routing.