Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d9d7d52c authored by Paul Colta's avatar Paul Colta
Browse files

HDMI: Send device to sleep if TV reported it turned off.

Since TVs are not mandated to send <Standby> when going to sleep, this patch allows source devices to query TV's power status once every 60s to check if the TV turned off, s.t. they can go to sleep.

Flag: android.media.tv.flags.hdmi_control_enhanced_behavior
Bug: 332780751
Bug: 362555748
Change-Id: I5ffdadc74c86b0b0a6d7d118f06daed16954728b
parent 04b5dc6c
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -171,6 +171,17 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
                            addAndStartAction(
                                    new HotplugDetectionAction(HdmiCecLocalDevicePlayback.this));
                        }

                        if (mService.isHdmiControlEnhancedBehaviorFlagEnabled()) {
                            List<PowerStatusMonitorActionFromPlayback>
                                    powerStatusMonitorActionsFromPlayback =
                                    getActions(PowerStatusMonitorActionFromPlayback.class);
                            if (powerStatusMonitorActionsFromPlayback.isEmpty()) {
                                addAndStartAction(
                                        new PowerStatusMonitorActionFromPlayback(
                                                HdmiCecLocalDevicePlayback.this));
                            }
                        }
                    }
                });
        addAndStartAction(action);
@@ -686,6 +697,7 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
        removeAction(DeviceDiscoveryAction.class);
        removeAction(HotplugDetectionAction.class);
        removeAction(NewDeviceAction.class);
        removeAction(PowerStatusMonitorActionFromPlayback.class);
        super.disableDevice(initiatedByCec, callback);
        clearDeviceInfoList();
        checkIfPendingActionsCleared();
+6 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.hdmi;

import static android.media.tv.flags.Flags.hdmiControlEnhancedBehavior;

import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
@@ -5137,4 +5139,8 @@ public class HdmiControlService extends SystemService {
            tv().startArcAction(enabled, callback);
        }
    }

    protected boolean isHdmiControlEnhancedBehaviorFlagEnabled() {
        return hdmiControlEnhancedBehavior();
    }
}
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.hdmi;

import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;

import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

/**
 * This action is used by playback devices to query TV's power status such that they can go to
 * standby when the TV reports power off.
 */
public class PowerStatusMonitorActionFromPlayback extends HdmiCecFeatureAction {
    private static final String TAG = "PowerStatusMonitorActionFromPlayback";

    // State that waits for <Report Power Status> once sending <Give Device Power Status>
    // to all external devices.
    private static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 1;
    // State that waits for next monitoring.
    private static final int STATE_WAIT_FOR_NEXT_MONITORING = 2;
    // Monitoring interval (60s)
    @VisibleForTesting
    protected static final int MONITORING_INTERVAL_MS = 60000;
    // Timeout once sending <Give Device Power Status>
    private static final int REPORT_POWER_STATUS_TIMEOUT_MS = 5000;
    // Maximum number of retries in case the <Give Device Power Status> failed being sent or times
    // out.
    private static final int GIVE_POWER_STATUS_FOR_SOURCE_RETRIES = 5;
    private int mPowerStatusRetries = 0;

    PowerStatusMonitorActionFromPlayback(HdmiCecLocalDevice source) {
        super(source);
    }

    @Override
    boolean start() {
        // Start after timeout since the device just finished allocation.
        mState = STATE_WAIT_FOR_NEXT_MONITORING;
        addTimer(mState, MONITORING_INTERVAL_MS);
        return true;
    }

