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

Commit dadce2bb authored by Yan Han's avatar Yan Han
Browse files

Add delayed Standby for Playback devices on hotplug out

Upon receiving a hotplug out event, a Playback device starts a 30 second
countdown. If no user interaction with the device occurs during that
time, it goes to Standby. Otherwise, it will check again every
30 seconds until a hotplug in event is received.

Test: atest HdmiCecLocalDevicePlaybackTest; manual
Bug: 206934433
Change-Id: If4a9f62c7ace93ee82a28c875d2efc7fa1dd635f
parent 7644c9c2
Loading
Loading
Loading
Loading
+36 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemProperties;
@@ -47,6 +48,10 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
    private static final boolean SET_MENU_LANGUAGE =
            HdmiProperties.set_menu_language_enabled().orElse(false);

    // How long to wait after hotplug out before possibly going to Standby.
    @VisibleForTesting
    static final long STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS = 30_000;

    // Used to keep the device awake while it is the active source. For devices that
    // cannot wake up via CEC commands, this address the inconvenience of having to
    // turn them on. True by default, and can be disabled (i.e. device can go to sleep
@@ -55,6 +60,9 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
    // Lazily initialized - should call getWakeLock() to get the instance.
    private ActiveWakeLock mWakeLock;

    // Handler for queueing a delayed Standby runnable after hotplug out.
    private Handler mDelayedStandbyHandler;

    // Determines what action should be taken upon receiving Routing Control messages.
    @VisibleForTesting
    protected HdmiProperties.playback_device_action_on_routing_control_values
