1 /*
2  * Copyright (C) 2020 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.targetprep;
18 
19 import android.hdmicec.cts.BaseHdmiCecCtsTest;
20 import android.hdmicec.cts.CecClientMessage;
21 import android.hdmicec.cts.CecMessage;
22 import android.hdmicec.cts.HdmiCecClientWrapper;
23 import android.hdmicec.cts.HdmiCecConstants;
24 import android.hdmicec.cts.LogicalAddress;
25 import android.hdmicec.cts.error.CecClientWrapperException;
26 import android.hdmicec.cts.error.ErrorCodes;
27 
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.invoker.TestInformation;
31 import com.android.tradefed.log.LogUtil.CLog;
32 import com.android.tradefed.targetprep.BaseTargetPreparer;
33 import com.android.tradefed.targetprep.TargetSetupError;
34 import com.android.tradefed.util.RunUtil;
35 
36 import java.io.BufferedReader;
37 import java.io.BufferedWriter;
38 import java.io.File;
39 import java.io.FileWriter;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.io.OutputStreamWriter;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.concurrent.TimeUnit;
46 
47 /* Sets up the CEC tests by discovering which port the CEC adapter connected to */
48 public class CecPortDiscoverer extends BaseTargetPreparer {
49 
50     private static final int TIMEOUT_MILLIS = 15000;
51     private static final int MAX_RETRY_COUNT = 3;
52 
53     private File mCecMapDir = HdmiCecConstants.CEC_MAP_FOLDER;
54     private File mDeviceEntry = null;
55     private File mPortEntry = null;
56 
57     private String instructionsOnError =
58             "\nIn case the setup is valid according to the README and "
59                     + "the test should have run, please verify that\n"
60                     + "1. cec-client is not running already on the port the DUT is connected to\n"
61                     + "2. "
62                     + HdmiCecConstants.CEC_MAP_FOLDER
63                     + " has been cleared of stale mappings (in case a"
64                     + " test was interrupted)\n";
65 
66     /** {@inheritDoc} */
67     @Override
setUp(TestInformation testInfo)68     public void setUp(TestInformation testInfo)
69             throws TargetSetupError, DeviceNotAvailableException {
70         ITestDevice device = testInfo.getDevice();
71         if (!device.hasFeature("feature:android.hardware.hdmi.cec")
72                 || !device.hasFeature("feature:android.software.leanback")
73                 || isTvEmulator(device)) {
74             // We are testing non-HDMI devices, so don't check for adapter availability
75             return;
76         }
77         synchronized (CecPortDiscoverer.class) {
78             if (!mCecMapDir.exists()) {
79                 mCecMapDir.mkdirs();
80             }
81             initValidClient(device);
82         }
83     }
84 
85     /**
86      * Check if the DUT is an emulator.
87      * @param device The DUT.
88      * @return true If the DUT is an emulator.
89      * @throws DeviceNotAvailableException
90      */
isTvEmulator(ITestDevice device)91     public static boolean isTvEmulator(ITestDevice device) throws DeviceNotAvailableException {
92         return !device.executeShellCommand("getprop " + HdmiCecConstants.PROPERTY_BUILD_FINGERPRINT
93                         + " | grep \"cf_x86\"")
94                 .isEmpty();
95     }
96 
97     /** {@inheritDoc} */
98     @Override
tearDown(TestInformation testInfo, Throwable e)99     public void tearDown(TestInformation testInfo, Throwable e) {
100         if (mDeviceEntry != null) {
101             mDeviceEntry.delete();
102         }
103         if (mPortEntry != null) {
104             mPortEntry.delete();
105         }
106     }
107 
initValidClient(ITestDevice device)108     private void initValidClient(ITestDevice device)
109             throws TargetSetupError, DeviceNotAvailableException {
110 
111         List<String> launchCommand = new ArrayList();
112         Process mCecClient;
113         /* This is a semi-functional object only, the methods that we can use are limited. */
114         HdmiCecClientWrapper cecClientWrapper = new HdmiCecClientWrapper();
115 
116         launchCommand.add("cec-client");
117         String serialNo = "";
118 
119         try {
120             List<String> comPorts = cecClientWrapper.getValidCecClientPorts();
121 
122             if (comPorts.size() == 0) {
123                 throw new TargetSetupError("No adapters connected to host.");
124             }
125 
126             int targetDeviceType =
127                     BaseHdmiCecCtsTest.getTargetLogicalAddress(device).getDeviceType();
128             int toDevice;
129             launchCommand.add("-t");
130             launchCommand.add("r");
131             launchCommand.add("-t");
132             if (targetDeviceType == HdmiCecConstants.CEC_DEVICE_TYPE_TV) {
133                 toDevice = LogicalAddress.PLAYBACK_1.getLogicalAddressAsInt();
134                 launchCommand.add("p");
135             } else {
136                 toDevice = LogicalAddress.TV.getLogicalAddressAsInt();
137                 launchCommand.add("x");
138             }
139 
140             serialNo = device.getProperty("ro.serialno");
141             String serialNoHashCode = String.valueOf(serialNo.hashCode());
142             String serialNoParam = CecMessage.convertStringToHexParams(serialNoHashCode);
143             /*
144              * formatParams prefixes with a ':' that we do not want in the vendorcommand
145              * command line utility.
146              */
147             serialNoParam = serialNoParam.substring(1);
148             StringBuilder sendVendorCommand = new StringBuilder("cmd hdmi_control vendorcommand ");
149             sendVendorCommand.append(" -t " + targetDeviceType);
150             sendVendorCommand.append(" -d " + toDevice);
151             sendVendorCommand.append(" -a " + serialNoParam);
152 
153             for (String port : comPorts) {
154                 launchCommand.add(port);
155                 boolean portBeingRetried = true;
156                 int retryCount = 0;
157                 do {
158                     File adapterMapping = new File(mCecMapDir, getPortFilename(port));
159                     /*
160                      * Check for the mapping before each iteration. It is possible that another DUT
161                      * got mapped to this port while this DUT is still trying to discover if this is
162                      * the right port.
163                      */
164                     if (adapterMapping.exists()) {
165                         /* Exit the current port's retry loop */
166                         launchCommand.remove(port);
167                         break;
168                     }
169                     mCecClient = RunUtil.getDefault().runCmdInBackground(launchCommand);
170                     BufferedReader inputConsole =
171                             new BufferedReader(
172                                     new InputStreamReader(mCecClient.getInputStream()));
173                     BufferedWriter outputConsole =
174                             new BufferedWriter(
175                                     new OutputStreamWriter(mCecClient.getOutputStream()));
176                     try {
177                         /* Wait for the client to become ready */
178                         if (cecClientWrapper.checkConsoleOutput(
179                                 CecClientMessage.CLIENT_CONSOLE_READY.toString(),
180                                 TIMEOUT_MILLIS, inputConsole)) {
181 
182                             device.executeShellCommand(sendVendorCommand.toString());
183                             if (cecClientWrapper.checkConsoleOutput(
184                                     serialNoParam, TIMEOUT_MILLIS, inputConsole)) {
185                                 if (targetDeviceType != HdmiCecConstants.CEC_DEVICE_TYPE_TV) {
186                                     // Timeout in milliseconds
187                                     long getVersionTimeout = 3000;
188 
189                                     String getVersionMessage = "tx 10:9f";
190                                     cecClientWrapper.sendConsoleMessage(
191                                             getVersionMessage, outputConsole);
192                                     String getVersionResponse = "01:9e";
193                                     if (cecClientWrapper.checkConsoleOutput(
194                                             getVersionResponse, getVersionTimeout, inputConsole)) {
195                                         throw new Exception(
196                                                 "Setup error! The sink device (TV) in the test setup"
197                                                     + " seems to have CEC enabled. Please disable"
198                                                     + " and retry tests.");
199                                     }
200                                 }
201 
202                                 writeMapping(port, serialNo);
203                                 return;
204                             }
205                             /* Since it did not find the required message. Check another port */
206                             portBeingRetried = false;
207                         } else {
208                             CLog.e("Console did not get ready!");
209                             throw new CecClientWrapperException(ErrorCodes.CecPortBusy);
210                         }
211                     } catch (CecClientWrapperException cwe) {
212                         if (cwe.getErrorCode() != ErrorCodes.CecPortBusy) {
213                             retryCount = MAX_RETRY_COUNT;
214                         } else {
215                             retryCount++;
216                         }
217                         if (retryCount >= MAX_RETRY_COUNT) {
218                             /* We have retried enough number of times. Check another port */
219                             portBeingRetried = false;
220                         } else {
221                             /* Give a break before checking the port again. */
222                             TimeUnit.MILLISECONDS.sleep(TIMEOUT_MILLIS);
223                         }
224                     } finally {
225                         /* Kill the unwanted cec-client process. */
226                         boolean processQuit = false;
227                         cecClientWrapper.sendConsoleMessage(
228                                 CecClientMessage.QUIT_CLIENT.toString(), outputConsole);
229                         if (cecClientWrapper.checkConsoleOutput(
230                                 CecClientMessage.CLIENT_CONSOLE_END.toString(),
231                                 TIMEOUT_MILLIS, inputConsole)) {
232                             outputConsole.close();
233                             inputConsole.close();
234                             if (mCecClient.waitFor(10, TimeUnit.SECONDS)) {
235                                 /* The cec-client process is quit */
236                                 processQuit = true;
237                             }
238                         }
239                         if (!processQuit) {
240                             /* Use destryForcibly if the cec-client process is not dead
241                              * in spite of the quit above.
242                              */
243                             Process killProcess = mCecClient.destroyForcibly();
244                             killProcess.waitFor(60, TimeUnit.SECONDS);
245                         }
246                     }
247                 } while (portBeingRetried);
248                 launchCommand.remove(port);
249             }
250         } catch (IOException | InterruptedException e) {
251             throw new TargetSetupError(
252                     "Caught "
253                             + e.getClass().getSimpleName()
254                             + ". "
255                             + "Could not get adapter mapping for device"
256                             + serialNo
257                             + "."
258                             + instructionsOnError,
259                     e);
260         } catch (Exception generic) {
261             throw new TargetSetupError(
262                     "Caught an exception with message '"
263                             + generic.getMessage()
264                             + "'. "
265                             + "Could not get adapter mapping for device"
266                             + serialNo
267                             + "."
268                             + instructionsOnError,
269                     generic);
270         }
271         throw new TargetSetupError(
272                 "Device " + serialNo + " not connected to any adapter!" + instructionsOnError);
273     }
274 
getPortFilename(String port)275     private String getPortFilename(String port) {
276         /* Returns only the name of the port, ignoring the path */
277         return new File(port).getName();
278     }
279 
writeMapping(String port, String serialNo)280     private void writeMapping(String port, String serialNo) throws TargetSetupError {
281         mDeviceEntry = new File(mCecMapDir, serialNo);
282         mPortEntry = new File(mCecMapDir, getPortFilename(port));
283         try (BufferedWriter device = new BufferedWriter(new FileWriter(mDeviceEntry));
284                 BufferedWriter adapter = new BufferedWriter(new FileWriter(mPortEntry))) {
285             mDeviceEntry.createNewFile();
286             device.write(port);
287             device.flush();
288             adapter.write(serialNo);
289             adapter.flush();
290         } catch (IOException ioe) {
291             throw new TargetSetupError(
292                     "Could not create mapping file " + mCecMapDir + "/" + mDeviceEntry.getName());
293         }
294     }
295 }
296