diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java index d6ac25a72a6e4..b23395f36e63b 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java @@ -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); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 69f7af23fe4cb..c08d343ba0afd 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -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,18 +3252,19 @@ public class HdmiControlService extends SystemService { assertRunOnServiceThread(); Slog.v(TAG, "onPendingActionsCleared"); - if (!mPowerStatusController.isPowerStatusTransientToStandby()) { - return; - } - mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY); - for (HdmiCecLocalDevice device : mHdmiCecNetwork.getLocalDeviceList()) { - device.onStandby(mStandbyMessageReceived, standbyAction); + if (mPowerStatusController.isPowerStatusTransientToStandby()) { + mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY); + for (HdmiCecLocalDevice device : mHdmiCecNetwork.getLocalDeviceList()) { + device.onStandby(mStandbyMessageReceived, standbyAction); + } + 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; - if (!isAudioSystemDevice()) { - mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, false); - mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, DISABLED); - } } private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) { diff --git a/services/core/java/com/android/server/hdmi/PowerManagerInternalWrapper.java b/services/core/java/com/android/server/hdmi/PowerManagerInternalWrapper.java new file mode 100644 index 0000000000000..59671a87473e1 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/PowerManagerInternalWrapper.java @@ -0,0 +1,41 @@ +/* + * 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); + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakePowerManagerInternalWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakePowerManagerInternalWrapper.java new file mode 100644 index 0000000000000..968a75c043ab2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakePowerManagerInternalWrapper.java @@ -0,0 +1,37 @@ +/* + * 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; + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index 055459c867055..2d13e692a3a9d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -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(); + } }