[CEC] Always send <Set Stream Path> after Routing Changes

Completely removes querying for power status of the tail device

Bug: 154458501
Test: atest RoutingControlActionTest
Change-Id: Ief7d7d5d72e6c8f17889832469d82e1fdf9115ad
This commit is contained in:
Michal Olech
2020-05-08 14:58:16 +02:00
parent 5c12473638
commit 9582aceaba
4 changed files with 297 additions and 88 deletions

View File

@@ -88,7 +88,7 @@ final class ActiveSourceHandler {
tv.updateActiveSource(current, "ActiveSourceHandler");
invokeCallback(HdmiControlManager.RESULT_SUCCESS);
} else {
tv.startRoutingControl(newActive.physicalAddress, current.physicalAddress, true,
tv.startRoutingControl(newActive.physicalAddress, current.physicalAddress,
mCallback);
}
}

View File

@@ -370,12 +370,11 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
return;
}
int newPath = mService.portIdToPath(portId);
startRoutingControl(oldPath, newPath, true, callback);
startRoutingControl(oldPath, newPath, callback);
}
@ServiceThreadOnly
void startRoutingControl(int oldPath, int newPath, boolean queryDevicePowerStatus,
IHdmiControlCallback callback) {
void startRoutingControl(int oldPath, int newPath, IHdmiControlCallback callback) {
assertRunOnServiceThread();
if (oldPath == newPath) {
return;
@@ -385,7 +384,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
mService.sendCecCommand(routingChange);
removeAction(RoutingControlAction.class);
addAndStartAction(
new RoutingControlAction(this, newPath, queryDevicePowerStatus, callback));
new RoutingControlAction(this, newPath, callback));
}
@ServiceThreadOnly
@@ -554,7 +553,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
if (isTailOfActivePath(path, getActivePath())) {
int newPath = mService.portIdToPath(getActivePortId());
setActivePath(newPath);
startRoutingControl(getActivePath(), newPath, false, null);
startRoutingControl(getActivePath(), newPath, null);
return true;
}
return false;
@@ -598,7 +597,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
getActiveSource().invalidate();
removeAction(RoutingControlAction.class);
int newPath = HdmiUtils.twoBytesToInt(params, 2);
addAndStartAction(new RoutingControlAction(this, newPath, true, null));
addAndStartAction(new RoutingControlAction(this, newPath, null));
}
return true;
}
@@ -1140,7 +1139,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
// Seq #23
if (isTailOfActivePath(path, getActivePath())) {
int newPath = mService.portIdToPath(getActivePortId());
startRoutingControl(getActivePath(), newPath, true, null);
startRoutingControl(getActivePath(), newPath, null);
}
}
@@ -1158,7 +1157,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
if (!routingForBootup && !isProhibitMode()) {
int newPath = mService.portIdToPath(getActivePortId());
setActivePath(newPath);
startRoutingControl(getActivePath(), newPath, routingForBootup, null);
startRoutingControl(getActivePath(), newPath, null);
}
} else {
int activePath = mService.getPhysicalAddress();

View File

@@ -18,12 +18,11 @@ package com.android.server.hdmi;
import android.annotation.Nullable;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.os.RemoteException;
import android.util.Slog;
import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
import com.android.internal.annotations.VisibleForTesting;
/**
* Feature action for routing control. Exchanges routing-related commands with other devices
@@ -43,23 +42,12 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
// State in which we wait for <Routing Information> to arrive. If timed out, we use the
// latest routing path to set the new active source.
private static final int STATE_WAIT_FOR_ROUTING_INFORMATION = 1;
// State in which we wait for <Report Power Status> in response to <Give Device Power Status>
// we have sent. If the response tells us the device power is on, we send <Set Stream Path>
// to make it the active source. Otherwise we do not send <Set Stream Path>, and possibly
// just show the blank screen.
private static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 2;
@VisibleForTesting
static final int STATE_WAIT_FOR_ROUTING_INFORMATION = 1;
// Time out in millseconds used for <Routing Information>
private static final int TIMEOUT_ROUTING_INFORMATION_MS = 1000;
// Time out in milliseconds used for <Report Power Status>
private static final int TIMEOUT_REPORT_POWER_STATUS_MS = 1000;
// true if <Give Power Status> should be sent once the new active routing path is determined.
private final boolean mQueryDevicePowerStatus;
// If set to true, call {@link HdmiControlService#invokeInputChangeListener()} when
// the routing control/active source change happens. The listener should be called if
// the events are triggered by external events such as manual switch port change or incoming
@@ -71,12 +59,10 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
// The latest routing path. Updated by each <Routing Information> from CEC switches.
private int mCurrentRoutingPath;
RoutingControlAction(HdmiCecLocalDevice localDevice, int path, boolean queryDevicePowerStatus,
IHdmiControlCallback callback) {
RoutingControlAction(HdmiCecLocalDevice localDevice, int path, IHdmiControlCallback callback) {
super(localDevice);
mCallback = callback;
mCurrentRoutingPath = path;
mQueryDevicePowerStatus = queryDevicePowerStatus;
// Callback is non-null when routing control action is brought up by binder API. Use
// this as an indicator for the input change notification. These API calls will get
// the result through this callback, not through notification. Any other events that
@@ -109,39 +95,16 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
removeActionExcept(RoutingControlAction.class, this);
addTimer(mState, TIMEOUT_ROUTING_INFORMATION_MS);
return true;
} else if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS
&& opcode == Constants.MESSAGE_REPORT_POWER_STATUS) {
handleReportPowerStatus(cmd.getParams()[0]);
return true;
}
return false;
}
private void handleReportPowerStatus(int devicePowerStatus) {
if (isPowerOnOrTransient(getTvPowerStatus())) {
updateActiveInput();
if (isPowerOnOrTransient(devicePowerStatus)) {
sendSetStreamPath();
}
}
finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
}
private void updateActiveInput() {
HdmiCecLocalDeviceTv tv = tv();
tv.setPrevPortId(tv.getActivePortId());
tv.updateActiveInput(mCurrentRoutingPath, mNotifyInputChange);
}
private int getTvPowerStatus() {
return tv().getPowerStatus();
}
private static boolean isPowerOnOrTransient(int status) {
return status == HdmiControlManager.POWER_STATUS_ON
|| status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
}
private void sendSetStreamPath() {
sendCommand(HdmiCecMessageBuilder.buildSetStreamPath(getSourceAddress(),
mCurrentRoutingPath));
@@ -160,46 +123,13 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
}
switch (timeoutState) {
case STATE_WAIT_FOR_ROUTING_INFORMATION:
HdmiDeviceInfo device =
localDevice().mService.getHdmiCecNetwork().getDeviceInfoByPath(
mCurrentRoutingPath);
if (device != null && mQueryDevicePowerStatus) {
int deviceLogicalAddress = device.getLogicalAddress();
queryDevicePowerStatus(deviceLogicalAddress, new SendMessageCallback() {
@Override
public void onSendCompleted(int error) {
handlDevicePowerStatusAckResult(
error == HdmiControlManager.RESULT_SUCCESS);
}
});
} else {
updateActiveInput();
finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
}
return;
case STATE_WAIT_FOR_REPORT_POWER_STATUS:
if (isPowerOnOrTransient(getTvPowerStatus())) {
updateActiveInput();
sendSetStreamPath();
}
updateActiveInput();
sendSetStreamPath();
finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
return;
}
}
private void queryDevicePowerStatus(int address, SendMessageCallback callback) {
sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), address),
callback);
}
private void handlDevicePowerStatusAckResult(boolean acked) {
if (acked) {
mState = STATE_WAIT_FOR_REPORT_POWER_STATUS;
addTimer(mState, TIMEOUT_REPORT_POWER_STATUS_MS);
} else {
updateActiveInput();
sendSetStreamPath();
finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
default:
Slog.e("CEC", "Invalid timeoutState (" + timeoutState + ").");
return;
}
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright (C) 2014 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_AUDIO_SYSTEM;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
import static com.android.server.hdmi.Constants.ADDR_TUNER_1;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
import static com.android.server.hdmi.Constants.MESSAGE_ACTIVE_SOURCE;
import static com.android.server.hdmi.Constants.MESSAGE_ROUTING_INFORMATION;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTING_INFORMATION;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.hdmi.IHdmiControlCallback;
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 com.android.server.hdmi.HdmiCecFeatureAction.ActionTimer;
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;
import java.util.List;
@SmallTest
@RunWith(JUnit4.class)
public class RoutingControlActionTest {
/*
* Example connection diagram used in tests. Double-lined paths indicate the currently active
* routes.
*
*
* +-----------+
* | TV |
* | 0.0.0.0 |
* +---+-----+-+
* | |
* <----------+ 1) AVR -> Switch
* +----------+ | | +-----------+
* | AVR +---------+ +--+ Switch |
* | 1.0.0.0 | | 2.0.0.0 |
* +--+---++--+ +--++-----+-+ <-------+ 2) Recorder -> Blu-ray
* | || || |
* | || || +--------+
* +-----------+ | || +----------+ +----++----+ |
* | XBox +--+ ++--+ Tuner | | Blueray | +-----+----+
* | 1.1.0.0 | | 1.2.0.0 | | 2.1.0.0 | | Recorder |
* +-----------+ +----++----+ +----------+ | 2.2.0.0 |
* || +----------+
* ||
* +----++----+
* | Player |
* | 1.2.1.0 |
* +----------+
*
*/
private static final int PHYSICAL_ADDRESS_TV = 0x0000;
private static final int PHYSICAL_ADDRESS_AVR = 0x1000;
private static final int PHYSICAL_ADDRESS_SWITCH = 0x2000;
private static final int PHYSICAL_ADDRESS_TUNER = 0x1200;
private static final int PHYSICAL_ADDRESS_PLAYER = 0x1210;
private static final int PHYSICAL_ADDRESS_BLUERAY = 0x2100;
private static final int PHYSICAL_ADDRESS_RECORDER = 0x2200;
private static final int PORT_1 = 1;
private static final int PORT_2 = 2;
private static final int VENDOR_ID_AVR = 0x11233;
private static final byte[] TUNER_PARAM =
new byte[] {(PHYSICAL_ADDRESS_TUNER >> 8) & 0xFF, PHYSICAL_ADDRESS_TUNER & 0xFF};
private static final byte[] PLAYER_PARAM =
new byte[] {(PHYSICAL_ADDRESS_PLAYER >> 8) & 0xFF, PHYSICAL_ADDRESS_PLAYER & 0xFF};
private static final HdmiDeviceInfo DEVICE_INFO_AVR =
new HdmiDeviceInfo(ADDR_AUDIO_SYSTEM, PHYSICAL_ADDRESS_AVR, PORT_1,
HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, VENDOR_ID_AVR, "Audio");
private static final HdmiDeviceInfo DEVICE_INFO_PLAYER =
new HdmiDeviceInfo(ADDR_PLAYBACK_1, PHYSICAL_ADDRESS_PLAYER, PORT_1,
HdmiDeviceInfo.DEVICE_PLAYBACK, VENDOR_ID_AVR, "Player");
private static final HdmiCecMessage ROUTING_INFORMATION_TUNER = new HdmiCecMessage(
ADDR_UNREGISTERED, ADDR_BROADCAST, MESSAGE_ROUTING_INFORMATION, TUNER_PARAM);
private static final HdmiCecMessage ROUTING_INFORMATION_PLAYER = new HdmiCecMessage(
ADDR_UNREGISTERED, ADDR_BROADCAST, MESSAGE_ROUTING_INFORMATION, PLAYER_PARAM);
private static final HdmiCecMessage ACTIVE_SOURCE_TUNER = new HdmiCecMessage(
ADDR_TUNER_1, ADDR_BROADCAST, MESSAGE_ACTIVE_SOURCE, TUNER_PARAM);
private static final HdmiCecMessage ACTIVE_SOURCE_PLAYER = new HdmiCecMessage(
ADDR_PLAYBACK_1, ADDR_BROADCAST, MESSAGE_ACTIVE_SOURCE, PLAYER_PARAM);
private HdmiControlService mHdmiControlService;
private HdmiCecController mHdmiCecController;
private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv;
private FakeNativeWrapper mNativeWrapper;
private Looper mMyLooper;
private TestLooper mTestLooper = new TestLooper();
private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
@Mock
private IPowerManager mIPowerManagerMock;
@Mock
private IThermalService mIThermalServiceMock;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
PowerManager powerManager = new PowerManager(context, mIPowerManagerMock,
mIThermalServiceMock, new Handler(mMyLooper));
HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(context);
mHdmiControlService =
new HdmiControlService(InstrumentationRegistry.getTargetContext()) {
@Override
boolean isControlEnabled() {
return true;
}
@Override
void wakeUp() {
}
@Override
protected void writeStringSystemProperty(String key, String value) {
// do nothing
}
@Override
boolean isPowerStandbyOrTransient() {
return false;
}
@Override
protected PowerManager getPowerManager() {
return powerManager;
}
@Override
protected HdmiCecConfig getHdmiCecConfig() {
return hdmiCecConfig;
}
};
mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
mHdmiCecLocalDeviceTv.init();
mHdmiControlService.setIoLooper(mMyLooper);
mNativeWrapper = new FakeNativeWrapper();
mHdmiCecController = HdmiCecController.createWithNativeWrapper(
mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
mHdmiControlService.setCecController(mHdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService));
mLocalDevices.add(mHdmiCecLocalDeviceTv);
HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
hdmiPortInfos[0] =
new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_AVR,
true, false, false);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
mNativeWrapper.setPhysicalAddress(0x0000);
mTestLooper.dispatchAll();
mNativeWrapper.clearResultMessages();
mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);
}
private static class TestActionTimer implements ActionTimer {
private int mState;
@Override
public void sendTimerMessage(int state, long delayMillis) {
mState = state;
}
@Override
public void clearTimerMessage() {
}
private int getState() {
return mState;
}
}
private static class TestInputSelectCallback extends IHdmiControlCallback.Stub {
private final List<Integer> mCallbackResult = new ArrayList<Integer>();
@Override
public void onComplete(int result) {
mCallbackResult.add(result);
}
private int getResult() {
assert (mCallbackResult.size() == 1);
return mCallbackResult.get(0);
}
}
private static RoutingControlAction createRoutingControlAction(HdmiCecLocalDeviceTv localDevice,
TestInputSelectCallback callback) {
return new RoutingControlAction(localDevice, PHYSICAL_ADDRESS_AVR, callback);
}
// Routing control succeeds against the device connected directly to the port. Action
// won't get any <Routing Information> in this case. It times out on <Routing Information>,
// regards the directly connected one as the new routing path to switch to.
@Test
public void testRoutingControl_succeedForDirectlyConnectedDevice() {
TestInputSelectCallback callback = new TestInputSelectCallback();
TestActionTimer actionTimer = new TestActionTimer();
mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);
RoutingControlAction action = createRoutingControlAction(mHdmiCecLocalDeviceTv, callback);
action.setActionTimer(actionTimer);
action.start();
assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_ROUTING_INFORMATION);
action.handleTimerEvent(actionTimer.getState());
mTestLooper.dispatchAll();
HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(
ADDR_TV, PHYSICAL_ADDRESS_AVR);
assertThat(mNativeWrapper.getResultMessages()).contains(setStreamPath);
}
// Succeeds by receiving a couple of <Routing Information> commands, followed by
// <Set Stream Path> going out in the end.
@Test
public void testRoutingControl_succeedForDeviceBehindSwitch() {
TestInputSelectCallback callback = new TestInputSelectCallback();
TestActionTimer actionTimer = new TestActionTimer();
mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_PLAYER);
RoutingControlAction action = createRoutingControlAction(mHdmiCecLocalDeviceTv, callback);
action.setActionTimer(actionTimer);
action.start();
assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_ROUTING_INFORMATION);
action.processCommand(ROUTING_INFORMATION_TUNER);
action.processCommand(ROUTING_INFORMATION_PLAYER);
action.handleTimerEvent(actionTimer.getState());
mTestLooper.dispatchAll();
HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(
ADDR_TV, PHYSICAL_ADDRESS_PLAYER);
assertThat(mNativeWrapper.getResultMessages()).contains(setStreamPath);
}
}