blob: 83525e9b880675e38180693a58165d6afc159bd0 [file] [log] [blame]
/*
* Copyright 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 android.hdmicec.cts;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import android.hdmicec.cts.HdmiCecConstants.CecDeviceType;
import android.hdmicec.cts.error.DumpsysParseException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import org.junit.Before;
import org.junit.rules.TestRule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Base class for all HDMI CEC CTS tests. */
@OptionClass(alias="hdmi-cec-client-cts-test")
public class BaseHdmiCecCtsTest extends BaseHostJUnit4Test {
public static final String PROPERTY_LOCALE = "persist.sys.locale";
private static final String POWER_CONTROL_MODE = "power_control_mode";
private static final String POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST =
"power_state_change_on_active_source_lost";
private static final String SET_MENU_LANGUAGE = "set_menu_language";
private static final String SET_MENU_LANGUAGE_ENABLED = "1";
/** Enum contains the list of possible address types. */
private enum AddressType {
DUMPSYS_AS_LOGICAL_ADDRESS("activeSourceLogicalAddress"),
DUMPSYS_PHYSICAL_ADDRESS("physicalAddress");
private String address;
public String getAddressType() {
return this.address;
}
private AddressType(String address) {
this.address = address;
}
}
public final HdmiCecClientWrapper hdmiCecClient;
public List<LogicalAddress> mDutLogicalAddresses = new ArrayList<>();
public @CecDeviceType int mTestDeviceType;
/**
* Constructor for BaseHdmiCecCtsTest.
*/
public BaseHdmiCecCtsTest() {
this(HdmiCecConstants.CEC_DEVICE_TYPE_UNKNOWN);
}
/**
* Constructor for BaseHdmiCecCtsTest.
*
* @param clientParams Extra parameters to use when launching cec-client
*/
public BaseHdmiCecCtsTest(String ...clientParams) {
this(HdmiCecConstants.CEC_DEVICE_TYPE_UNKNOWN, clientParams);
}
/**
* Constructor for BaseHdmiCecCtsTest.
*
* @param testDeviceType The primary test device type. This is used to determine to which
* logical address of the DUT messages should be sent.
* @param clientParams Extra parameters to use when launching cec-client
*/
public BaseHdmiCecCtsTest(@CecDeviceType int testDeviceType, String... clientParams) {
this.hdmiCecClient = new HdmiCecClientWrapper(clientParams);
mTestDeviceType = testDeviceType;
}
@Before
public void setUp() throws Exception {
setCec14();
mDutLogicalAddresses = getDumpsysLogicalAddresses();
hdmiCecClient.setTargetLogicalAddress(getTargetLogicalAddress());
boolean startAsTv = !hasDeviceType(HdmiCecConstants.CEC_DEVICE_TYPE_TV);
hdmiCecClient.init(startAsTv, getDevice());
}
/** Class with predefined rules which can be used by HDMI CEC CTS tests. */
public static class CecRules {
public static TestRule requiresCec(BaseHostJUnit4Test testPointer) {
return new RequiredFeatureRule(testPointer, HdmiCecConstants.HDMI_CEC_FEATURE);
}
public static TestRule requiresLeanback(BaseHostJUnit4Test testPointer) {
return new RequiredFeatureRule(testPointer, HdmiCecConstants.LEANBACK_FEATURE);
}
public static TestRule requiresDeviceType(
BaseHostJUnit4Test testPointer, @CecDeviceType int dutDeviceType) {
return RequiredPropertyRule.asCsvContainsValue(
testPointer,
HdmiCecConstants.HDMI_DEVICE_TYPE_PROPERTY,
Integer.toString(dutDeviceType));
}
/** This rule will skip the test if the DUT belongs to the HDMI device type deviceType. */
public static TestRule skipDeviceType(
BaseHostJUnit4Test testPointer, @CecDeviceType int deviceType) {
return RequiredPropertyRule.asCsvDoesNotContainsValue(
testPointer,
HdmiCecConstants.HDMI_DEVICE_TYPE_PROPERTY,
Integer.toString(deviceType));
}
}
@Option(name = HdmiCecConstants.PHYSICAL_ADDRESS_NAME,
description = "HDMI CEC physical address of the DUT",
mandatory = false)
public static int dutPhysicalAddress = HdmiCecConstants.DEFAULT_PHYSICAL_ADDRESS;
/** Gets the physical address of the DUT by parsing the dumpsys hdmi_control. */
public int getDumpsysPhysicalAddress() throws DumpsysParseException {
return getDumpsysPhysicalAddress(getDevice());
}
/** Gets the physical address of the specified device by parsing the dumpsys hdmi_control. */
public static int getDumpsysPhysicalAddress(ITestDevice device) throws DumpsysParseException {
return parseRequiredAddressFromDumpsys(device, AddressType.DUMPSYS_PHYSICAL_ADDRESS);
}
/** Gets the list of logical addresses of the DUT by parsing the dumpsys hdmi_control. */
public List<LogicalAddress> getDumpsysLogicalAddresses() throws DumpsysParseException {
return getDumpsysLogicalAddresses(getDevice());
}
/** Gets the list of logical addresses of the device by parsing the dumpsys hdmi_control. */
public static List<LogicalAddress> getDumpsysLogicalAddresses(ITestDevice device)
throws DumpsysParseException {
List<LogicalAddress> logicalAddressList = new ArrayList<>();
String line;
String pattern =
"(.*?)"
+ "(mDeviceInfo:)(.*)(logical_address: )"
+ "(?<"
+ "logicalAddress"
+ ">0x\\p{XDigit}{2})"
+ "(.*?)";
Pattern p = Pattern.compile(pattern);
try {
String dumpsys = device.executeShellCommand("dumpsys hdmi_control");
BufferedReader reader = new BufferedReader(new StringReader(dumpsys));
while ((line = reader.readLine()) != null) {
Matcher m = p.matcher(line);
if (m.matches()) {
int address = Integer.decode(m.group("logicalAddress"));
LogicalAddress logicalAddress = LogicalAddress.getLogicalAddress(address);
logicalAddressList.add(logicalAddress);
}
}
if (!logicalAddressList.isEmpty()) {
return logicalAddressList;
}
} catch (IOException | DeviceNotAvailableException e) {
throw new DumpsysParseException(
"Could not parse logicalAddress from dumpsys.", e);
}
throw new DumpsysParseException(
"Could not parse logicalAddress from dumpsys.");
}
/**
* Gets the system audio mode status of the device by parsing the dumpsys hdmi_control. Returns
* true when system audio mode is on and false when system audio mode is off
*/
public boolean isSystemAudioModeOn(ITestDevice device) throws DumpsysParseException {
List<LogicalAddress> logicalAddressList = new ArrayList<>();
String line;
String pattern =
"(.*?)"
+ "(mSystemAudioActivated: )"
+ "(?<"
+ "systemAudioModeStatus"
+ ">[true|false])"
+ "(.*?)";
Pattern p = Pattern.compile(pattern);
try {
String dumpsys = device.executeShellCommand("dumpsys hdmi_control");
BufferedReader reader = new BufferedReader(new StringReader(dumpsys));
while ((line = reader.readLine()) != null) {
Matcher m = p.matcher(line);
if (m.matches()) {
return m.group("systemAudioModeStatus").equals("true");
}
}
} catch (IOException | DeviceNotAvailableException e) {
throw new DumpsysParseException("Could not parse system audio mode from dumpsys.", e);
}
throw new DumpsysParseException("Could not parse system audio mode from dumpsys.");
}
/** Gets the DUT's logical address to which messages should be sent */
public LogicalAddress getTargetLogicalAddress() throws DumpsysParseException {
return getTargetLogicalAddress(getDevice(), mTestDeviceType);
}
/** Gets the given device's logical address to which messages should be sent */
public static LogicalAddress getTargetLogicalAddress(ITestDevice device) throws DumpsysParseException {
return getTargetLogicalAddress(device, HdmiCecConstants.CEC_DEVICE_TYPE_UNKNOWN);
}
/** Gets the given device's logical address to which messages should be sent, based on the test
* device type.
*
* When the test doesn't specify a device type, or the device doesn't have a logical address
* that matches the specified device type, use the first logical address.
*
*/
public static LogicalAddress getTargetLogicalAddress(ITestDevice device, int testDeviceType)
throws DumpsysParseException {
List<LogicalAddress> logicalAddressList = getDumpsysLogicalAddresses(device);
for (LogicalAddress address : logicalAddressList) {
if (address.getDeviceType() == testDeviceType) {
return address;
}
}
return logicalAddressList.get(0);
}
/**
* Parses the dumpsys hdmi_control to get the logical address of the current device registered
* as active source.
*/
public LogicalAddress getDumpsysActiveSourceLogicalAddress() throws DumpsysParseException {
ITestDevice device = getDevice();
int address =
parseRequiredAddressFromDumpsys(device, AddressType.DUMPSYS_AS_LOGICAL_ADDRESS);
return LogicalAddress.getLogicalAddress(address);
}
private static int parseRequiredAddressFromDumpsys(ITestDevice device, AddressType addressType)
throws DumpsysParseException {
Matcher m;
String line;
String pattern;
switch (addressType) {
case DUMPSYS_PHYSICAL_ADDRESS:
pattern =
"(.*?)"
+ "(physical_address: )"
+ "(?<"
+ addressType.getAddressType()
+ ">0x\\p{XDigit}{4})"
+ "(.*?)";
break;
case DUMPSYS_AS_LOGICAL_ADDRESS:
pattern =
"(.*?)"
+ "(mActiveSource: )"
+ "(\\(0x)"
+ "(?<"
+ addressType.getAddressType()
+ ">\\d+)"
+ "(, )"
+ "(0x)"
+ "(?<physicalAddress>\\d+)"
+ "(\\))"
+ "(.*?)";
break;
default:
throw new DumpsysParseException(
"Incorrect parameters", new IllegalArgumentException());
}
try {
Pattern p = Pattern.compile(pattern);
String dumpsys = device.executeShellCommand("dumpsys hdmi_control");
BufferedReader reader = new BufferedReader(new StringReader(dumpsys));
while ((line = reader.readLine()) != null) {
m = p.matcher(line);
if (m.matches()) {
int address = Integer.decode(m.group(addressType.getAddressType()));
return address;
}
}
} catch (IOException | DeviceNotAvailableException e) {
throw new DumpsysParseException(
"Could not parse " + addressType.getAddressType() + " from dumpsys.", e);
}
throw new DumpsysParseException(
"Could not parse " + addressType.getAddressType() + " from dumpsys.");
}
public boolean hasDeviceType(@CecDeviceType int deviceType) {
for (LogicalAddress address : mDutLogicalAddresses) {
if (address.getDeviceType() == deviceType) {
return true;
}
}
return false;
}
public boolean hasLogicalAddress(LogicalAddress address) {
return mDutLogicalAddresses.contains(address);
}
private static void setCecVersion(ITestDevice device, int cecVersion) throws Exception {
device.executeShellCommand("cmd hdmi_control cec_setting set hdmi_cec_version " +
cecVersion);
TimeUnit.SECONDS.sleep(HdmiCecConstants.TIMEOUT_CEC_REINIT_SECONDS);
}
/**
* Configures the device to use CEC 2.0. Skips the test if the device does not support CEC 2.0.
* @throws Exception
*/
public void setCec20() throws Exception {
setCecVersion(getDevice(), HdmiCecConstants.CEC_VERSION_2_0);
hdmiCecClient.sendCecMessage(hdmiCecClient.getSelfDevice(), CecOperand.GET_CEC_VERSION);
String reportCecVersion = hdmiCecClient.checkExpectedOutput(hdmiCecClient.getSelfDevice(),
CecOperand.CEC_VERSION);
boolean supportsCec2 = CecMessage.getParams(reportCecVersion)
>= HdmiCecConstants.CEC_VERSION_2_0;
// Device still reports a CEC version < 2.0.
assumeTrue(supportsCec2);
}
public void setCec14() throws Exception {
setCecVersion(getDevice(), HdmiCecConstants.CEC_VERSION_1_4);
}
public String getSystemLocale() throws Exception {
ITestDevice device = getDevice();
return device.executeShellCommand("getprop " + PROPERTY_LOCALE).trim();
}
public static String extractLanguage(String locale) {
return locale.split("[^a-zA-Z]")[0];
}
public void setSystemLocale(String locale) throws Exception {
ITestDevice device = getDevice();
device.executeShellCommand("setprop " + PROPERTY_LOCALE + " " + locale);
}
public boolean isLanguageEditable() throws Exception {
return getSettingsValue(SET_MENU_LANGUAGE).equals(SET_MENU_LANGUAGE_ENABLED);
}
public static String getSettingsValue(ITestDevice device, String setting) throws Exception {
return device.executeShellCommand("cmd hdmi_control cec_setting get " + setting)
.replace(setting + " = ", "").trim();
}
public String getSettingsValue(String setting) throws Exception {
return getSettingsValue(getDevice(), setting);
}
public static String setSettingsValue(ITestDevice device, String setting, String value)
throws Exception {
String val = getSettingsValue(device, setting);
device.executeShellCommand("cmd hdmi_control cec_setting set " + setting + " " +
value);
return val;
}
public String setSettingsValue(String setting, String value) throws Exception {
return setSettingsValue(getDevice(), setting, value);
}
public String getDeviceList() throws Exception {
return getDevice().executeShellCommand(
"dumpsys hdmi_control | sed -n '/mDeviceInfos/,/mCecController/{//!p;}'");
}
public void sendDeviceToSleepAndValidate() throws Exception {
sendDeviceToSleep();
assertDeviceWakefulness(HdmiCecConstants.WAKEFULNESS_ASLEEP);
}
public void waitForTransitionTo(int finalState) throws Exception {
int powerStatus;
int waitTimeSeconds = 0;
LogicalAddress cecClientDevice = hdmiCecClient.getSelfDevice();
int transitionState;
if (finalState == HdmiCecConstants.CEC_POWER_STATUS_STANDBY) {
transitionState = HdmiCecConstants.CEC_POWER_STATUS_IN_TRANSITION_TO_STANDBY;
} else if (finalState == HdmiCecConstants.CEC_POWER_STATUS_ON) {
transitionState = HdmiCecConstants.CEC_POWER_STATUS_IN_TRANSITION_TO_ON;
} else {
throw new Exception("Unsupported final power state!");
}
do {
TimeUnit.SECONDS.sleep(HdmiCecConstants.SLEEP_TIMESTEP_SECONDS);
waitTimeSeconds += HdmiCecConstants.SLEEP_TIMESTEP_SECONDS;
hdmiCecClient.sendCecMessage(cecClientDevice, CecOperand.GIVE_POWER_STATUS);
powerStatus =
CecMessage.getParams(
hdmiCecClient.checkExpectedOutput(
cecClientDevice, CecOperand.REPORT_POWER_STATUS));
if (powerStatus == finalState) {
return;
}
} while (powerStatus == transitionState
&& waitTimeSeconds <= HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
if (powerStatus != finalState) {
// Transition not complete even after wait, throw an Exception.
throw new Exception("Power status did not change to expected state.");
}
}
public void sendDeviceToSleepWithoutWait() throws Exception {
ITestDevice device = getDevice();
WakeLockHelper.acquirePartialWakeLock(device);
device.executeShellCommand("input keyevent KEYCODE_SLEEP");
}
public void sendDeviceToSleep() throws Exception {
sendDeviceToSleepWithoutWait();
assertDeviceWakefulness(HdmiCecConstants.WAKEFULNESS_ASLEEP);
waitForTransitionTo(HdmiCecConstants.CEC_POWER_STATUS_STANDBY);
}
public void sendDeviceToSleepAndValidateUsingStandbyMessage(boolean directlyAddressed)
throws Exception {
ITestDevice device = getDevice();
WakeLockHelper.acquirePartialWakeLock(device);
if (directlyAddressed) {
hdmiCecClient.sendCecMessage(LogicalAddress.TV, CecOperand.STANDBY);
} else {
hdmiCecClient.sendCecMessage(
LogicalAddress.TV, LogicalAddress.BROADCAST, CecOperand.STANDBY);
}
waitForTransitionTo(HdmiCecConstants.CEC_POWER_STATUS_STANDBY);
}
public void wakeUpDevice() throws Exception {
ITestDevice device = getDevice();
device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
assertDeviceWakefulness(HdmiCecConstants.WAKEFULNESS_AWAKE);
waitForTransitionTo(HdmiCecConstants.CEC_POWER_STATUS_ON);
WakeLockHelper.releasePartialWakeLock(device);
}
public void wakeUpDeviceWithoutWait() throws Exception {
ITestDevice device = getDevice();
device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
assertDeviceWakefulness(HdmiCecConstants.WAKEFULNESS_AWAKE);
WakeLockHelper.releasePartialWakeLock(device);
}
public void checkStandbyAndWakeUp() throws Exception {
assertDeviceWakefulness(HdmiCecConstants.WAKEFULNESS_ASLEEP);
wakeUpDevice();
}
public void assertDeviceWakefulness(String wakefulness) throws Exception {
ITestDevice device = getDevice();
String actualWakefulness;
int waitTimeSeconds = 0;
do {
TimeUnit.SECONDS.sleep(HdmiCecConstants.SLEEP_TIMESTEP_SECONDS);
waitTimeSeconds += HdmiCecConstants.SLEEP_TIMESTEP_SECONDS;
actualWakefulness =
device.executeShellCommand("dumpsys power | grep mWakefulness=")
.trim().replace("mWakefulness=", "");
} while (!actualWakefulness.equals(wakefulness)
&& waitTimeSeconds <= HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
assertWithMessage(
"Device wakefulness is "
+ actualWakefulness
+ " but expected to be "
+ wakefulness)
.that(actualWakefulness)
.isEqualTo(wakefulness);
}
/**
* Checks a given condition once every {@link HdmiCecConstants.SLEEP_TIMESTEP_SECONDS} seconds
* until it is true, or {@link HdmiCecConstants.MAX_SLEEP_TIME_SECONDS} seconds have passed.
* Triggers an assertion failure if the condition remains false after the time limit.
* @param condition Callable that returns whether the condition is met
* @param errorMessage The message to print if the condition is false
*/
public void waitForCondition(Callable<Boolean> condition, String errorMessage)
throws Exception {
int waitTimeSeconds = 0;
boolean conditionState;
do {
TimeUnit.SECONDS.sleep(HdmiCecConstants.SLEEP_TIMESTEP_SECONDS);
waitTimeSeconds += HdmiCecConstants.SLEEP_TIMESTEP_SECONDS;
conditionState = condition.call();
} while (!conditionState && waitTimeSeconds <= HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
assertWithMessage(errorMessage).that(conditionState).isTrue();
}
public void sendOtp() throws Exception {
ITestDevice device = getDevice();
device.executeShellCommand("cmd hdmi_control onetouchplay");
}
public String setPowerControlMode(String valToSet) throws Exception {
return setSettingsValue(POWER_CONTROL_MODE, valToSet);
}
public String setPowerStateChangeOnActiveSourceLost(String valToSet) throws Exception {
return setSettingsValue(POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, valToSet);
}
public boolean isDeviceActiveSource(ITestDevice device) throws DumpsysParseException {
final String activeSource = "activeSource";
final String pattern =
"(.*?)"
+ "(isActiveSource\\(\\): )"
+ "(?<"
+ activeSource
+ ">\\btrue\\b|\\bfalse\\b)"
+ "(.*?)";
try {
Pattern p = Pattern.compile(pattern);
String dumpsys = device.executeShellCommand("dumpsys hdmi_control");
BufferedReader reader = new BufferedReader(new StringReader(dumpsys));
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = p.matcher(line);
if (matcher.matches()) {
return matcher.group(activeSource).equals("true");
}
}
} catch (IOException | DeviceNotAvailableException e) {
throw new DumpsysParseException("Could not fetch 'dumpsys hdmi_control' output.", e);
}
throw new DumpsysParseException("Could not parse isActiveSource() from dumpsys.");
}
/**
* For source devices, simulate that a sink is connected by responding to the
* {@code Give Power Status} message that is sent when re-enabling CEC.
* Validate that HdmiControlService#mIsCecAvailable is set to true as a result.
*/
public void simulateCecSinkConnected(ITestDevice device, LogicalAddress source)
throws Exception {
hdmiCecClient.clearClientOutput();
device.executeShellCommand("cmd hdmi_control cec_setting set hdmi_cec_enabled 0");
waitForCondition(() -> !isCecAvailable(device), "Could not disable CEC");
device.executeShellCommand("cmd hdmi_control cec_setting set hdmi_cec_enabled 1");
// When a CEC device has just become available, the CEC adapter isn't able to send it
// messages right away. Therefore we let the first <Give Power Status> message time-out, and
// only respond to the retry.
hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
hdmiCecClient.clearClientOutput();
hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
hdmiCecClient.sendCecMessage(LogicalAddress.TV, source, CecOperand.REPORT_POWER_STATUS,
CecMessage.formatParams(HdmiCecConstants.CEC_POWER_STATUS_STANDBY));
waitForCondition(() -> isCecAvailable(device),
"Simulating that a sink is connected, failed.");
}
boolean isCecAvailable(ITestDevice device) throws Exception {
return device.executeShellCommand("dumpsys hdmi_control | grep mIsCecAvailable:")
.replace("mIsCecAvailable:", "").trim().equals("true");
}
/**
* Returns whether an audio output device is using full volume behavior by checking if it is in
* the "mFullVolumeDevices" line in audio dumpsys. Example: "mFullVolumeDevices=0x400,0x40001".
*/
public boolean isFullVolumeDevice(int audioOutputDevice) throws Exception {
String[] splitLine = getDevice().executeShellCommand(
"dumpsys audio | grep mFullVolumeDevices").split("=");
if (splitLine.length < 2) {
// No full volume devices
return false;
}
String[] deviceStrings = splitLine[1].trim().split(",");
for (String deviceString : deviceStrings) {
try {
if (Integer.decode(deviceString) == audioOutputDevice) {
return true;
}
} catch (NumberFormatException e) {
// Ignore this device and continue
}
}
return false;
}
}