Keep display.power on the Android baseline

screen_off blanks a fielded panel for real (owner/admin FORCE_LOCK, else the
accessibility lock); screen_on is a logged no-op. One capability renders both
dashboard buttons, so withholding the pair to hide the dead ON button also takes
blank-at-night — the half that gets scheduled — away from every panel that has
not updated. Panels that have updated declare for themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
ScreenTinker 2026-08-06 18:10:52 -05:00
parent 4f7b4e3989
commit b4363d8d26
3 changed files with 36 additions and 19 deletions

View file

@ -74,7 +74,7 @@ const BASELINE = {
// still in the field. Both are Tier 0: MainActivity applies them with no owner, no admin and
// no WRITE_SETTINGS, so they are unconditional on any build a fielded panel could be running.
'audio.mute', 'audio.volume',
'display.rotation', 'display.brightness',
'display.rotation', 'display.power', 'display.brightness',
// Capture without accessibility falls back to ScreenshotCapture.captureView, which is a real
// frame of the player's own view — i.e. of the content. Narrower than the full-screen path,
// but the operator gets a picture, not a dead button.
@ -82,12 +82,24 @@ const BASELINE = {
'remote.input',
'system.restart_player', 'system.self_update',
'sync.clock', 'offline.cache',
// NOT display.power. v1.9.28 MainActivity answers screen_on with
// display.power is KEPT for the un-updated Android fleet, deliberately, with the trade-off
// recorded here because it is genuinely two-sided.
//
// v1.9.28 MainActivity answers screen_on with
// Log.w("screen_on: no privileged wake path on a non-rooted panel — no-op")
// so the ON half is dead on 100% of fielded Android panels, and screen_off only works with
// owner / device-admin / accessibility. The dashboard renders BOTH buttons off this one
// capability. A panel you can sleep and cannot wake is the worst possible version of this
// feature, which is exactly why PlayerCapabilities.kt gates its own claim on both halves.
// so the ON half is dead on every fielded Android panel, while screen_off does work (device
// owner / device-admin FORCE_LOCK, else the accessibility lock). One capability renders BOTH
// dashboard buttons, so this baseline cannot offer the working half without the dead one.
//
// Withholding it takes away blank-at-night, which is the half signage actually schedules, from
// every panel that has not updated. Keeping it means an operator can sleep a screen and not
// wake it from the dashboard — mitigated by the fact that a schedule, a restart, or anyone
// standing at the panel will wake it, while nothing else can blank it.
//
// A panel that HAS updated declares for itself, and PlayerCapabilities.kt gates its own claim
// on both halves — so this governs the un-updated fleet only. If the dead ON button turns out
// to be the louder complaint, split it into display.power_off / display.power_on rather than
// dropping the pair.
//
// NOT system.reboot. STPolicy.reboot() requires device owner; off-owner v1.9.28 falls back to
// the accessibility power DIALOG, which needs a human standing at the screen — and on the

View file

@ -27,16 +27,17 @@ test('the legacy fleet is not locked out of the commands it has always accepted'
// nothing, and a refusal keyed off "declared nothing => supports nothing" bricks every control
// in the product at once.
const legacy = { client_type: 'apk', android_version: '9' };
for (const cmd of ['launch', 'refresh', 'update', 'set_volume', 'set_brightness']) {
for (const cmd of ['launch', 'refresh', 'update', 'set_volume', 'set_brightness',
// screen_off blanks a fielded panel for real, and screen_on shares its capability. The pair
// stays reachable so scheduled blank-at-night keeps working on displays that have not updated;
// see the display.power note in lib/player-capabilities.js for the accepted trade-off.
'screen_off', 'screen_on']) {
assert.equal(caps.commandAllowed(legacy, cmd).ok, true, `${cmd} must still reach a legacy Android panel`);
}
// reboot / screen_on / screen_off are NOT on that list any more, and that is the parity audit's
// finding rather than an oversight: v1.9.28 answers screen_on with a logged no-op on every
// panel, and STPolicy.reboot() needs device owner. Keeping them would have been the other half
// of the same bug — a control that appears to work and changes nothing.
for (const cmd of ['reboot', 'screen_on', 'screen_off']) {
assert.equal(caps.commandAllowed(legacy, cmd).ok, false, `${cmd} is privilege-gated on a fielded panel`);
}
// reboot IS refused, and that is the parity audit's finding rather than an oversight:
// STPolicy.reboot() needs device owner, and the off-owner fallback paints an accessibility power
// DIALOG over the signage that a human has to dismiss. That is worse than a refusal.
assert.equal(caps.commandAllowed(legacy, 'reboot').ok, false, 'reboot needs device owner');
});
test('a command with no capability requirement is never refused', () => {

View file

@ -212,11 +212,15 @@ test('BICONDITIONAL: offline.cache in a baseline iff that player has a media cac
'a widget that refuses to register a worker cannot cache one byte');
});
test('display.power is claimed only where BOTH halves work without privilege', () => {
// Android v1.9.28 answers screen_on with a logged no-op and gates screen_off on
// owner/admin/accessibility, so the pair is not honest for an undeclared panel. Tizen implements
// both with a plain overlay and no signing requirement.
assert.equal(caps.BASELINE.android.includes('display.power'), false);
test('display.power is claimed where at least one half does something', () => {
// Android is the deliberate exception and the baseline comment carries the reasoning: v1.9.28
// answers screen_on with a logged no-op, but screen_off genuinely blanks the panel (owner/admin
// FORCE_LOCK, else the accessibility lock). One capability renders both dashboard buttons, so
// withholding it to hide the dead ON button also removes blank-at-night — the half signage
// actually schedules — from every panel that has not updated. Kept, knowingly.
// Tizen implements both with a plain overlay and no signing requirement.
assert.ok(caps.BASELINE.android.includes('display.power'),
'screen_off works on a fielded Android panel; do not withhold it to hide screen_on');
assert.ok(caps.BASELINE.tizen.includes('display.power'));
assert.equal(caps.BASELINE.web.includes('display.power'), false, 'a browser tab cannot power a panel');
assert.equal(caps.BASELINE.brightsign.includes('display.power'), false, 'CEC needs the host bridge');