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