| /* |
| * Copyright (C) 2019 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 com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.util.RunUtil; |
| |
| import org.junit.rules.ExternalResource; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.util.concurrent.TimeUnit; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Pattern; |
| |
| /** Class that helps communicate with the cec-client */ |
| public final class HdmiCecClientWrapper extends ExternalResource { |
| |
| private static final String CEC_CONSOLE_READY = "waiting for input"; |
| private static final int MILLISECONDS_TO_READY = 10000; |
| private static final int DEFAULT_TIMEOUT = 20000; |
| private static final int BUFFER_SIZE = 1024; |
| |
| private Process mCecClient; |
| private BufferedWriter mOutputConsole; |
| private BufferedReader mInputConsole; |
| private boolean mCecClientInitialised = false; |
| |
| private LogicalAddress selfDevice = LogicalAddress.RECORDER_1; |
| private LogicalAddress targetDevice = LogicalAddress.UNKNOWN; |
| private String clientParams[]; |
| |
| public HdmiCecClientWrapper(String ...clientParams) { |
| this.clientParams = clientParams; |
| } |
| |
| @Override |
| protected void after() { |
| this.killCecProcess(); |
| }; |
| |
| |
| void setTargetLogicalAddress(LogicalAddress dutLogicalAddress) { |
| targetDevice = dutLogicalAddress; |
| } |
| |
| /** Initialise the client */ |
| void init(boolean startAsTv) throws Exception { |
| if (targetDevice == LogicalAddress.UNKNOWN) { |
| throw new IllegalStateException("Missing logical address of the target device."); |
| } |
| |
| List<String> commands = new ArrayList(); |
| |
| commands.add("cec-client"); |
| /* "-p 2" starts the client as if it is connected to HDMI port 2, taking the physical |
| * address 2.0.0.0 */ |
| commands.add("-p"); |
| commands.add("2"); |
| if (startAsTv) { |
| commands.add("-t"); |
| commands.add("-x"); |
| selfDevice = LogicalAddress.TV; |
| } |
| commands.addAll(Arrays.asList(clientParams)); |
| |
| mCecClient = RunUtil.getDefault().runCmdInBackground(commands); |
| mInputConsole = new BufferedReader(new InputStreamReader(mCecClient.getInputStream())); |
| |
| /* Wait for the client to become ready */ |
| mCecClientInitialised = true; |
| if (checkConsoleOutput(CecClientMessage.CLIENT_CONSOLE_READY + "", MILLISECONDS_TO_READY)) { |
| mOutputConsole = new BufferedWriter( |
| new OutputStreamWriter(mCecClient.getOutputStream()), BUFFER_SIZE); |
| return; |
| } |
| |
| mCecClientInitialised = false; |
| |
| throw (new Exception("Could not initialise cec-client process")); |
| } |
| |
| private void checkCecClient() throws Exception { |
| if (!mCecClientInitialised) { |
| throw new Exception("cec-client not initialised!"); |
| } |
| if (!mCecClient.isAlive()) { |
| throw new Exception("cec-client not running!"); |
| } |
| } |
| |
| /** |
| * Sends a CEC message with source marked as broadcast to the device passed in the constructor |
| * through the output console of the cec-communication channel. |
| */ |
| public void sendCecMessage(CecOperand message) throws Exception { |
| sendCecMessage(LogicalAddress.BROADCAST, targetDevice, message, ""); |
| } |
| |
| /** |
| * Sends a CEC message from source device to the device passed in the constructor through the |
| * output console of the cec-communication channel. |
| */ |
| public void sendCecMessage(LogicalAddress source, CecOperand message) throws Exception { |
| sendCecMessage(source, targetDevice, message, ""); |
| } |
| |
| /** |
| * Sends a CEC message from source device to a destination device through the output console of |
| * the cec-communication channel. |
| */ |
| public void sendCecMessage(LogicalAddress source, LogicalAddress destination, |
| CecOperand message) throws Exception { |
| sendCecMessage(source, destination, message, ""); |
| } |
| |
| /** |
| * Sends a CEC message from source device to a destination device through the output console of |
| * the cec-communication channel with the appended params. |
| */ |
| public void sendCecMessage(LogicalAddress source, LogicalAddress destination, |
| CecOperand message, String params) throws Exception { |
| checkCecClient(); |
| String sendMessageString = "tx " + source + destination + ":" + message + params; |
| CLog.e("Sending message: " + sendMessageString); |
| mOutputConsole.write(sendMessageString); |
| mOutputConsole.newLine(); |
| mOutputConsole.flush(); |
| } |
| |
| /** |
| * Sends a <USER_CONTROL_PRESSED> and <USER_CONTROL_RELEASED> from source to destination |
| * through the output console of the cec-communication channel with the mentioned keycode. |
| */ |
| public void sendUserControlPressAndRelease(LogicalAddress source, LogicalAddress destination, |
| int keycode, boolean holdKey) throws Exception { |
| sendUserControlPress(source, destination, keycode, holdKey); |
| /* Sleep less than 200ms between press and release */ |
| TimeUnit.MILLISECONDS.sleep(100); |
| mOutputConsole.write("tx " + source + destination + ":" + |
| CecOperand.USER_CONTROL_RELEASED); |
| mOutputConsole.flush(); |
| } |
| |
| /** |
| * Sends a <UCP> message from source to destination through the output console of the |
| * cec-communication channel with the mentioned keycode. If holdKey is true, the method will |
| * send multiple <UCP> messages to simulate a long press. No <UCR> will be sent. |
| */ |
| public void sendUserControlPress(LogicalAddress source, LogicalAddress destination, |
| int keycode, boolean holdKey) throws Exception { |
| String key = String.format("%02x", keycode); |
| String command = "tx " + source + destination + ":" + |
| CecOperand.USER_CONTROL_PRESSED + ":" + key; |
| |
| if (holdKey) { |
| /* Repeat once between 200ms and 450ms for at least 5 seconds. Since message will be |
| * sent once later, send 16 times in loop every 300ms. */ |
| int repeat = 16; |
| for (int i = 0; i < repeat; i++) { |
| mOutputConsole.write(command); |
| mOutputConsole.newLine(); |
| mOutputConsole.flush(); |
| TimeUnit.MILLISECONDS.sleep(300); |
| } |
| } |
| |
| mOutputConsole.write(command); |
| mOutputConsole.newLine(); |
| mOutputConsole.flush(); |
| } |
| |
| /** |
| * Sends a series of <UCP> [firstKeycode] from source to destination through the output console |
| * of the cec-communication channel immediately followed by <UCP> [secondKeycode]. No <UCR> |
| * message is sent. |
| */ |
| public void sendUserControlInterruptedPressAndHold( |
| LogicalAddress source, LogicalAddress destination, |
| int firstKeycode, int secondKeycode, boolean holdKey) throws Exception { |
| sendUserControlPress(source, destination, firstKeycode, holdKey); |
| /* Sleep less than 200ms between press and release */ |
| TimeUnit.MILLISECONDS.sleep(100); |
| sendUserControlPress(source, destination, secondKeycode, false); |
| } |
| |
| /** Sends a message to the output console of the cec-client */ |
| public void sendConsoleMessage(String message) throws Exception { |
| checkCecClient(); |
| CLog.v("Sending message:: " + message); |
| mOutputConsole.write(message); |
| mOutputConsole.flush(); |
| } |
| |
| /** Check for any string on the input console of the cec-client, uses default timeout */ |
| public boolean checkConsoleOutput(String expectedMessage) throws Exception { |
| return checkConsoleOutput(expectedMessage, DEFAULT_TIMEOUT); |
| } |
| |
| /** Check for any string on the input console of the cec-client */ |
| public boolean checkConsoleOutput(String expectedMessage, |
| long timeoutMillis) throws Exception { |
| checkCecClient(); |
| long startTime = System.currentTimeMillis(); |
| long endTime = startTime; |
| |
| while ((endTime - startTime <= timeoutMillis)) { |
| if (mInputConsole.ready()) { |
| String line = mInputConsole.readLine(); |
| if (line.contains(expectedMessage)) { |
| CLog.v("Found " + expectedMessage + " in " + line); |
| return true; |
| } |
| } |
| endTime = System.currentTimeMillis(); |
| } |
| return false; |
| } |
| |
| /** Gets all the messages received from the given source device during a period of duration |
| * seconds. |
| */ |
| public List<CecOperand> getAllMessages(LogicalAddress source, int duration) throws Exception { |
| List<CecOperand> receivedOperands = new ArrayList<>(); |
| long startTime = System.currentTimeMillis(); |
| long endTime = startTime; |
| Pattern pattern = Pattern.compile("(.*>>)(.*?)" + |
| "(" + source + "\\p{XDigit}):(.*)", |
| Pattern.CASE_INSENSITIVE); |
| |
| while ((endTime - startTime <= duration)) { |
| if (mInputConsole.ready()) { |
| String line = mInputConsole.readLine(); |
| if (pattern.matcher(line).matches()) { |
| CecOperand operand = CecMessage.getOperand(line); |
| if (!receivedOperands.contains(operand)) { |
| receivedOperands.add(operand); |
| } |
| } |
| } |
| endTime = System.currentTimeMillis(); |
| } |
| return receivedOperands; |
| } |
| |
| |
| /** |
| * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and |
| * returns the first line that contains that message within default timeout. If the CEC message |
| * is not found within the timeout, an exception is thrown. |
| */ |
| public String checkExpectedOutput(CecOperand expectedMessage) throws Exception { |
| return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, DEFAULT_TIMEOUT); |
| } |
| |
| /** |
| * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client |
| * communication channel and returns the first line that contains that message within |
| * default timeout. If the CEC message is not found within the timeout, an exception is thrown. |
| */ |
| public String checkExpectedOutput(LogicalAddress toDevice, |
| CecOperand expectedMessage) throws Exception { |
| return checkExpectedOutput(toDevice, expectedMessage, DEFAULT_TIMEOUT); |
| } |
| |
| /** |
| * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and |
| * returns the first line that contains that message within timeoutMillis. If the CEC message |
| * is not found within the timeout, an exception is thrown. |
| */ |
| public String checkExpectedOutput(CecOperand expectedMessage, |
| long timeoutMillis) throws Exception { |
| return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, timeoutMillis); |
| } |
| |
| /** |
| * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client |
| * communication channel and returns the first line that contains that message within |
| * timeoutMillis. If the CEC message is not found within the timeout, an exception is thrown. |
| */ |
| public String checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage, |
| long timeoutMillis) throws Exception { |
| checkCecClient(); |
| long startTime = System.currentTimeMillis(); |
| long endTime = startTime; |
| Pattern pattern = Pattern.compile("(.*>>)(.*?)" + |
| "(" + targetDevice + toDevice + "):" + |
| "(" + expectedMessage + ")(.*)", |
| Pattern.CASE_INSENSITIVE); |
| |
| while ((endTime - startTime <= timeoutMillis)) { |
| if (mInputConsole.ready()) { |
| String line = mInputConsole.readLine(); |
| if (pattern.matcher(line).matches()) { |
| CLog.v("Found " + expectedMessage.name() + " in " + line); |
| return line; |
| } |
| } |
| endTime = System.currentTimeMillis(); |
| } |
| throw new Exception("Could not find message " + expectedMessage.name()); |
| } |
| |
| /** |
| * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client |
| * communication channel and throws an exception if it finds the line that contains the message |
| * within the default timeout. If the CEC message is not found within the timeout, function |
| * returns without error. |
| */ |
| public void checkOutputDoesNotContainMessage(LogicalAddress toDevice, |
| CecOperand incorrectMessage) throws Exception { |
| checkOutputDoesNotContainMessage(toDevice, incorrectMessage, DEFAULT_TIMEOUT); |
| } |
| |
| /** |
| * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client |
| * communication channel and throws an exception if it finds the line that contains the message |
| * within timeoutMillis. If the CEC message is not found within the timeout, function returns |
| * without error. |
| */ |
| public void checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage, |
| long timeoutMillis) throws Exception { |
| |
| checkCecClient(); |
| long startTime = System.currentTimeMillis(); |
| long endTime = startTime; |
| Pattern pattern = Pattern.compile("(.*>>)(.*?)" + |
| "(" + targetDevice + toDevice + "):" + |
| "(" + incorrectMessage + ")(.*)", |
| Pattern.CASE_INSENSITIVE); |
| |
| while ((endTime - startTime <= timeoutMillis)) { |
| if (mInputConsole.ready()) { |
| String line = mInputConsole.readLine(); |
| if (pattern.matcher(line).matches()) { |
| CLog.v("Found " + incorrectMessage.name() + " in " + line); |
| throw new Exception("Found " + incorrectMessage.name() + " to " + toDevice + |
| " with params " + CecMessage.getParamsAsString(line)); |
| } |
| } |
| endTime = System.currentTimeMillis(); |
| } |
| } |
| |
| /** Returns the device type that the cec-client has started as. */ |
| public LogicalAddress getSelfDevice() { |
| return selfDevice; |
| } |
| |
| /** |
| * Kills the cec-client process that was created in init(). |
| */ |
| private void killCecProcess() { |
| try { |
| checkCecClient(); |
| sendConsoleMessage(CecClientMessage.QUIT_CLIENT.toString()); |
| mOutputConsole.close(); |
| mInputConsole.close(); |
| mCecClientInitialised = false; |
| if (!mCecClient.waitFor(MILLISECONDS_TO_READY, TimeUnit.MILLISECONDS)) { |
| /* Use a pkill cec-client if the cec-client process is not dead in spite of the |
| * quit above. |
| */ |
| List<String> commands = new ArrayList<>(); |
| Process killProcess; |
| commands.add("pkill"); |
| commands.add("cec-client"); |
| killProcess = RunUtil.getDefault().runCmdInBackground(commands); |
| killProcess.waitFor(); |
| } |
| } catch (Exception e) { |
| /* If cec-client is not running, do not throw an exception, just return. */ |
| CLog.w("Unable to close cec-client", e); |
| } |
| } |
| } |