diff options
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc')
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); + } + } +} |