Add retries to CEC detection
When CEC source devices attempt to query whether the sink device supports CEC, they query the target devices power status. As this query might happen when the sink device is just waking up, as part of a One Touch Play sequence, the sink device might be too slow to respond to the query in time (2s). Add a single retry to ensure the sink device has another chance to respond to the query. If the target device doesn't ACK the query message, assume no CEC support. The retry should increase stability of CEC volume control mode detection. Bug: 172539380 Test: atest DevicePowerStatusActionTest Change-Id: Ic6cb5da968da5e12fbaea6b3ad2fa5cff82728e8
This commit is contained in:
@@ -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<IHdmiControlCallback> 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();
|
||||
|
||||
@@ -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<HdmiCecLocalDevice> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user