Skip to content

Add command_screen_off to turn the display off while the device stays awake - #7162

Draft
markfrancisonly wants to merge 11 commits into
home-assistant:mainfrom
markfrancisonly:feature/add-screen-off-command
Draft

Add command_screen_off to turn the display off while the device stays awake#7162
markfrancisonly wants to merge 11 commits into
home-assistant:mainfrom
markfrancisonly:feature/add-screen-off-command

Conversation

@markfrancisonly

@markfrancisonly markfrancisonly commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a command_screen_off notification command that turns the display off, and extends command_screen_on to 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

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

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 SecurityException when the admin is deactivated mid-call), the admin receiver, the activation activity, and the command routing.

markfrancisonly and others added 2 commits July 10, 2026 08:57
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>
Copilot AI review requested due to automatic review settings July 11, 2026 17:16

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @markfrancisonly

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!

@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_off handling in MessagingManager, including permission prompting and wake-lock release on command_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

Comment thread app/src/main/res/xml/changelog_master.xml Outdated
Comment thread app/src/main/res/xml/changelog_master.xml Outdated
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>
@markfrancisonly

Copy link
Copy Markdown
Contributor Author

requires device admin, but works inside google's boundaries

@jpelgrom

Copy link
Copy Markdown
Member

Hi, thanks for contributing. Did you see #6973?

Tested on a Samsung SM-X110 (Android 17)

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.

@markfrancisonly

markfrancisonly commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

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?

@markfrancisonly markfrancisonly changed the title Feature/add screen off command Add command_screen_off to turn the display off while the device stays awake Jul 12, 2026
@markfrancisonly

Copy link
Copy Markdown
Contributor Author

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.

@TimoPtr TimoPtr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would like @jpelgrom opinion too about using the admin API.

Comment on lines +8 to +11
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use the Jupiter API like in the rest of the project.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use Jupiter API

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread automotive/src/main/AndroidManifest.xml Outdated
@home-assistant
home-assistant Bot marked this pull request as draft July 13, 2026 13:09
@TimoPtr

TimoPtr commented Jul 13, 2026

Copy link
Copy Markdown
Member

@jpelgrom what about gating this on minimal only? It's coming back quite often.

markfrancisonly and others added 2 commits July 19, 2026 14:04
…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>
@markfrancisonly

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressed in be39869 (after merging main):

  • ScreenOffHelper owns the whole wake lock flow for both commands; MessagingManager only routes
  • ScreenOffAdminRequestActivity.newInstance(), plus a toast when the activation screen cannot be opened
  • Automotive: the admin receiver/activity are no longer declared there, and the command shows a “not supported” toast
  • ScreenOffHelperTest and ScreenOffAdminReceiverTest are plain JUnit Jupiter now; the activity and command-routing tests stay on Robolectric (JUnit 4) since they need the activity lifecycle and looper/activity shadows

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.

@markfrancisonly
markfrancisonly requested a review from TimoPtr July 19, 2026 19:00
markfrancisonly and others added 4 commits July 19, 2026 15:02
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>
@jpelgrom

Copy link
Copy Markdown
Member

I would like @jpelgrom opinion too about using the admin API.

@jpelgrom what about gating this on minimal only? It's coming back quite often.

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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants