1 /*
2  * Copyright (C) 2016 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 package com.android.tradefed.device;
17 
18 import com.android.ddmlib.IDevice;
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.util.CommandResult;
21 import com.android.tradefed.util.CommandStatus;
22 
23 /**
24  * Implementation of a {@link ITestDevice} for a full stack android device connected via
25  * adb connect.
26  * Assume the device serial will be in the format <hostname>:<portnumber> in adb.
27  */
28 public class RemoteAndroidDevice extends TestDevice {
29     public static final long WAIT_FOR_ADB_CONNECT = 2 * 60 * 1000;
30 
31     protected static final long RETRY_INTERVAL_MS = 5000;
32     protected static final int MAX_RETRIES = 5;
33     protected static final long DEFAULT_SHORT_CMD_TIMEOUT = 20 * 1000;
34 
35     private static final String ADB_SUCCESS_CONNECT_TAG = "connected to";
36     private static final String ADB_ALREADY_CONNECTED_TAG = "already";
37     private static final String ADB_CONN_REFUSED = "Connection refused";
38 
39     /**
40      * Creates a {@link RemoteAndroidDevice}.
41      *
42      * @param device the associated {@link IDevice}
43      * @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
44      * @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
45      */
RemoteAndroidDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)46     public RemoteAndroidDevice(IDevice device, IDeviceStateMonitor stateMonitor,
47             IDeviceMonitor allocationMonitor) {
48         super(device, stateMonitor, allocationMonitor);
49     }
50 
51     /**
52      * {@inheritDoc}
53      */
54     @Override
postAdbRootAction()55     public void postAdbRootAction() throws DeviceNotAvailableException {
56         // attempt to reconnect first to make sure we didn't loose the connection because of
57         // adb root.
58         adbTcpConnect(getHostName(), getPortNum());
59         waitForAdbConnect(WAIT_FOR_ADB_CONNECT);
60     }
61 
62     /**
63      * {@inheritDoc}
64      */
65     @Override
postAdbUnrootAction()66     public void postAdbUnrootAction() throws DeviceNotAvailableException {
67         // attempt to reconnect first to make sure we didn't loose the connection because of
68         // adb unroot.
69         adbTcpConnect(getHostName(), getPortNum());
70         waitForAdbConnect(WAIT_FOR_ADB_CONNECT);
71     }
72 
73     /**
74      * Return the hostname associated with the device. Extracted from the serial.
75      */
getHostName()76     public String getHostName() {
77         if (!checkSerialFormatValid()) {
78             throw new RuntimeException(
79                     String.format("Serial Format is unexpected: %s "
80                             + "should look like <hostname>:<port>", getSerialNumber()));
81         }
82         return getSerialNumber().split(":")[0];
83     }
84 
85     /**
86      * Return the port number asociated with the device. Extracted from the serial.
87      */
getPortNum()88     public String getPortNum() {
89         if (!checkSerialFormatValid()) {
90             throw new RuntimeException(
91                     String.format("Serial Format is unexpected: %s "
92                             + "should look like <hostname>:<port>", getSerialNumber()));
93         }
94         return getSerialNumber().split(":")[1];
95     }
96 
97     /**
98      * Check if the format of the serial is as expected <hostname>:port
99      * @return true if the format is valid, false otherwise.
100      */
checkSerialFormatValid()101     private boolean checkSerialFormatValid() {
102         String[] serial =  getSerialNumber().split(":");
103         if (serial.length == 2) {
104             try {
105                 Integer.parseInt(serial[1]);
106                 return true;
107             } catch (NumberFormatException nfe) {
108                 return false;
109             }
110         }
111         return false;
112     }
113 
114     /**
115      * Helper method to adb connect to a given tcp ip Android device
116      *
117      * @param host the hostname/ip of a tcp/ip Android device
118      * @param port the port number of a tcp/ip device
119      * @return true if we successfully connected to the device, false
120      *         otherwise.
121      */
adbTcpConnect(String host, String port)122     public boolean adbTcpConnect(String host, String port) {
123         for (int i = 0; i < MAX_RETRIES; i++) {
124             CommandResult result = getRunUtil().runTimedCmd(DEFAULT_SHORT_CMD_TIMEOUT, "adb",
125                     "connect", String.format("%s:%s", host, port));
126             if (CommandStatus.SUCCESS.equals(result.getStatus()) &&
127                 result.getStdout().contains(ADB_SUCCESS_CONNECT_TAG)) {
128                 CLog.d("adb connect output: status: %s stdout: %s stderr: %s",
129                         result.getStatus(), result.getStdout(), result.getStderr());
130 
131                 // It is possible to get a positive result without it being connected because of
132                 // the ssh bridge. Retrying to get confirmation, and expecting "already connected".
133                 if(confirmAdbTcpConnect(host, port)) {
134                     return true;
135                 }
136             } else if (CommandStatus.SUCCESS.equals(result.getStatus()) &&
137                     result.getStdout().contains(ADB_CONN_REFUSED)) {
138                 // If we find "Connection Refused", we bail out directly as more connect won't help
139                 return false;
140             }
141             CLog.d("adb connect output: status: %s stdout: %s stderr: %s, retrying.",
142                     result.getStatus(), result.getStdout(), result.getStderr());
143             getRunUtil().sleep((i + 1) * RETRY_INTERVAL_MS);
144         }
145         return false;
146     }
147 
confirmAdbTcpConnect(String host, String port)148     private boolean confirmAdbTcpConnect(String host, String port) {
149         CommandResult resultConfirmation =
150                 getRunUtil().runTimedCmd(DEFAULT_SHORT_CMD_TIMEOUT, "adb", "connect",
151                 String.format("%s:%s", host, port));
152         if (CommandStatus.SUCCESS.equals(resultConfirmation.getStatus()) &&
153                 resultConfirmation.getStdout().contains(ADB_ALREADY_CONNECTED_TAG)) {
154             CLog.d("adb connect confirmed:\nstdout: %s\nsterr: %s",
155                     resultConfirmation.getStdout(), resultConfirmation.getStderr());
156             return true;
157         } else {
158             CLog.d("adb connect confirmation failed:\nstatus:%s\nstdout: %s\nsterr: %s",
159                     resultConfirmation.getStatus(), resultConfirmation.getStdout(),
160                     resultConfirmation.getStderr());
161         }
162         return false;
163     }
164 
165     /**
166      * Helper method to adb disconnect from a given tcp ip Android device
167      *
168      * @param host the hostname/ip of a tcp/ip Android device
169      * @param port the port number of a tcp/ip device
170      * @return true if we successfully disconnected to the device, false
171      *         otherwise.
172      */
adbTcpDisconnect(String host, String port)173     public boolean adbTcpDisconnect(String host, String port) {
174         CommandResult result = getRunUtil().runTimedCmd(DEFAULT_SHORT_CMD_TIMEOUT, "adb",
175                 "disconnect",
176                 String.format("%s:%s", host, port));
177         return CommandStatus.SUCCESS.equals(result.getStatus());
178     }
179 
180     /**
181      * Check if the adb connection is enabled.
182      */
waitForAdbConnect(final long waitTime)183     public void waitForAdbConnect(final long waitTime) throws DeviceNotAvailableException {
184         CLog.i("Waiting %d ms for adb connection.", waitTime);
185         long startTime = System.currentTimeMillis();
186         while (System.currentTimeMillis() - startTime < waitTime) {
187             if (confirmAdbTcpConnect(getHostName(), getPortNum())) {
188                 CLog.d("Adb connection confirmed.");
189                 return;
190             }
191             getRunUtil().sleep(RETRY_INTERVAL_MS);
192         }
193         throw new DeviceNotAvailableException(
194                 String.format("No adb connection after %sms.", waitTime), getSerialNumber());
195     }
196 
197     /**
198      * {@inheritDoc}
199      */
200     @Override
isEncryptionSupported()201     public boolean isEncryptionSupported() {
202         // Prevent device from being encrypted since we won't have a way to decrypt on Remote
203         // devices since fastboot cannot be use remotely
204         return false;
205     }
206 
207     /**
208      * {@inheritDoc}
209      */
210     @Override
getMacAddress()211     public String getMacAddress() {
212         return null;
213     }
214 }
215