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.util.sl4a; 17 18 import com.android.tradefed.device.DeviceNotAvailableException; 19 import com.android.tradefed.device.ITestDevice; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.util.IRunUtil; 22 import com.android.tradefed.util.RunUtil; 23 24 import org.json.JSONArray; 25 import org.json.JSONException; 26 import org.json.JSONObject; 27 28 import java.io.BufferedReader; 29 import java.io.File; 30 import java.io.IOException; 31 import java.io.InputStreamReader; 32 import java.io.PrintWriter; 33 import java.net.ServerSocket; 34 import java.net.Socket; 35 import java.util.HashMap; 36 import java.util.Map; 37 38 /** 39 * Sl4A client to interact via RPC with SL4A scripting layer. 40 */ 41 public class Sl4aClient implements AutoCloseable { 42 43 private static final String INIT = "initiate"; 44 public static final String IS_SL4A_RUNNING_CMD = 45 "ps -e | grep \"S com.googlecode.android_scripting\""; 46 public static final String IS_SL4A_RUNNING_CMD_OLD = 47 "ps | grep \"S com.googlecode.android_scripting\""; 48 public static final String SL4A_LAUNCH_CMD = 49 "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER " + 50 "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s " + 51 "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher"; 52 public static final String STOP_SL4A_CMD = "am force-stop com.googlecode.android_scripting"; 53 54 private static final int UNKNOWN_ID = -1; 55 56 private ITestDevice mDevice; 57 private int mHostPort; 58 private int mDeviceSidePort; 59 private Socket mSocket; 60 private Long mCounter = 1L; 61 private int mUid = UNKNOWN_ID; 62 63 private Sl4aEventDispatcher mEventDispatcher; 64 65 /** 66 * Creates the Sl4A client. 67 * 68 * @param device the {ITestDevice} that the client will be for. 69 * @param hostPort the port on the host machine to connect to the sl4a client. 70 * @param devicePort the device port used to communicate to. 71 */ Sl4aClient(ITestDevice device, int hostPort, int devicePort)72 public Sl4aClient(ITestDevice device, int hostPort, int devicePort) { 73 mDevice = device; 74 mHostPort = hostPort; 75 mDeviceSidePort = devicePort; 76 } 77 78 /** 79 * Creates the Sl4A client. 80 * 81 * @param device the {ITestDevice} that the client will be for. 82 * @param sl4aApkFile file path to hte sl4a apk to install, or null if already installed. 83 */ Sl4aClient(ITestDevice device, File sl4aApkFile)84 public Sl4aClient(ITestDevice device, File sl4aApkFile) throws DeviceNotAvailableException { 85 installSl4a(device, sl4aApkFile); 86 ServerSocket s = null; 87 int port = -1; 88 try { 89 s = new ServerSocket(0); 90 s.setReuseAddress(true); 91 port = s.getLocalPort(); 92 s.close(); 93 } catch (IOException e) { 94 throw new RuntimeException(e); 95 } 96 mDevice = device; 97 mHostPort = port; 98 mDeviceSidePort = 9998; 99 } 100 installSl4a(ITestDevice device, File sl4aApkFile)101 private static void installSl4a(ITestDevice device, File sl4aApkFile) 102 throws DeviceNotAvailableException { 103 if (sl4aApkFile != null) { 104 if (!sl4aApkFile.exists()) { 105 throw new RuntimeException(String.format("Sl4A apk '%s' was not found.", 106 sl4aApkFile.getAbsoluteFile())); 107 } 108 String res = device.installPackage(sl4aApkFile, true); 109 if (res != null) { 110 throw new RuntimeException(String.format("Error when installing the Sl4A apk: %s", 111 res)); 112 } 113 } 114 } 115 116 /** 117 * Convenience method to create and start a client ready to use. 118 * 119 * @param device the {ITestDevice} that the client will be for. 120 * @param sl4aApkFile file path to hte sl4a apk to install, or null if already installed. 121 * @return an {@link Sl4aClient} instance that has been started. 122 * @throws DeviceNotAvailableException 123 */ startSL4A(ITestDevice device, File sl4aApkFile)124 public static Sl4aClient startSL4A(ITestDevice device, File sl4aApkFile) 125 throws DeviceNotAvailableException { 126 installSl4a(device, sl4aApkFile); 127 ServerSocket s = null; 128 int port = -1; 129 try { 130 s = new ServerSocket(0); 131 s.setReuseAddress(true); 132 port = s.getLocalPort(); 133 s.close(); 134 } catch (IOException e) { 135 throw new RuntimeException(e); 136 } 137 // even after being closed, socket may remain in TIME_WAIT state 138 // reuse address allows to connect to it even in this state. 139 Sl4aClient sl4aClient = new Sl4aClient(device, port, 9998); 140 sl4aClient.startSl4A(); 141 return sl4aClient; 142 } 143 144 /** 145 * Return the default runutil instance. Exposed for testing. 146 */ getRunUtil()147 protected IRunUtil getRunUtil() { 148 return RunUtil.getDefault(); 149 } 150 151 /** 152 * Starts the sl4a client on the device side. 153 * Assume the sl4a apk is installed. 154 */ startSl4A()155 public void startSl4A() throws DeviceNotAvailableException { 156 mDevice.executeShellCommand(String.format(SL4A_LAUNCH_CMD, mDeviceSidePort)); 157 // Allow some times for the process to start. 158 getRunUtil().sleep(2000); 159 if (isSl4ARunning() == false) { 160 throw new RuntimeException("sl4a is not running."); 161 } 162 open(); 163 } 164 165 /** 166 * Return true if the sl4a device side client is running. 167 */ isSl4ARunning()168 public boolean isSl4ARunning() throws DeviceNotAvailableException { 169 // Grep for process with a preceding S which means it is truly started. 170 // Some devices running older version do not support ps -e command, use ps instead 171 // Right now there is no easy way to find out which system support -e option 172 String out1 = mDevice.executeShellCommand(IS_SL4A_RUNNING_CMD_OLD); 173 String out2 = mDevice.executeShellCommand(IS_SL4A_RUNNING_CMD); 174 if (out1 == null || out2 == null) { 175 CLog.i("Null string return"); 176 return false; 177 } else if (out1.trim().isEmpty() && out2.trim().isEmpty()) { 178 CLog.i("Empty return"); 179 return false; 180 } else { 181 return true; 182 } 183 } 184 185 /** Helper to actually starts the connection host to device for sl4a. */ open()186 public void open() { 187 try { 188 mDevice.executeAdbCommand("forward", "tcp:" + mHostPort, "tcp:" + mDeviceSidePort); 189 String res = mDevice.executeAdbCommand("forward", "--list"); 190 CLog.d("forwardings: %s", res); 191 mSocket = new Socket("localhost", mHostPort); 192 CLog.i("is sl4a socket connected: %s", mSocket.isConnected()); 193 String rep = sendCommand(Sl4aClient.INIT); 194 CLog.i("response sl4a INIT: '%s', from device %s", rep, mDevice.getSerialNumber()); 195 JSONObject init = new JSONObject(rep); 196 mUid = init.getInt("uid"); 197 startEventDispatcher(); 198 } catch (IOException | DeviceNotAvailableException | JSONException e) { 199 throw new RuntimeException(e); 200 } 201 } 202 203 /** 204 * Starts the event dispatcher. Exposed for testing. 205 */ startEventDispatcher()206 protected void startEventDispatcher() throws DeviceNotAvailableException { 207 if (isSl4ARunning() == true) { 208 mEventDispatcher = new Sl4aEventDispatcher(this, 5000); 209 mEventDispatcher.start(); 210 } else { 211 throw new RuntimeException("sl4a is not running."); 212 } 213 } 214 215 /** 216 * Helper for initial handshake with SL4A client device side. 217 */ sendCommand(String cmd)218 private String sendCommand(String cmd) throws IOException { 219 Map<String, String> info = new HashMap<>(); 220 info.put("cmd", cmd); 221 info.put("uid", mUid +""); 222 JSONObject message = new JSONObject(info); 223 PrintWriter out = new PrintWriter(mSocket.getOutputStream(), true); 224 out.print(message.toString()); 225 out.print('\n'); 226 CLog.d("flushing"); 227 out.flush(); 228 CLog.d("sent"); 229 BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); 230 CLog.d("reading"); 231 String response = in.readLine(); 232 return response; 233 } 234 235 /** 236 * Helper to send a message through the sl4a socket. 237 * 238 * @param message the JSON object to be sent through the socket. 239 * @return the response of the request. 240 * @throws IOException 241 */ sendThroughSocket(String message)242 private synchronized Object sendThroughSocket(String message) throws IOException { 243 CLog.v("preparing sending: '%s' to device %s", message, mDevice.getSerialNumber()); 244 PrintWriter out = new PrintWriter(mSocket.getOutputStream(), false); 245 out.print(message); 246 out.print('\n'); 247 out.flush(); 248 BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); 249 String response = in.readLine(); 250 CLog.v("response: '%s' from device %s", response, mDevice.getSerialNumber()); 251 try { 252 JSONObject resp = new JSONObject(response); 253 if (!resp.isNull("error")) { 254 throw new IOException(String.format("RPC error: %s", resp.get("error"))); 255 } 256 if (resp.isNull("result")) { 257 return null; 258 } 259 // TODO: verify id is matching 260 return resp.get("result"); 261 } catch (JSONException e) { 262 throw new IOException(e); 263 } 264 } 265 266 /** 267 * Close the sl4a connection to device side and Kills any running instance of sl4a. 268 * If no instance is running then nothing is done. 269 */ 270 @Override close()271 public void close() { 272 try { 273 if (mEventDispatcher != null) { 274 mEventDispatcher.cancel(); 275 } 276 if (mSocket != null) { 277 mSocket.close(); 278 } 279 mDevice.executeShellCommand(STOP_SL4A_CMD); 280 mDevice.executeAdbCommand("forward", "--remove", "tcp:" + mHostPort); 281 } catch (IOException | DeviceNotAvailableException e) { 282 CLog.e(e); 283 } 284 } 285 286 /** 287 * Execute an RPC call on the sl4a layer. 288 * 289 * @param methodName the name of the method to be called on device side. 290 * @param args the arg list to be used on the method. 291 * @return the result of the request. 292 * @throws IOException if the requested method does not exists. 293 */ rpcCall(String methodName, Object... args)294 public Object rpcCall(String methodName, Object... args) throws IOException { 295 JSONArray argsFormatted = new JSONArray(); 296 if (args != null) { 297 for (Object arg : args) { 298 argsFormatted.put(arg); 299 } 300 } 301 JSONObject message = new JSONObject(); 302 try { 303 message.put("id", mCounter); 304 message.put("method", methodName); 305 message.put("params", argsFormatted); 306 } catch (JSONException e) { 307 CLog.e(e); 308 throw new IOException("Failed to format the message", e); 309 } 310 mCounter++; 311 return sendThroughSocket(message.toString()); 312 } 313 314 /** 315 * Return the event dispatcher to wait for events. 316 */ getEventDispatcher()317 public Sl4aEventDispatcher getEventDispatcher() { 318 return mEventDispatcher; 319 } 320 } 321