1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.hdmicec.cts;
18 
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
21 import com.android.tradefed.util.RunUtil;
22 
23 import java.io.BufferedReader;
24 import java.io.BufferedWriter;
25 import java.io.InputStreamReader;
26 import java.io.OutputStreamWriter;
27 import java.util.concurrent.TimeUnit;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.List;
31 import java.util.regex.Pattern;
32 
33 import org.junit.rules.ExternalResource;
34 
35 /** Class that helps communicate with the cec-client */
36 public final class HdmiCecClientWrapper extends ExternalResource {
37 
38     private static final String CEC_CONSOLE_READY = "waiting for input";
39     private static final int MILLISECONDS_TO_READY = 10000;
40     private static final int DEFAULT_TIMEOUT = 20000;
41     private static final int BUFFER_SIZE = 1024;
42 
43     private Process mCecClient;
44     private BufferedWriter mOutputConsole;
45     private BufferedReader mInputConsole;
46     private boolean mCecClientInitialised = false;
47 
48     private LogicalAddress targetDevice;
49     private String clientParams[];
50 
HdmiCecClientWrapper(LogicalAddress targetDevice, String ...clientParams)51     public HdmiCecClientWrapper(LogicalAddress targetDevice, String ...clientParams) {
52         this.targetDevice = targetDevice;
53         this.clientParams = clientParams;
54     }
55 
56     @Override
before()57     protected void before() throws Throwable {
58         this.init();
59     };
60 
61     @Override
after()62     protected void after() {
63         this.killCecProcess();
64     };
65 
66     /** Initialise the client */
init()67     private void init() throws Exception {
68         List<String> commands = new ArrayList();
69 
70         commands.add("cec-client");
71         /* "-p 2" starts the client as if it is connected to HDMI port 2, taking the physical
72          * address 2.0.0.0 */
73         commands.add("-p");
74         commands.add("2");
75         /* "-t x" starts the client as a TV device */
76         commands.add("-t");
77         commands.add("x");
78         commands.addAll(Arrays.asList(clientParams));
79 
80         mCecClient = RunUtil.getDefault().runCmdInBackground(commands);
81         mInputConsole = new BufferedReader(new InputStreamReader(mCecClient.getInputStream()));
82 
83         /* Wait for the client to become ready */
84         mCecClientInitialised = true;
85         if (checkConsoleOutput(CecClientMessage.CLIENT_CONSOLE_READY + "", MILLISECONDS_TO_READY)) {
86             mOutputConsole = new BufferedWriter(
87                                 new OutputStreamWriter(mCecClient.getOutputStream()), BUFFER_SIZE);
88             return;
89         }
90 
91         mCecClientInitialised = false;
92 
93         throw (new Exception("Could not initialise cec-client process"));
94     }
95 
checkCecClient()96     private void checkCecClient() throws Exception {
97         if (!mCecClientInitialised) {
98             throw new Exception("cec-client not initialised!");
99         }
100         if (!mCecClient.isAlive()) {
101             throw new Exception("cec-client not running!");
102         }
103     }
104 
105     /**
106      * Sends a CEC message with source marked as broadcast to the device passed in the constructor
107      * through the output console of the cec-communication channel.
108      */
sendCecMessage(CecOperand message)109     public void sendCecMessage(CecOperand message) throws Exception {
110         sendCecMessage(LogicalAddress.BROADCAST, targetDevice, message, "");
111     }
112 
113     /**
114      * Sends a CEC message from source device to the device passed in the constructor through the
115      * output console of the cec-communication channel.
116      */
sendCecMessage(LogicalAddress source, CecOperand message)117     public void sendCecMessage(LogicalAddress source, CecOperand message) throws Exception {
118         sendCecMessage(source, targetDevice, message, "");
119     }
120 
121     /**
122      * Sends a CEC message from source device to a destination device through the output console of
123      * the cec-communication channel.
124      */
sendCecMessage(LogicalAddress source, LogicalAddress destination, CecOperand message)125     public void sendCecMessage(LogicalAddress source, LogicalAddress destination,
126         CecOperand message) throws Exception {
127         sendCecMessage(source, destination, message, "");
128     }
129 
130     /**
131      * Sends a CEC message from source device to a destination device through the output console of
132      * the cec-communication channel with the appended params.
133      */
sendCecMessage(LogicalAddress source, LogicalAddress destination, CecOperand message, String params)134     public void sendCecMessage(LogicalAddress source, LogicalAddress destination,
135             CecOperand message, String params) throws Exception {
136         checkCecClient();
137         mOutputConsole.write("tx " + source + destination + ":" + message + params);
138         mOutputConsole.newLine();
139         mOutputConsole.flush();
140     }
141 
142     /**
143      * Sends a <USER_CONTROL_PRESSED> and <USER_CONTROL_RELEASED> from source to destination
144      * through the output console of the cec-communication channel with the mentioned keycode.
145      */
sendUserControlPressAndRelease(LogicalAddress source, LogicalAddress destination, int keycode, boolean holdKey)146     public void sendUserControlPressAndRelease(LogicalAddress source, LogicalAddress destination,
147             int keycode, boolean holdKey) throws Exception {
148         sendUserControlPress(source, destination, keycode, holdKey);
149         /* Sleep less than 200ms between press and release */
150         TimeUnit.MILLISECONDS.sleep(100);
151         mOutputConsole.write("tx " + source + destination + ":" +
152                               CecOperand.USER_CONTROL_RELEASED);
153         mOutputConsole.flush();
154     }
155 
156     /**
157      * Sends a <UCP> message from source to destination through the output console of the
158      * cec-communication channel with the mentioned keycode. If holdKey is true, the method will
159      * send multiple <UCP> messages to simulate a long press. No <UCR> will be sent.
160      */
sendUserControlPress(LogicalAddress source, LogicalAddress destination, int keycode, boolean holdKey)161     public void sendUserControlPress(LogicalAddress source, LogicalAddress destination,
162             int keycode, boolean holdKey) throws Exception {
163         String key = String.format("%02x", keycode);
164         String command = "tx " + source + destination + ":" +
165                 CecOperand.USER_CONTROL_PRESSED + ":" + key;
166 
167         if (holdKey) {
168             /* Repeat once between 200ms and 450ms for at least 5 seconds. Since message will be
169              * sent once later, send 16 times in loop every 300ms. */
170             int repeat = 16;
171             for (int i = 0; i < repeat; i++) {
172                 mOutputConsole.write(command);
173                 mOutputConsole.newLine();
174                 mOutputConsole.flush();
175                 TimeUnit.MILLISECONDS.sleep(300);
176             }
177         }
178 
179         mOutputConsole.write(command);
180         mOutputConsole.newLine();
181         mOutputConsole.flush();
182     }
183 
184     /**
185      * Sends a series of <UCP> [firstKeycode] from source to destination through the output console
186      * of the cec-communication channel immediately followed by <UCP> [secondKeycode]. No <UCR>
187      *  message is sent.
188      */
sendUserControlInterruptedPressAndHold( LogicalAddress source, LogicalAddress destination, int firstKeycode, int secondKeycode, boolean holdKey)189     public void sendUserControlInterruptedPressAndHold(
190         LogicalAddress source, LogicalAddress destination,
191             int firstKeycode, int secondKeycode, boolean holdKey) throws Exception {
192         sendUserControlPress(source, destination, firstKeycode, holdKey);
193         /* Sleep less than 200ms between press and release */
194         TimeUnit.MILLISECONDS.sleep(100);
195         sendUserControlPress(source, destination, secondKeycode, false);
196     }
197 
198     /** Sends a message to the output console of the cec-client */
sendConsoleMessage(String message)199     public void sendConsoleMessage(String message) throws Exception {
200         checkCecClient();
201         CLog.v("Sending message:: " + message);
202         mOutputConsole.write(message);
203         mOutputConsole.flush();
204     }
205 
206     /** Check for any string on the input console of the cec-client, uses default timeout */
checkConsoleOutput(String expectedMessage)207     public boolean checkConsoleOutput(String expectedMessage) throws Exception {
208         return checkConsoleOutput(expectedMessage, DEFAULT_TIMEOUT);
209     }
210 
211     /** Check for any string on the input console of the cec-client */
checkConsoleOutput(String expectedMessage, long timeoutMillis)212     public boolean checkConsoleOutput(String expectedMessage,
213                                        long timeoutMillis) throws Exception {
214         checkCecClient();
215         long startTime = System.currentTimeMillis();
216         long endTime = startTime;
217 
218         while ((endTime - startTime <= timeoutMillis)) {
219             if (mInputConsole.ready()) {
220                 String line = mInputConsole.readLine();
221                 if (line.contains(expectedMessage)) {
222                     CLog.v("Found " + expectedMessage + " in " + line);
223                     return true;
224                 }
225             }
226             endTime = System.currentTimeMillis();
227         }
228         return false;
229     }
230 
231     /** Gets all the messages received from the given source device during a period of duration
232      * seconds.
233      */
getAllMessages(LogicalAddress source, int duration)234     public List<CecOperand> getAllMessages(LogicalAddress source, int duration) throws Exception {
235         List<CecOperand> receivedOperands = new ArrayList<>();
236         long startTime = System.currentTimeMillis();
237         long endTime = startTime;
238         Pattern pattern = Pattern.compile("(.*>>)(.*?)" +
239                 "(" + source + "\\p{XDigit}):(.*)",
240             Pattern.CASE_INSENSITIVE);
241 
242         while ((endTime - startTime <= duration)) {
243             if (mInputConsole.ready()) {
244                 String line = mInputConsole.readLine();
245                 if (pattern.matcher(line).matches()) {
246                     CecOperand operand = CecMessage.getOperand(line);
247                     if (!receivedOperands.contains(operand)) {
248                         receivedOperands.add(operand);
249                     }
250                 }
251             }
252             endTime = System.currentTimeMillis();
253         }
254         return receivedOperands;
255     }
256 
257 
258     /**
259      * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and
260      * returns the first line that contains that message within default timeout. If the CEC message
261      * is not found within the timeout, an exception is thrown.
262      */
checkExpectedOutput(CecOperand expectedMessage)263     public String checkExpectedOutput(CecOperand expectedMessage) throws Exception {
264         return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, DEFAULT_TIMEOUT);
265     }
266 
267     /**
268      * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client
269      * communication channel and returns the first line that contains that message within
270      * default timeout. If the CEC message is not found within the timeout, an exception is thrown.
271      */
checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage)272     public String checkExpectedOutput(LogicalAddress toDevice,
273                                       CecOperand expectedMessage) throws Exception {
274         return checkExpectedOutput(toDevice, expectedMessage, DEFAULT_TIMEOUT);
275     }
276 
277     /**
278      * Looks for the CEC expectedMessage broadcast on the cec-client communication channel and
279      * returns the first line that contains that message within timeoutMillis. If the CEC message
280      * is not found within the timeout, an exception is thrown.
281      */
checkExpectedOutput(CecOperand expectedMessage, long timeoutMillis)282     public String checkExpectedOutput(CecOperand expectedMessage,
283                                       long timeoutMillis) throws Exception {
284         return checkExpectedOutput(LogicalAddress.BROADCAST, expectedMessage, timeoutMillis);
285     }
286 
287     /**
288      * Looks for the CEC expectedMessage sent to CEC device toDevice on the cec-client
289      * communication channel and returns the first line that contains that message within
290      * timeoutMillis. If the CEC message is not found within the timeout, an exception is thrown.
291      */
checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage, long timeoutMillis)292     public String checkExpectedOutput(LogicalAddress toDevice, CecOperand expectedMessage,
293                                        long timeoutMillis) throws Exception {
294         checkCecClient();
295         long startTime = System.currentTimeMillis();
296         long endTime = startTime;
297         Pattern pattern = Pattern.compile("(.*>>)(.*?)" +
298                                           "(" + targetDevice + toDevice + "):" +
299                                           "(" + expectedMessage + ")(.*)",
300                                           Pattern.CASE_INSENSITIVE);
301 
302         while ((endTime - startTime <= timeoutMillis)) {
303             if (mInputConsole.ready()) {
304                 String line = mInputConsole.readLine();
305                 if (pattern.matcher(line).matches()) {
306                     CLog.v("Found " + expectedMessage.name() + " in " + line);
307                     return line;
308                 }
309             }
310             endTime = System.currentTimeMillis();
311         }
312         throw new Exception("Could not find message " + expectedMessage.name());
313     }
314 
315     /**
316      * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client
317      * communication channel and throws an exception if it finds the line that contains the message
318      * within the default timeout. If the CEC message is not found within the timeout, function
319      * returns without error.
320      */
checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage)321     public void checkOutputDoesNotContainMessage(LogicalAddress toDevice,
322             CecOperand incorrectMessage) throws Exception {
323         checkOutputDoesNotContainMessage(toDevice, incorrectMessage, DEFAULT_TIMEOUT);
324      }
325 
326     /**
327      * Looks for the CEC message incorrectMessage sent to CEC device toDevice on the cec-client
328      * communication channel and throws an exception if it finds the line that contains the message
329      * within timeoutMillis. If the CEC message is not found within the timeout, function returns
330      * without error.
331      */
checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage, long timeoutMillis)332     public void checkOutputDoesNotContainMessage(LogicalAddress toDevice, CecOperand incorrectMessage,
333             long timeoutMillis) throws Exception {
334 
335         checkCecClient();
336         long startTime = System.currentTimeMillis();
337         long endTime = startTime;
338         Pattern pattern = Pattern.compile("(.*>>)(.*?)" +
339                                           "(" + targetDevice + toDevice + "):" +
340                                           "(" + incorrectMessage + ")(.*)",
341                                           Pattern.CASE_INSENSITIVE);
342 
343         while ((endTime - startTime <= timeoutMillis)) {
344             if (mInputConsole.ready()) {
345                 String line = mInputConsole.readLine();
346                 if (pattern.matcher(line).matches()) {
347                     CLog.v("Found " + incorrectMessage.name() + " in " + line);
348                     throw new Exception("Found " + incorrectMessage.name() + " to " + toDevice +
349                             " with params " + CecMessage.getParamsAsString(line));
350                 }
351             }
352             endTime = System.currentTimeMillis();
353         }
354      }
355 
356     /**
357      * Kills the cec-client process that was created in init().
358      */
killCecProcess()359     private void killCecProcess() {
360         try {
361             checkCecClient();
362             sendConsoleMessage(CecClientMessage.QUIT_CLIENT.toString());
363             mOutputConsole.close();
364             mInputConsole.close();
365             mCecClientInitialised = false;
366             if (!mCecClient.waitFor(MILLISECONDS_TO_READY, TimeUnit.MILLISECONDS)) {
367                 /* Use a pkill cec-client if the cec-client process is not dead in spite of the
368                  * quit above.
369                  */
370                 List<String> commands = new ArrayList<>();
371                 Process killProcess;
372                 commands.add("pkill");
373                 commands.add("cec-client");
374                 killProcess = RunUtil.getDefault().runCmdInBackground(commands);
375                 killProcess.waitFor();
376             }
377         } catch (Exception e) {
378             /* If cec-client is not running, do not throw an exception, just return. */
379             CLog.w("Unable to close cec-client", e);
380         }
381     }
382 }
383