aboutsummaryrefslogtreecommitdiff
path: root/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc')
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java37
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java49
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java201
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java79
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java121
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java24
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java252
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java36
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java25
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java29
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java36
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java285
12 files changed, 1174 insertions, 0 deletions
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java
new file mode 100644
index 0000000..9428c82
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.Context;
+import java.io.IOException;
+
+public class AndroidProxy {
+
+ private final JsonRpcServer mJsonRpcServer;
+
+ public AndroidProxy(Context context) {
+ mJsonRpcServer = new JsonRpcServer(context);
+ }
+
+ public void startLocal(int port) throws IOException {
+ mJsonRpcServer.startLocal(port);
+ }
+
+ public int getPort() {
+ return mJsonRpcServer.getPort();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java
new file mode 100644
index 0000000..6bdd8ca
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The {@link AsyncRpc} annotation is used to annotate server-side implementations of RPCs that
+ * trigger asynchronous events. This behaves generally the same as {@link Rpc}, but methods that are
+ * annotated with {@link AsyncRpc} are expected to take the extra parameter which is the ID to use
+ * when posting async events.
+ *
+ * <p>Sample Usage:
+ *
+ * <pre>{@code
+ * {@literal @}AsyncRpc(description = "An example showing the usage of AsyncRpc")
+ * public void doSomethingAsync(String callbackId, ...) {
+ * // start some async operation and post a Snippet Event object with the given callbackId.
+ * }
+ * }</pre>
+ *
+ * AsyncRpc methods can still return serializable values, which will be transported in the regular
+ * return value field of the Rpc protocol.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface AsyncRpc {
+ /** Returns brief description of the function. Should be limited to one or two sentences. */
+ String description();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java
new file mode 100644
index 0000000..a1d3425
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class JsonBuilder {
+
+ private JsonBuilder() {}
+
+ @SuppressWarnings("unchecked")
+ public static Object build(Object data) throws JSONException {
+ if (data == null) {
+ return JSONObject.NULL;
+ }
+ if (data instanceof Integer) {
+ return data;
+ }
+ if (data instanceof Float) {
+ return data;
+ }
+ if (data instanceof Double) {
+ return data;
+ }
+ if (data instanceof Long) {
+ return data;
+ }
+ if (data instanceof String) {
+ return data;
+ }
+ if (data instanceof Boolean) {
+ return data;
+ }
+ if (data instanceof JsonSerializable) {
+ return ((JsonSerializable) data).toJSON();
+ }
+ if (data instanceof JSONObject) {
+ return data;
+ }
+ if (data instanceof JSONArray) {
+ return data;
+ }
+ if (data instanceof Set<?>) {
+ List<Object> items = new ArrayList<>((Set<?>) data);
+ return buildJsonList(items);
+ }
+ if (data instanceof Collection<?>) {
+ List<Object> items = new ArrayList<>((Collection<?>) data);
+ return buildJsonList(items);
+ }
+ if (data instanceof List<?>) {
+ return buildJsonList((List<?>) data);
+ }
+ if (data instanceof Bundle) {
+ return buildJsonBundle((Bundle) data);
+ }
+ if (data instanceof Intent) {
+ return buildJsonIntent((Intent) data);
+ }
+ if (data instanceof Map<?, ?>) {
+ // TODO(damonkohler): I would like to make this a checked cast if possible.
+ return buildJsonMap((Map<String, ?>) data);
+ }
+ if (data instanceof ParcelUuid) {
+ return data.toString();
+ }
+ // TODO(xpconanfan): Deprecate the following default non-primitive type builders.
+ if (data instanceof InetSocketAddress) {
+ return buildInetSocketAddress((InetSocketAddress) data);
+ }
+ if (data instanceof InetAddress) {
+ return buildInetAddress((InetAddress) data);
+ }
+ if (data instanceof URL) {
+ return buildURL((URL) data);
+ }
+ if (data instanceof byte[]) {
+ JSONArray result = new JSONArray();
+ for (byte b : (byte[]) data) {
+ result.put(b & 0xFF);
+ }
+ return result;
+ }
+ if (data instanceof Object[]) {
+ return buildJSONArray((Object[]) data);
+ }
+ // Try with custom converter provided by user.
+ Object result = SnippetObjectConverterManager.getInstance().objectToJson(data);
+ if (result != null) {
+ return result;
+ }
+ return data.toString();
+ }
+
+ private static Object buildInetAddress(InetAddress data) {
+ JSONArray address = new JSONArray();
+ address.put(data.getHostName());
+ address.put(data.getHostAddress());
+ return address;
+ }
+
+ private static Object buildInetSocketAddress(InetSocketAddress data) {
+ JSONArray address = new JSONArray();
+ address.put(data.getHostName());
+ address.put(data.getPort());
+ return address;
+ }
+
+ private static JSONArray buildJSONArray(Object[] data) throws JSONException {
+ JSONArray result = new JSONArray();
+ for (Object o : data) {
+ result.put(build(o));
+ }
+ return result;
+ }
+
+ private static JSONObject buildJsonBundle(Bundle bundle) throws JSONException {
+ JSONObject result = new JSONObject();
+ bundle.setClassLoader(JsonBuilder.class.getClassLoader());
+ for (String key : bundle.keySet()) {
+ result.put(key, build(bundle.get(key)));
+ }
+ return result;
+ }
+
+ private static JSONObject buildJsonIntent(Intent data) throws JSONException {
+ JSONObject result = new JSONObject();
+ result.put("data", data.getDataString());
+ result.put("type", data.getType());
+ result.put("extras", build(data.getExtras()));
+ result.put("categories", build(data.getCategories()));
+ result.put("action", data.getAction());
+ ComponentName component = data.getComponent();
+ if (component != null) {
+ result.put("packagename", component.getPackageName());
+ result.put("classname", component.getClassName());
+ }
+ result.put("flags", data.getFlags());
+ return result;
+ }
+
+ private static <T> JSONArray buildJsonList(final List<T> list) throws JSONException {
+ JSONArray result = new JSONArray();
+ for (T item : list) {
+ result.put(build(item));
+ }
+ return result;
+ }
+
+ private static JSONObject buildJsonMap(Map<String, ?> map) throws JSONException {
+ JSONObject result = new JSONObject();
+ for (Entry<String, ?> entry : map.entrySet()) {
+ String key = entry.getKey();
+ if (key == null) {
+ key = "";
+ }
+ result.put(key, build(entry.getValue()));
+ }
+ return result;
+ }
+
+ private static Object buildURL(URL data) throws JSONException {
+ JSONObject url = new JSONObject();
+ url.put("Authority", data.getAuthority());
+ url.put("Host", data.getHost());
+ url.put("Path", data.getPath());
+ url.put("Port", data.getPort());
+ url.put("Protocol", data.getProtocol());
+ return url;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java
new file mode 100644
index 0000000..90cf8f9
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents a JSON RPC result.
+ *
+ * @see <a href="http://json-rpc.org/wiki/specification">http://json-rpc.org/wiki/specification</a>
+ */
+public class JsonRpcResult {
+
+ private JsonRpcResult() {
+ // Utility class.
+ }
+
+ public static JSONObject empty(int id) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JSONObject.NULL);
+ json.put("callback", JSONObject.NULL);
+ json.put("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject result(int id, Object data) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JsonBuilder.build(data));
+ json.put("callback", JSONObject.NULL);
+ json.put("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject callback(int id, Object data, String callbackId) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JsonBuilder.build(data));
+ json.put("callback", callbackId);
+ json.put("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject error(int id, Throwable t) throws JSONException {
+ String stackTrace = getStackTrace(t);
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JSONObject.NULL);
+ json.put("callback", JSONObject.NULL);
+ json.put("error", stackTrace);
+ return json;
+ }
+
+ public static String getStackTrace(Throwable throwable) {
+ StringWriter stackTraceWriter = new StringWriter();
+ stackTraceWriter.write("\n-------------- Java Stacktrace ---------------\n");
+ throwable.printStackTrace(new PrintWriter(stackTraceWriter));
+ stackTraceWriter.write("----------------------------------------------");
+ return stackTraceWriter.toString();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java
new file mode 100644
index 0000000..1dde423
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.Context;
+import com.google.android.mobly.snippet.manager.SnippetManager;
+import com.google.android.mobly.snippet.util.Log;
+import com.google.android.mobly.snippet.util.RpcUtil;
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** A JSON RPC server that forwards RPC calls to a specified receiver object. */
+public class JsonRpcServer extends SimpleServer {
+ private static final String CMD_CLOSE_SESSION = "closeSl4aSession";
+ private static final String CMD_HELP = "help";
+
+ private final SnippetManager mSnippetManager;
+ private final RpcUtil mRpcUtil;
+
+ /** Construct a {@link JsonRpcServer} connected to the provided {@link SnippetManager}. */
+ public JsonRpcServer(Context context) {
+ mSnippetManager = SnippetManager.initSnippetManager(context);
+ mRpcUtil = new RpcUtil();
+ }
+
+ @Override
+ protected void handleRPCConnection(
+ Socket sock, Integer UID, BufferedReader reader, PrintWriter writer) throws Exception {
+ Log.d("UID " + UID);
+ String data;
+ while ((data = reader.readLine()) != null) {
+ Log.v("Session " + UID + " Received: " + data);
+ JSONObject request = new JSONObject(data);
+ int id = request.getInt("id");
+ String method = request.getString("method");
+ JSONArray params = request.getJSONArray("params");
+
+ // Handle builtin commands
+ if (method.equals(CMD_HELP)) {
+ help(writer, id, mSnippetManager, UID);
+ continue;
+ } else if (method.equals(CMD_CLOSE_SESSION)) {
+ Log.d("Got shutdown signal");
+ synchronized (writer) {
+ // Shut down all RPC receivers.
+ mSnippetManager.shutdown();
+
+ // Shut down this client connection. As soon as this happens, the client will
+ // kill us by triggering the 'stop' action from another instrumentation, so no
+ // other cleanup steps are guaranteed to execute.
+ send(writer, JsonRpcResult.empty(id), UID);
+ reader.close();
+ writer.close();
+ sock.close();
+
+ // Shut down this server.
+ shutdown();
+ }
+ return;
+ }
+ JSONObject returnValue = mRpcUtil.invokeRpc(method, params, id, UID);
+ send(writer, returnValue, UID);
+ }
+ }
+
+ private void help(PrintWriter writer, int id, SnippetManager receiverManager, Integer UID)
+ throws JSONException {
+ // Create a map from class simple name to the methods inside it.
+ Map<String, Set<MethodDescriptor>> methods = new TreeMap<>();
+ for (String method : receiverManager.getMethodNames()) {
+ MethodDescriptor descriptor = receiverManager.getMethodDescriptor(method);
+ String snippetClassName = descriptor.getSnippetClass().getSimpleName();
+ Set<MethodDescriptor> snippetClassMethods = methods.get(snippetClassName);
+ if (snippetClassMethods == null) {
+ // Preserve insertion order (alphabetical)
+ snippetClassMethods = new LinkedHashSet<>();
+ methods.put(snippetClassName, snippetClassMethods);
+ }
+ snippetClassMethods.add(descriptor);
+ }
+ StringBuilder result = new StringBuilder();
+ for (Map.Entry<String, Set<MethodDescriptor>> entry : methods.entrySet()) {
+ result.append("\nRPCs provided by ").append(entry.getKey()).append(":\n");
+ for (MethodDescriptor descriptor : entry.getValue()) {
+ result.append(" ").append(descriptor.getHelp()).append("\n");
+ }
+ }
+ send(writer, JsonRpcResult.result(id, result), UID);
+ }
+
+ private void send(PrintWriter writer, JSONObject result, int UID) {
+ writer.write(result + "\n");
+ writer.flush();
+ Log.v("Session " + UID + " Sent: " + result);
+ }
+
+ @Override
+ protected void handleConnection(Socket socket) throws Exception {}
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java
new file mode 100644
index 0000000..5871e01
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public interface JsonSerializable {
+ JSONObject toJSON() throws JSONException;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java
new file mode 100644
index 0000000..b9c8a7a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.Intent;
+import android.net.Uri;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.manager.SnippetManager;
+import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
+import com.google.android.mobly.snippet.util.AndroidUtil;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** An adapter that wraps {@code Method}. */
+public final class MethodDescriptor {
+ private final Method mMethod;
+ private final Class<? extends Snippet> mClass;
+
+ private MethodDescriptor(Class<? extends Snippet> clazz, Method method) {
+ mClass = clazz;
+ mMethod = method;
+ }
+
+ @Override
+ public String toString() {
+ return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName();
+ }
+
+ /** Collects all methods with {@code RPC} annotation from given class. */
+ public static Collection<MethodDescriptor> collectFrom(Class<? extends Snippet> clazz) {
+ List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>();
+ for (Method method : clazz.getMethods()) {
+ if (method.isAnnotationPresent(Rpc.class)
+ || method.isAnnotationPresent(AsyncRpc.class)) {
+ descriptors.add(new MethodDescriptor(clazz, method));
+ }
+ }
+ return descriptors;
+ }
+
+ /**
+ * Invokes the call that belongs to this object with the given parameters. Wraps the response
+ * (possibly an exception) in a JSONObject.
+ *
+ * @param parameters {@code JSONArray} containing the parameters
+ * @return result
+ * @throws Throwable the exception raised from executing the RPC method.
+ */
+ public Object invoke(SnippetManager manager, final JSONArray parameters) throws Throwable {
+ final Type[] parameterTypes = getGenericParameterTypes();
+ final Object[] args = new Object[parameterTypes.length];
+
+ if (parameters.length() > args.length) {
+ throw new RpcError("Too many parameters specified.");
+ }
+
+ for (int i = 0; i < args.length; i++) {
+ final Type parameterType = parameterTypes[i];
+ if (i < parameters.length()) {
+ args[i] = convertParameter(parameters, i, parameterType);
+ } else {
+ throw new RpcError("Argument " + (i + 1) + " is not present");
+ }
+ }
+
+ return manager.invoke(mClass, mMethod, args);
+ }
+
+ /** Converts a parameter from JSON into a Java Object. */
+ // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative
+ // would be to work on one supplied parameter and return the converted parameter. However,
+ // that's problematic because you lose the ability to call the getXXX methods on the JSON array.
+ // @VisibleForTesting
+ private static Object convertParameter(final JSONArray parameters, int index, Type type)
+ throws JSONException, RpcError {
+ try {
+ // We must handle null and numbers explicitly because we cannot magically cast them. We
+ // also need to convert implicitly from numbers to bools.
+ if (parameters.isNull(index)) {
+ return null;
+ } else if (type == Boolean.class || type == boolean.class) {
+ try {
+ return parameters.getBoolean(index);
+ } catch (JSONException e) {
+ return parameters.getInt(index) != 0;
+ }
+ } else if (type == Long.class || type == long.class) {
+ return parameters.getLong(index);
+ } else if (type == Double.class || type == double.class) {
+ return parameters.getDouble(index);
+ } else if (type == Integer.class || type == int.class) {
+ return parameters.getInt(index);
+ } else if (type == Intent.class) {
+ return buildIntent(parameters.getJSONObject(index));
+ } else if (type == String.class) {
+ return parameters.getString(index);
+ } else if (type == Integer[].class || type == int[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ Integer[] result = new Integer[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = list.getInt(i);
+ }
+ return result;
+ } else if (type == Long[].class || type == long[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ Long[] result = new Long[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = list.getLong(i);
+ }
+ return result;
+ } else if (type == Byte.class || type == byte[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ byte[] result = new byte[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = (byte) list.getInt(i);
+ }
+ return result;
+ } else if (type == String[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ String[] result = new String[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = list.getString(i);
+ }
+ return result;
+ } else if (type == JSONObject.class) {
+ return parameters.getJSONObject(index);
+ } else if (type == JSONArray.class) {
+ return parameters.getJSONArray(index);
+ } else {
+ // Try any custom converter provided.
+ Object object =
+ SnippetObjectConverterManager.getInstance()
+ .jsonToObject(parameters.getJSONObject(index), type);
+ if (object != null) {
+ return object;
+ }
+ // Magically cast the parameter to the right Java type.
+ return ((Class<?>) type).cast(parameters.get(index));
+ }
+ } catch (ClassCastException e) {
+ throw new RpcError(
+ "Argument "
+ + (index + 1)
+ + " should be of type "
+ + ((Class<?>) type).getSimpleName()
+ + ", but is of type "
+ + parameters.get(index).getClass().getSimpleName());
+ }
+ }
+
+ private static Object buildIntent(JSONObject jsonObject) throws JSONException {
+ Intent intent = new Intent();
+ if (jsonObject.has("action")) {
+ intent.setAction(jsonObject.getString("action"));
+ }
+ if (jsonObject.has("data") && jsonObject.has("type")) {
+ intent.setDataAndType(
+ Uri.parse(jsonObject.optString("data", null)),
+ jsonObject.optString("type", null));
+ } else if (jsonObject.has("data")) {
+ intent.setData(Uri.parse(jsonObject.optString("data", null)));
+ } else if (jsonObject.has("type")) {
+ intent.setType(jsonObject.optString("type", null));
+ }
+ if (jsonObject.has("packagename") && jsonObject.has("classname")) {
+ intent.setClassName(
+ jsonObject.getString("packagename"), jsonObject.getString("classname"));
+ }
+ if (jsonObject.has("flags")) {
+ intent.setFlags(jsonObject.getInt("flags"));
+ }
+ if (!jsonObject.isNull("extras")) {
+ AndroidUtil.putExtrasFromJsonObject(jsonObject.getJSONObject("extras"), intent);
+ }
+ if (!jsonObject.isNull("categories")) {
+ JSONArray categories = jsonObject.getJSONArray("categories");
+ for (int i = 0; i < categories.length(); i++) {
+ intent.addCategory(categories.getString(i));
+ }
+ }
+ return intent;
+ }
+
+ public String getName() {
+ return mMethod.getName();
+ }
+
+ private Type[] getGenericParameterTypes() {
+ return mMethod.getGenericParameterTypes();
+ }
+
+ public boolean isAsync() {
+ return mMethod.isAnnotationPresent(AsyncRpc.class);
+ }
+
+ Class<? extends Snippet> getSnippetClass() {
+ return mClass;
+ }
+
+ private String getAnnotationDescription() {
+ if (isAsync()) {
+ AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class);
+ return annotation.description();
+ }
+ Rpc annotation = mMethod.getAnnotation(Rpc.class);
+ return annotation.description();
+ }
+ /**
+ * Returns a human-readable help text for this RPC, based on annotations in the source code.
+ *
+ * @return derived help string
+ */
+ String getHelp() {
+ StringBuilder paramBuilder = new StringBuilder();
+ Class<?>[] parameterTypes = mMethod.getParameterTypes();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (i != 0) {
+ paramBuilder.append(", ");
+ }
+ paramBuilder.append(parameterTypes[i].getSimpleName());
+ }
+ return String.format(
+ Locale.US,
+ "%s %s(%s) returns %s // %s",
+ isAsync() ? "@AsyncRpc" : "@Rpc",
+ mMethod.getName(),
+ paramBuilder,
+ mMethod.getReturnType().getSimpleName(),
+ getAnnotationDescription());
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java
new file mode 100644
index 0000000..af321ba
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The {@link Rpc} annotation is used to annotate server-side implementations of RPCs. It describes
+ * meta-information (currently a brief documentation of the function), and marks a function as the
+ * implementation of an RPC.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface Rpc {
+ /** Returns brief description of the function. Should be limited to one or two sentences. */
+ String description();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java
new file mode 100644
index 0000000..0862673
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+@SuppressWarnings("serial")
+public class RpcError extends Exception {
+
+ public RpcError(String message) {
+ super(message);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java
new file mode 100644
index 0000000..f03fd2a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Use this annotation to specify minimum SDK level (if higher than 3). */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RpcMinSdk {
+ /** Minimum SDK Level. */
+ int value();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java
new file mode 100644
index 0000000..cde08f0
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.mobly.snippet.rpc;
+
+import com.google.android.mobly.snippet.Snippet;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation will cause the RPC to execute on the main app thread.
+ *
+ * <p>This annotation can be applied to:
+ *
+ * <ul>
+ * <li>The constructor of a class implementing the {@link Snippet} interface.
+ * <li>A method annotated with the {@link Rpc} or {@link AsyncRpc} annotation.
+ * <li>The {@link Snippet#shutdown()} method.
+ * </ul>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RunOnUiThread {}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java
new file mode 100644
index 0000000..db7255a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import com.google.android.mobly.snippet.util.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** A simple server. */
+public abstract class SimpleServer {
+ private static int threadIndex = 0;
+ private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads =
+ new ConcurrentHashMap<>();
+ private final List<SimpleServerObserver> mObservers = new ArrayList<>();
+ private volatile boolean mStopServer = false;
+ private ServerSocket mServer;
+ private Thread mServerThread;
+
+ public interface SimpleServerObserver {
+ void onConnect();
+
+ void onDisconnect();
+ }
+
+ protected abstract void handleConnection(Socket socket) throws Exception;
+
+ protected abstract void handleRPCConnection(
+ Socket socket, Integer UID, BufferedReader reader, PrintWriter writer) throws Exception;
+
+ /** Adds an observer. */
+ public void addObserver(SimpleServerObserver observer) {
+ mObservers.add(observer);
+ }
+
+ /** Removes an observer. */
+ public void removeObserver(SimpleServerObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ private void notifyOnConnect() {
+ for (SimpleServerObserver observer : mObservers) {
+ observer.onConnect();
+ }
+ }
+
+ private void notifyOnDisconnect() {
+ for (SimpleServerObserver observer : mObservers) {
+ observer.onDisconnect();
+ }
+ }
+
+ private final class ConnectionThread extends Thread {
+ private final Socket mmSocket;
+ private final BufferedReader reader;
+ private final PrintWriter writer;
+ private final Integer UID;
+ private final boolean isRpc;
+
+ private ConnectionThread(
+ Socket socket,
+ boolean rpc,
+ Integer uid,
+ BufferedReader reader,
+ PrintWriter writer) {
+ setName("SimpleServer ConnectionThread " + getId());
+ mmSocket = socket;
+ this.UID = uid;
+ this.reader = reader;
+ this.writer = writer;
+ this.isRpc = rpc;
+ }
+
+ @Override
+ public void run() {
+ Log.v("Server thread " + getId() + " started.");
+ try {
+ if (isRpc) {
+ Log.d("Handling RPC connection in " + getId());
+ handleRPCConnection(mmSocket, UID, reader, writer);
+ } else {
+ Log.d("Handling Non-RPC connection in " + getId());
+ handleConnection(mmSocket);
+ }
+ } catch (Exception e) {
+ if (!mStopServer) {
+ Log.e("Server error.", e);
+ }
+ } finally {
+ close();
+ mConnectionThreads.remove(this.UID);
+ notifyOnDisconnect();
+ Log.v("Server thread " + getId() + " stopped.");
+ }
+ }
+
+ private void close() {
+ if (mmSocket != null) {
+ try {
+ mmSocket.close();
+ } catch (IOException e) {
+ Log.e(e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ private InetAddress getPrivateInetAddress() throws UnknownHostException, SocketException {
+
+ InetAddress candidate = null;
+ Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+ for (NetworkInterface netint : Collections.list(nets)) {
+ if (!netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active
+ continue;
+ }
+ Enumeration<InetAddress> addresses = netint.getInetAddresses();
+ for (InetAddress address : Collections.list(addresses)) {
+ if (address instanceof Inet4Address) {
+ Log.d("local address " + address);
+ return address; // Prefer ipv4
+ }
+ candidate = address; // Probably an ipv6
+ }
+ }
+ if (candidate != null) {
+ return candidate; // return ipv6 address if no suitable ipv6
+ }
+ return InetAddress.getLocalHost(); // No damn matches. Give up, return local host.
+ }
+
+ /**
+ * Starts the RPC server bound to the localhost address.
+ *
+ * @param port the port to bind to or 0 to pick any unused port
+ * @throws IOException
+ */
+ public void startLocal(int port) throws IOException {
+ InetAddress address = getPrivateInetAddress();
+ mServer = new ServerSocket(port, 5 /* backlog */, address);
+ start();
+ }
+
+ public int getPort() {
+ return mServer.getLocalPort();
+ }
+
+ private void start() {
+ mServerThread =
+ new Thread() {
+ @Override
+ public void run() {
+ while (!mStopServer) {
+ try {
+ Socket sock = mServer.accept();
+ if (!mStopServer) {
+ startConnectionThread(sock);
+ } else {
+ sock.close();
+ }
+ } catch (IOException e) {
+ if (!mStopServer) {
+ Log.e("Failed to accept connection.", e);
+ }
+ } catch (JSONException e) {
+ if (!mStopServer) {
+ Log.e("Failed to parse request.", e);
+ }
+ }
+ }
+ }
+ };
+ mServerThread.start();
+ Log.v("Bound to " + mServer.getInetAddress());
+ }
+
+ private void startConnectionThread(final Socket sock) throws IOException, JSONException {
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(sock.getInputStream()), 8192);
+ PrintWriter writer = new PrintWriter(sock.getOutputStream(), true);
+ String data;
+ if ((data = reader.readLine()) != null) {
+ Log.v("Received: " + data);
+ JSONObject request = new JSONObject(data);
+ if (request.has("cmd") && request.has("uid")) {
+ String cmd = request.getString("cmd");
+ int uid = request.getInt("uid");
+ JSONObject result = new JSONObject();
+ if (cmd.equals("initiate")) {
+ Log.d("Initiate a new session");
+ threadIndex += 1;
+ int mUID = threadIndex;
+ ConnectionThread networkThread =
+ new ConnectionThread(sock, true, mUID, reader, writer);
+ mConnectionThreads.put(mUID, networkThread);
+ networkThread.start();
+ notifyOnConnect();
+ result.put("uid", mUID);
+ result.put("status", true);
+ result.put("error", null);
+ } else if (cmd.equals("continue")) {
+ Log.d("Continue an existing session");
+ Log.d("keys: " + mConnectionThreads.keySet().toString());
+ if (!mConnectionThreads.containsKey(uid)) {
+ result.put("uid", uid);
+ result.put("status", false);
+ result.put("error", "Session does not exist.");
+ } else {
+ ConnectionThread networkThread =
+ new ConnectionThread(sock, true, uid, reader, writer);
+ mConnectionThreads.put(uid, networkThread);
+ networkThread.start();
+ notifyOnConnect();
+ result.put("uid", uid);
+ result.put("status", true);
+ result.put("error", null);
+ }
+ } else {
+ result.put("uid", uid);
+ result.put("status", false);
+ result.put("error", "Unrecognized command.");
+ }
+ writer.write(result + "\n");
+ writer.flush();
+ Log.v("Sent: " + result);
+ } else {
+ ConnectionThread networkThread =
+ new ConnectionThread(sock, false, 0, reader, writer);
+ mConnectionThreads.put(0, networkThread);
+ networkThread.start();
+ notifyOnConnect();
+ }
+ }
+ }
+
+ public void shutdown() throws Exception {
+ // Stop listening on the server socket to ensure that
+ // beyond this point there are no incoming requests.
+ mStopServer = true;
+ try {
+ mServer.close();
+ } catch (IOException e) {
+ Log.e("Failed to close server socket.", e);
+ }
+ // Since the server is not running, the mNetworkThreads set can only
+ // shrink from this point onward. We can just stop all of the running helper
+ // threads. In the worst case, one of the running threads will already have
+ // shut down. Since this is a CopyOnWriteList, we don't have to worry about
+ // concurrency issues while iterating over the set of threads.
+ for (ConnectionThread connectionThread : mConnectionThreads.values()) {
+ connectionThread.close();
+ }
+ for (SimpleServerObserver observer : mObservers) {
+ removeObserver(observer);
+ }
+ }
+}