    @Override
    boolean processCommand(HdmiCecMessage cmd) {
        if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS
                && cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS
                && cmd.getSource() == Constants.ADDR_TV) {
            return handleReportPowerStatusFromTv(cmd);
        }
        return false;
    }

    private boolean handleReportPowerStatusFromTv(HdmiCecMessage cmd) {
        int powerStatus = cmd.getParams()[0] & 0xFF;
        if (powerStatus == POWER_STATUS_STANDBY) {
            Slog.d(TAG, "TV reported it turned off, going to sleep.");
            source().getService().standby();
            return true;
        }
        return false;
    }

    @Override
    void handleTimerEvent(int state) {
        switch (mState) {
            case STATE_WAIT_FOR_NEXT_MONITORING:
                mPowerStatusRetries = 0;
                queryPowerStatus();
                break;
            case STATE_WAIT_FOR_REPORT_POWER_STATUS:
                handleTimeout();
                break;
        }
    }

    private void queryPowerStatus() {
        sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(),
                        Constants.ADDR_TV));

        mState = STATE_WAIT_FOR_REPORT_POWER_STATUS;
        addTimer(mState, REPORT_POWER_STATUS_TIMEOUT_MS);
    }

    private void handleTimeout() {
        if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS) {
            if (mPowerStatusRetries++ < GIVE_POWER_STATUS_FOR_SOURCE_RETRIES) {
                queryPowerStatus();
            } else {
                mPowerStatusRetries = 0;
                mState = STATE_WAIT_FOR_NEXT_MONITORING;
                addTimer(mState, MONITORING_INTERVAL_MS);
            }
        }
    }
}
+44 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static com.android.server.hdmi.HdmiCecLocalDevicePlayback.POPUP_AFTER_ACT
import static com.android.server.hdmi.HdmiCecLocalDevicePlayback.STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_BOOT_UP;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.android.server.hdmi.PowerStatusMonitorActionFromPlayback.MONITORING_INTERVAL_MS;

import static com.google.common.truth.Truth.assertThat;

@@ -145,6 +146,11 @@ public class HdmiCecLocalDevicePlaybackTest {
                    protected void sendBroadcastAsUser(@RequiresPermission Intent intent) {
                        // do nothing
                    }

                    @Override
                    protected boolean isHdmiControlEnhancedBehaviorFlagEnabled() {
                        return true;
                    }
                };

        mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
@@ -2556,6 +2562,44 @@ public class HdmiCecLocalDevicePlaybackTest {
        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(unexpectedMessage);
    }

    @Test
    public void powerStatusMonitorActionFromPlayback_TvReportPowerOff_goToSleep() {
        mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
        mTestLooper.dispatchAll();

        assertThat(mHdmiCecLocalDevicePlayback.getActions(
                PowerStatusMonitorActionFromPlayback.class)).hasSize(1);
        assertThat(mPowerManager.isInteractive()).isTrue();
        mNativeWrapper.clearResultMessages();
        mTestLooper.moveTimeForward(MONITORING_INTERVAL_MS);
        mTestLooper.dispatchAll();

        HdmiCecMessage givePowerStatus =
                HdmiCecMessageBuilder.buildGiveDevicePowerStatus(mPlaybackLogicalAddress,
                        Constants.ADDR_TV);
        HdmiCecMessage reportPowerStatusTvOn =
                HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_TV, mPlaybackLogicalAddress,
                        HdmiControlManager.POWER_STATUS_ON);
        HdmiCecMessage reportPowerStatusTvStandby =
                HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_TV, mPlaybackLogicalAddress,
                        HdmiControlManager.POWER_STATUS_STANDBY);

        assertThat(mNativeWrapper.getResultMessages().contains(givePowerStatus)).isTrue();
        mNativeWrapper.onCecMessage(reportPowerStatusTvOn);
        mTestLooper.dispatchAll();

        assertThat(mPowerManager.isInteractive()).isTrue();
        mNativeWrapper.clearResultMessages();
        mTestLooper.moveTimeForward(MONITORING_INTERVAL_MS);
        mTestLooper.dispatchAll();

        assertThat(mNativeWrapper.getResultMessages().contains(givePowerStatus)).isTrue();
        mNativeWrapper.onCecMessage(reportPowerStatusTvStandby);
        mTestLooper.dispatchAll();

        assertThat(mPowerManager.isInteractive()).isFalse();
    }

    private void skipActiveSourceLostUi(long idleDuration) {
        mTestLooper.moveTimeForward(POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
        mTestLooper.dispatchAll();