@@ -64,6 +72,8 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {

    HdmiCecLocalDevicePlayback(HdmiControlService service) {
        super(service, HdmiDeviceInfo.DEVICE_PLAYBACK);

        mDelayedStandbyHandler = new Handler(service.getServiceLooper());
    }

    @Override
@@ -195,9 +205,33 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
    void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();
        mCecMessageCache.flushAll();
        // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3.
        if (!connected) {

        if (connected) {
            mDelayedStandbyHandler.removeCallbacksAndMessages(null);
        } else {
            // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3
            getWakeLock().release();

            mDelayedStandbyHandler.removeCallbacksAndMessages(null);
            mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
                    STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        }
    }

    /**
     * Runnable for going to Standby if the device has been inactive for a certain amount of time.
     * Posts a new instance of itself as a delayed message if the device was active.
     */
    private class DelayedStandbyRunnable implements Runnable {
        @Override
        public void run() {
            if (mService.getPowerManagerInternal().wasDeviceIdleFor(
                    STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS)) {
                mService.standby();
            } else {
                mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
                        STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
            }
        }
    }

+24 −10
Original line number Diff line number Diff line
@@ -407,6 +407,9 @@ public class HdmiControlService extends SystemService {
    @Nullable
    private PowerManagerWrapper mPowerManager;

    @Nullable
    private PowerManagerInternalWrapper mPowerManagerInternal;

    @Nullable
    private Looper mIoLooper;

@@ -734,6 +737,7 @@ public class HdmiControlService extends SystemService {
            mTvInputManager = (TvInputManager) getContext().getSystemService(
                    Context.TV_INPUT_SERVICE);
            mPowerManager = new PowerManagerWrapper(getContext());
            mPowerManagerInternal = new PowerManagerInternalWrapper();
        } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
            runOnServiceThread(this::bootCompleted);
        }
@@ -758,10 +762,19 @@ public class HdmiControlService extends SystemService {
        mPowerManager = powerManager;
    }

    @VisibleForTesting
    void setPowerManagerInternal(PowerManagerInternalWrapper powerManagerInternal) {
        mPowerManagerInternal = powerManagerInternal;
    }

    PowerManagerWrapper getPowerManager() {
        return mPowerManager;
    }

    PowerManagerInternalWrapper getPowerManagerInternal() {
        return mPowerManagerInternal;
    }

    /**
     * Called when the initialization of local devices is complete.
     */
@@ -3239,20 +3252,21 @@ public class HdmiControlService extends SystemService {
        assertRunOnServiceThread();
        Slog.v(TAG, "onPendingActionsCleared");

        if (!mPowerStatusController.isPowerStatusTransientToStandby()) {
            return;
        }
        if (mPowerStatusController.isPowerStatusTransientToStandby()) {
            mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY);
            for (HdmiCecLocalDevice device : mHdmiCecNetwork.getLocalDeviceList()) {
                device.onStandby(mStandbyMessageReceived, standbyAction);
            }
        mStandbyMessageReceived = false;
            if (!isAudioSystemDevice()) {
                mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, false);
                mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, DISABLED);
            }
        }

        // Always reset this flag to set up for the next standby
        mStandbyMessageReceived = false;
    }

    private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) {
        VendorCommandListenerRecord record = new VendorCommandListenerRecord(listener, deviceType);
        try {
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.os.PowerManagerInternal;

import com.android.server.LocalServices;

/**
 * Abstraction around {@link PowerManagerInternal} to allow faking it in tests.
 */
public class PowerManagerInternalWrapper {
    private static final String TAG = "PowerManagerInternalWrapper";

    private PowerManagerInternal mPowerManagerInternal;

    public PowerManagerInternalWrapper() {
        mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
    }

    /**
     * Wraps {@link PowerManagerInternal#wasDeviceIdleFor(long)}
     */
    public boolean wasDeviceIdleFor(long ms) {
        return mPowerManagerInternal.wasDeviceIdleFor(ms);
    }
}
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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;

/**
 * Fake class which stubs PowerManagerInternalWrapper (useful for testing).
 */
public class FakePowerManagerInternalWrapper extends PowerManagerInternalWrapper {

    private long mIdleDurationMs = -1;

    /**
     * Sets the duration (in milliseconds) that this device has been idle for.
     */
    public void setIdleDuration(long idleDurationMs) {
        mIdleDurationMs = idleDurationMs;
    }

    @Override
    public boolean wasDeviceIdleFor(long ms) {
        return ms <= mIdleDurationMs;
    }
}
+40 −0
Original line number Diff line number Diff line
@@ -76,6 +76,8 @@ public class HdmiCecLocalDevicePlaybackTest {
    private int mPlaybackLogicalAddress;
    private boolean mWokenUp;
    private boolean mActiveMediaSessionsPaused;
    private FakePowerManagerInternalWrapper mPowerManagerInternal =
            new FakePowerManagerInternalWrapper();

    @Before
    public void setUp() {
@@ -146,6 +148,7 @@ public class HdmiCecLocalDevicePlaybackTest {
        mHdmiControlService.initService();
        mPowerManager = new FakePowerManagerWrapper(context);
        mHdmiControlService.setPowerManager(mPowerManager);
        mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
        mPlaybackPhysicalAddress = 0x2000;
        mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress);
@@ -1969,4 +1972,41 @@ public class HdmiCecLocalDevicePlaybackTest {
        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortPressed);
        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortReleased);
    }

    @Test
    public void onHotplugInAfterHotplugOut_noStandbyAfterDelay() {
        mPowerManager.setInteractive(true);
        mNativeWrapper.onHotplugEvent(1, false);
        mTestLooper.dispatchAll();

        mTestLooper.moveTimeForward(
                HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS / 2);
        mNativeWrapper.onHotplugEvent(1, true);
        mTestLooper.dispatchAll();

        mPowerManagerInternal.setIdleDuration(
                HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        mTestLooper.moveTimeForward(HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        mTestLooper.dispatchAll();

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

    @Test
    public void onHotplugOut_standbyAfterDelay_onlyAfterDeviceIdle() {
        mPowerManager.setInteractive(true);
        mNativeWrapper.onHotplugEvent(1, false);
        mTestLooper.dispatchAll();

        mPowerManagerInternal.setIdleDuration(0);
        mTestLooper.moveTimeForward(HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        mTestLooper.dispatchAll();
        assertThat(mPowerManager.isInteractive()).isTrue();

        mPowerManagerInternal.setIdleDuration(
                HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        mTestLooper.moveTimeForward(HdmiCecLocalDevicePlayback.STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        mTestLooper.dispatchAll();
        assertThat(mPowerManager.isInteractive()).isFalse();
    }
}