diff --git a/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java b/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java index b52ab76660d7d..1a0a639c3c66c 100644 --- a/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java +++ b/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java @@ -20,6 +20,7 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiPlaybackClient; import android.hardware.hdmi.HdmiPlaybackClient.DisplayStatusCallback; import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.tv.cec.V1_0.SendMessageResult; import android.os.RemoteException; import android.util.Slog; @@ -42,6 +43,10 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction { private final int mTargetAddress; private final List mCallbacks = new ArrayList<>(); + // Retry the power status query as it might happen when the target device is waking up. In + // that case a device may be quite busy and can fail to respond within the 2s timeout. + private int mRetriesOnTimeout = 1; + static DevicePowerStatusAction create(HdmiCecLocalDevice source, int targetAddress, IHdmiControlCallback callback) { if (source == null || callback == null) { @@ -68,13 +73,21 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction { private void queryDevicePowerStatus() { sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), - mTargetAddress)); + mTargetAddress), error -> { + // Don't retry on timeout if the remote device didn't even ACK the message. Assume + // the device is not present or not capable of CEC. + if (error == SendMessageResult.NACK) { + // Got no response from TV. Report status 'unknown'. + invokeCallback(HdmiControlManager.POWER_STATUS_UNKNOWN); + finish(); + } + }); } @Override boolean processCommand(HdmiCecMessage cmd) { if (mState != STATE_WAITING_FOR_REPORT_POWER_STATUS - || mTargetAddress != cmd.getSource()) { + || mTargetAddress != cmd.getSource()) { return false; } if (cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS) { @@ -92,6 +105,12 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction { return; } if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) { + if (mRetriesOnTimeout > 0) { + mRetriesOnTimeout--; + start(); + return; + } + // Got no response from TV. Report status 'unknown'. invokeCallback(HdmiControlManager.POWER_STATUS_UNKNOWN); finish(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java new file mode 100644 index 0000000000000..da9dcb79fb764 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2020 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 com.android.server.hdmi.Constants.ADDR_TV; +import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.tv.cec.V1_0.SendMessageResult; +import android.media.AudioManager; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.IThermalService; +import android.os.Looper; +import android.os.PowerManager; +import android.os.test.TestLooper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +/** Tests for {@link DevicePowerStatusAction} */ +@SmallTest +@RunWith(JUnit4.class) +public class DevicePowerStatusActionTest { + + private static final int TIMEOUT_MS = HdmiConfig.TIMEOUT_MS + 1; + + private Context mContextSpy; + private HdmiControlService mHdmiControlService; + private HdmiCecLocalDevice mPlaybackDevice; + private FakeNativeWrapper mNativeWrapper; + + private TestLooper mTestLooper = new TestLooper(); + private ArrayList mLocalDevices = new ArrayList<>(); + private int mPhysicalAddress; + + private DevicePowerStatusAction mDevicePowerStatusAction; + + @Mock private IHdmiControlCallback mCallbackMock; + @Mock private IPowerManager mIPowerManagerMock; + @Mock private IThermalService mIThermalServiceMock; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + + PowerManager powerManager = new PowerManager(mContextSpy, mIPowerManagerMock, + mIThermalServiceMock, new Handler(mTestLooper.getLooper())); + when(mContextSpy.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager); + when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(powerManager); + when(mIPowerManagerMock.isInteractive()).thenReturn(true); + + HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); + + mHdmiControlService = new HdmiControlService(mContextSpy) { + @Override + AudioManager getAudioManager() { + return new AudioManager() { + @Override + public void setWiredDeviceConnectionState( + int type, int state, String address, String name) { + // Do nothing. + } + }; + } + + @Override + void wakeUp() { + } + + @Override + boolean isPowerStandby() { + return false; + } + + @Override + protected PowerManager getPowerManager() { + return powerManager; + } + + @Override + protected void writeStringSystemProperty(String key, String value) { + // do nothing + } + + @Override + protected HdmiCecConfig getHdmiCecConfig() { + return hdmiCecConfig; + } + }; + + Looper looper = mTestLooper.getLooper(); + mHdmiControlService.setIoLooper(looper); + mNativeWrapper = new FakeNativeWrapper(); + HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper( + this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); + mHdmiControlService.setCecController(hdmiCecController); + mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); + mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService)); + mHdmiControlService.initService(); + mPhysicalAddress = 0x2000; + mNativeWrapper.setPhysicalAddress(mPhysicalAddress); + mPlaybackDevice = new HdmiCecLocalDevicePlayback( + mHdmiControlService); + mPlaybackDevice.init(); + mLocalDevices.add(mPlaybackDevice); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV, + mCallbackMock); + mTestLooper.dispatchAll(); + } + + @Test + public void queryDisplayStatus_sendsRequestAndHandlesResponse() throws Exception { + mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); + mTestLooper.dispatchAll(); + + HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus( + mPlaybackDevice.mAddress, ADDR_TV); + assertThat(mNativeWrapper.getResultMessages()).contains(expected); + + HdmiCecMessage response = HdmiCecMessageBuilder.buildReportPowerStatus( + ADDR_TV, mPlaybackDevice.mAddress, HdmiControlManager.POWER_STATUS_STANDBY); + mNativeWrapper.onCecMessage(response); + mTestLooper.dispatchAll(); + + verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_STANDBY); + } + + @Test + public void queryDisplayStatus_sendsRequest_nack() throws Exception { + mNativeWrapper.setMessageSendResult(Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS, + SendMessageResult.NACK); + + mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); + mTestLooper.dispatchAll(); + + verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_UNKNOWN); + } + + @Test + public void queryDisplayStatus_sendsRequest_timeout_retriesSuccessfully() throws Exception { + mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); + + HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus( + mPlaybackDevice.mAddress, ADDR_TV); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(expected); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(expected); + + HdmiCecMessage response = HdmiCecMessageBuilder.buildReportPowerStatus( + ADDR_TV, mPlaybackDevice.mAddress, HdmiControlManager.POWER_STATUS_STANDBY); + mNativeWrapper.onCecMessage(response); + mTestLooper.dispatchAll(); + + verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_STANDBY); + } + + @Test + public void queryDisplayStatus_sendsRequest_timeout_retriesFailure() throws Exception { + mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); + mTestLooper.dispatchAll(); + + HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus( + mPlaybackDevice.mAddress, ADDR_TV); + assertThat(mNativeWrapper.getResultMessages()).contains(expected); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(expected); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + + verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_UNKNOWN); + } +}