aboutsummaryrefslogtreecommitdiff
path: root/third_party/sl4a/src
diff options
context:
space:
mode:
authorAlexander Dorokhine <adorokhine@google.com>2016-11-09 18:34:39 -0800
committerAlexander Dorokhine <adorokhine@google.com>2016-11-09 19:07:34 -0800
commit146e4a6479ec8e841fb8fc56c7771c3ec75c8b0b (patch)
treeb6d497a7baea1cb4ca4808aa7369fcd387c92c60 /third_party/sl4a/src
parent30e605fb891e2d2f16c4cb356acdca1b5fda9765 (diff)
downloadmobly-snippet-lib-146e4a6479ec8e841fb8fc56c7771c3ec75c8b0b.tar.gz
Refactor SL4A to act as an RPC library.
All facade code has been removed and the project has been moved around into a single source tree.
Diffstat (limited to 'third_party/sl4a/src')
-rw-r--r--third_party/sl4a/src/main/AndroidManifest.xml15
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Constants.java29
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/FacadeManager.java70
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/ReflectionFacadeManagerFactory.java98
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java64
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java63
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Converter.java28
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java207
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java57
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java123
-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.java596
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/ParameterDescriptor.java41
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java43
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java38
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDeprecated.java37
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java26
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java32
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcName.java35
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java37
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcParameter.java44
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiver.java22
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManager.java97
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManagerFactory.java25
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcStartEvent.java35
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java367
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/ForegroundService.java41
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/SnippetService.java119
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java200
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java182
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java67
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java36
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java34
33 files changed, 2932 insertions, 0 deletions
diff --git a/third_party/sl4a/src/main/AndroidManifest.xml b/third_party/sl4a/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6c42f77
--- /dev/null
+++ b/third_party/sl4a/src/main/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet">
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <application>
+ <service
+ android:name=".service.SnippetService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.google.android.mobly.snippet.action.LAUNCH_SERVER"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Constants.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Constants.java
new file mode 100644
index 0000000..898975d
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Constants.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;
+
+import android.content.ComponentName;
+
+public interface Constants {
+ public static final String ACTION_KILL =
+ "com.google.android.mobly.snippet.action.KILL";
+ public static final String ACTION_LAUNCH_SERVER =
+ "com.google.android.mobly.snippet.action.LAUNCH_SERVER";
+
+ public static final String EXTRA_SERVICE_PORT =
+ "com.google.android.mobly.snippet.extra.SERVICE_PORT";
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/FacadeManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/FacadeManager.java
new file mode 100644
index 0000000..5a0e1ad
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/FacadeManager.java
@@ -0,0 +1,70 @@
+/*
+ * 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.facade;
+
+import android.app.Service;
+import android.content.Context;
+
+import com.google.android.mobly.snippet.rpc.RpcDeprecated;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.rpc.RpcReceiver;
+import com.google.android.mobly.snippet.rpc.RpcReceiverManager;
+import com.google.android.mobly.snippet.util.SnippetLibException;
+import com.google.android.mobly.snippet.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+public class FacadeManager extends RpcReceiverManager {
+
+ private final Context mContext;
+ private int mSdkLevel;
+
+ public FacadeManager(int sdkLevel, Context context,
+ Collection<Class<? extends RpcReceiver>> classList) {
+ super(context, classList);
+ mSdkLevel = sdkLevel;
+ mContext = context;
+ }
+
+ @Override
+ public Object invoke(Class<? extends RpcReceiver> clazz, Method method, Object[] args)
+ throws Exception {
+ try {
+ if (method.isAnnotationPresent(RpcDeprecated.class)) {
+ String replacedBy = method.getAnnotation(RpcDeprecated.class).value();
+ String title = method.getName() + " is deprecated";
+ Log.notify(mContext, title, title, String.format("Please use %s instead.", replacedBy));
+ } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
+ int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+ if (mSdkLevel < requiredSdkLevel) {
+ throw new SnippetLibException(
+ String.format("%s requires API level %d, current level is %d",
+ method.getName(), requiredSdkLevel, mSdkLevel));
+ }
+ }
+ return super.invoke(clazz, method, args);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof SecurityException) {
+ Log.notify(mContext, "RPC invoke failed...", mContext.getPackageName(), e.getCause()
+ .getMessage());
+ }
+ throw e;
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/ReflectionFacadeManagerFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/ReflectionFacadeManagerFactory.java
new file mode 100644
index 0000000..5b87107
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/facade/ReflectionFacadeManagerFactory.java
@@ -0,0 +1,98 @@
+/*
+ * 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.facade;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.google.android.mobly.snippet.rpc.RpcReceiver;
+import com.google.android.mobly.snippet.rpc.RpcReceiverManager;
+import com.google.android.mobly.snippet.rpc.RpcReceiverManagerFactory;
+import com.google.android.mobly.snippet.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class ReflectionFacadeManagerFactory implements RpcReceiverManagerFactory {
+ private static final String METADATA_TAG_NAME = "mobly-snippets";
+
+ private final Context mContext;
+ private final Set<Class<? extends RpcReceiver>> mClasses;
+ private final Map<Integer, RpcReceiverManager> mFacadeManagers;
+
+ public ReflectionFacadeManagerFactory(Context context) {
+ mContext = context;
+ mClasses = loadRpcReceivers();
+ mFacadeManagers = new HashMap<>();
+ }
+
+ @Override
+ public FacadeManager create(Integer UID) {
+ int sdkLevel = Build.VERSION.SDK_INT;
+ FacadeManager facadeManager = new FacadeManager(sdkLevel, mContext, mClasses);
+ mFacadeManagers.put(UID, facadeManager);
+ return facadeManager;
+ }
+
+ @Override
+ public Map<Integer, RpcReceiverManager> getRpcReceiverManagers() {
+ return Collections.unmodifiableMap(mFacadeManagers);
+ }
+
+ private Set<Class<? extends RpcReceiver>> loadRpcReceivers() {
+ ApplicationInfo appInfo;
+ try {
+ appInfo = mContext
+ .getPackageManager()
+ .getApplicationInfo(mContext.getPackageName(), PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalStateException(
+ "Failed to find ApplicationInfo with package name: "
+ + mContext.getPackageName());
+ }
+ Bundle metadata = appInfo.metaData;
+ String snippets = metadata.getString(METADATA_TAG_NAME);
+ if (snippets == null) {
+ throw new IllegalStateException(
+ "AndroidManifest.xml does not contain a <metadata> tag with "
+ + "name=\"" + METADATA_TAG_NAME + "\"");
+ }
+ String[] snippetClassNames = snippets.split("\\s*,\\s*");
+ Set<Class<? extends RpcReceiver>> receiverSet = new HashSet<>();
+ for (String snippetClassName : snippetClassNames) {
+ try {
+ Log.i("Trying to load RpcReceiver class: " + snippetClassName);
+ Class<?> snippetClass = Class.forName(snippetClassName);
+ receiverSet.add((Class<? extends RpcReceiver>) snippetClass);
+ } catch (ClassNotFoundException e) {
+ Log.e("Failed to find class " + snippetClassName);
+ throw new RuntimeException(e);
+ }
+ }
+ if (receiverSet.isEmpty()) {
+ throw new IllegalStateException("Found no subclasses of RpcReceiver.");
+ }
+ return receiverSet;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java
new file mode 100644
index 0000000..9533f26
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java
@@ -0,0 +1,64 @@
+/*
+ * 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.future;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * FutureResult represents an eventual execution result for asynchronous operations.
+ *
+ */
+public class FutureResult<T> implements Future<T> {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private volatile T mResult = null;
+
+ public void set(T result) {
+ mResult = result;
+ mLatch.countDown();
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public T get() throws InterruptedException {
+ mLatch.await();
+ return mResult;
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException {
+ mLatch.await(timeout, unit);
+ return mResult;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return mResult != null;
+ }
+
+}
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..eb0d82c
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java
@@ -0,0 +1,63 @@
+/*
+ * 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.app.Service;
+import android.content.Context;
+import android.content.Intent;
+
+import com.google.android.mobly.snippet.facade.ReflectionFacadeManagerFactory;
+
+import java.net.InetSocketAddress;
+import java.util.UUID;
+
+public class AndroidProxy {
+
+ private InetSocketAddress mAddress;
+ private final JsonRpcServer mJsonRpcServer;
+ private final UUID mSecret;
+ private final RpcReceiverManagerFactory mFacadeManagerFactory;
+
+ /**
+ *
+ * @param context
+ * Android context (required to build facades).
+ * @param requiresHandshake
+ * indicates whether RPC security protocol should be enabled.
+ */
+ public AndroidProxy(Context context, boolean requiresHandshake) {
+ if (requiresHandshake) {
+ mSecret = UUID.randomUUID();
+ } else {
+ mSecret = null;
+ }
+ mFacadeManagerFactory = new ReflectionFacadeManagerFactory(context);
+ mJsonRpcServer = new JsonRpcServer(mFacadeManagerFactory, getSecret());
+ }
+
+ public InetSocketAddress startLocal(int port) {
+ mAddress = mJsonRpcServer.startLocal(port);
+ return mAddress;
+ }
+
+ private String getSecret() {
+ if (mSecret == null) {
+ return null;
+ }
+ return mSecret.toString();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Converter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Converter.java
new file mode 100644
index 0000000..8de44c1
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Converter.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * A converter can take a String and turn it into an instance of type T (the type parameter to the
+ * converter).
+ *
+ */
+public interface Converter<T> {
+
+ /** Convert a string into type T. */
+ T convert(String value);
+}
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..6c922b4
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java
@@ -0,0 +1,207 @@
+/*
+ * 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.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;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+public class 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<Object>((Set<?>) data);
+ return buildJsonList(items);
+ }
+ if (data instanceof Collection<?>) {
+ List<Object> items = new ArrayList<Object>((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();
+ }
+ 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);
+ }
+
+ 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();
+ 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;
+ }
+
+ private static JSONObject buildUri(Uri uri) throws JSONException {
+ return new JSONObject().put("Uri", build((uri != null) ? uri.toString() : ""));
+ }
+
+ private JsonBuilder() {
+ // This is a utility class.
+ }
+}
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..0c1d0f2
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+/**
+ * Represents a JSON RPC result.
+ *
+ * @see http://json-rpc.org/wiki/specification
+ *
+ */
+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("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("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject error(int id, Throwable t) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JSONObject.NULL);
+ json.put("error", t.toString());
+ return json;
+ }
+}
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..cde1303
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java
@@ -0,0 +1,123 @@
+/*
+ * 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.PrintWriter;
+import java.net.Socket;
+import java.util.Map;
+
+import org.json.JSONArray;
+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 final RpcReceiverManagerFactory mRpcReceiverManagerFactory;
+
+ // private final String mHandshake;
+
+ /**
+ * Construct a {@link JsonRpcServer} connected to the provided {@link RpcReceiverManager}.
+ *
+ * @param managerFactory the {@link RpcReceiverManager} to register with the server
+ * @param handshake the secret handshake required for authorization to use this server
+ */
+ public JsonRpcServer(RpcReceiverManagerFactory managerFactory, String handshake) {
+ // mHandshake = handshake;
+ mRpcReceiverManagerFactory = managerFactory;
+ }
+
+ @Override
+ public void shutdown() {
+ super.shutdown();
+ // Notify all RPC receiving objects. They may have to clean up some of their state.
+ for (RpcReceiverManager manager : mRpcReceiverManagerFactory.getRpcReceiverManagers()
+ .values()) {
+ manager.shutdown();
+ }
+ }
+
+ @Override
+ protected void handleRPCConnection(Socket sock, Integer UID, BufferedReader reader,
+ PrintWriter writer) throws Exception {
+ RpcReceiverManager receiverManager = null;
+ Map<Integer, RpcReceiverManager> mgrs = mRpcReceiverManagerFactory.getRpcReceiverManagers();
+ synchronized (mgrs) {
+ Log.d("UID " + UID);
+ Log.d("manager map keys: "
+ + mRpcReceiverManagerFactory.getRpcReceiverManagers().keySet());
+ if (mgrs.containsKey(UID)) {
+ Log.d("Look up existing session");
+ receiverManager = mgrs.get(UID);
+ } else {
+ Log.d("Create a new session");
+ receiverManager = mRpcReceiverManagerFactory.create(UID);
+ }
+ }
+ // boolean passedAuthentication = false;
+ 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");
+
+ MethodDescriptor rpc = receiverManager.getMethodDescriptor(method);
+ if (rpc == null) {
+ send(writer, JsonRpcResult.error(id, new RpcError("Unknown RPC: " + method)), UID);
+ continue;
+ }
+ try {
+ send(writer, JsonRpcResult.result(id, rpc.invoke(receiverManager, params)), UID);
+ } catch (Throwable t) {
+ Log.e("Invocation error.", t);
+ send(writer, JsonRpcResult.error(id, t), UID);
+ }
+ if (method.equals(CMD_CLOSE_SESSION)) {
+ Log.d("Got shutdown signal");
+ synchronized (writer) {
+ receiverManager.shutdown();
+ reader.close();
+ writer.close();
+ sock.close();
+ shutdown();
+ mgrs.remove(UID);
+ }
+ return;
+ }
+ }
+ }
+
+ 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..bffd13d
--- /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 {
+ public 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..7463480
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java
@@ -0,0 +1,596 @@
+/*
+ * 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 android.os.Bundle;
+import android.os.Parcelable;
+
+import com.google.android.mobly.snippet.util.AndroidUtil;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * An adapter that wraps {@code Method}.
+ *
+ */
+public final class MethodDescriptor {
+ private static final Map<Class<?>, Converter<?>> sConverters = populateConverters();
+
+ private final Method mMethod;
+ private final Class<? extends RpcReceiver> mClass;
+
+ public MethodDescriptor(Class<? extends RpcReceiver> 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 RpcReceiver> clazz) {
+ List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>();
+ for (Method method : clazz.getMethods()) {
+ if (method.isAnnotationPresent(Rpc.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
+ */
+ public Object invoke(RpcReceiverManager manager, final JSONArray parameters) throws Throwable {
+
+ final Type[] parameterTypes = getGenericParameterTypes();
+ final Object[] args = new Object[parameterTypes.length];
+ final Annotation annotations[][] = getParameterAnnotations();
+
+ 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 if (MethodDescriptor.hasDefaultValue(annotations[i])) {
+ args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
+ } else {
+ throw new RpcError("Argument " + (i + 1) + " is not present");
+ }
+ }
+
+ return invoke(manager, args);
+ }
+
+ /**
+ * Invokes the call that belongs to this object with the given parameters. Wraps the response
+ * (possibly an exception) in a JSONObject.
+ *
+ * @param parameters {@code Bundle} containing the parameters
+ * @return result
+ * @throws Throwable
+ */
+ public Object invoke(RpcReceiverManager manager, final Bundle parameters) throws Throwable {
+ final Annotation annotations[][] = getParameterAnnotations();
+ final Class<?>[] parameterTypes = getMethod().getParameterTypes();
+ final Object[] args = new Object[parameterTypes.length];
+
+ for (int i = 0; i < parameterTypes.length; i++) {
+ Class<?> parameterType = parameterTypes[i];
+ String parameterName = getName(annotations[i]);
+ if (i < parameterTypes.length) {
+ args[i] = convertParameter(parameters, parameterType, parameterName);
+ } else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
+ args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
+ } else {
+ throw new RpcError("Argument " + (i + 1) + " is not present");
+ }
+ }
+ return invoke(manager, args);
+ }
+
+ private Object invoke(RpcReceiverManager manager, Object[] args) throws Throwable{
+ Object result = null;
+ try {
+ result = manager.invoke(mClass, mMethod, args);
+ } catch (Throwable t) {
+ throw t.getCause();
+ }
+ return result;
+ }
+
+ /**
+ * Converts a parameter from JSON into a Java Object.
+ *
+ * @return TODO
+ */
+ // 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
+ static Object convertParameter(final JSONArray parameters, int index, Type type)
+ throws JSONException, RpcError {
+ try {
+ // Log.d("sl4a", parameters.toString());
+ // Log.d("sl4a", type.toString());
+ // 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) {
+ try {
+ return parameters.getBoolean(index);
+ } catch (JSONException e) {
+ return new Boolean(parameters.getInt(index) != 0);
+ }
+ } else if (type == Long.class) {
+ return parameters.getLong(index);
+ } else if (type == Double.class) {
+ return parameters.getDouble(index);
+ } else if (type == Integer.class) {
+ return parameters.getInt(index);
+ } else if (type == Intent.class) {
+ return buildIntent(parameters.getJSONObject(index));
+ } else if (type == Integer[].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 == 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 {
+ // 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() + ".");
+ }
+ }
+
+ private Object convertParameter(Bundle bundle, Class<?> type, String name) {
+ Object param = null;
+ if (type.isAssignableFrom(Boolean.class)) {
+ param = bundle.getBoolean(name, false);
+ }
+ if (type.isAssignableFrom(Boolean[].class)) {
+ param = bundle.getBooleanArray(name);
+ }
+ if (type.isAssignableFrom(String.class)) {
+ param = bundle.getString(name);
+ }
+ if (type.isAssignableFrom(String[].class)) {
+ param = bundle.getStringArray(name);
+ }
+ if (type.isAssignableFrom(Integer.class)) {
+ param = bundle.getInt(name, 0);
+ }
+ if (type.isAssignableFrom(Integer[].class)) {
+ param = bundle.getIntArray(name);
+ }
+ if (type.isAssignableFrom(Bundle.class)) {
+ param = bundle.getBundle(name);
+ }
+ if (type.isAssignableFrom(Parcelable.class)) {
+ param = bundle.getParcelable(name);
+ }
+ if (type.isAssignableFrom(Parcelable[].class)) {
+ param = bundle.getParcelableArray(name);
+ }
+ if (type.isAssignableFrom(Intent.class)) {
+ param = bundle.getParcelable(name);
+ }
+ return param;
+ }
+
+ public 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 Method getMethod() {
+ return mMethod;
+ }
+
+ public Class<? extends RpcReceiver> getDeclaringClass() {
+ return mClass;
+ }
+
+ public String getName() {
+ if (mMethod.isAnnotationPresent(RpcName.class)) {
+ return mMethod.getAnnotation(RpcName.class).name();
+ }
+ return mMethod.getName();
+ }
+
+ public Type[] getGenericParameterTypes() {
+ return mMethod.getGenericParameterTypes();
+ }
+
+ public Annotation[][] getParameterAnnotations() {
+ return mMethod.getParameterAnnotations();
+ }
+
+ /**
+ * Returns a human-readable help text for this RPC, based on annotations in the source code.
+ *
+ * @return derived help string
+ */
+ public String getHelp() {
+ StringBuilder helpBuilder = new StringBuilder();
+ Rpc rpcAnnotation = mMethod.getAnnotation(Rpc.class);
+
+ helpBuilder.append(mMethod.getName());
+ helpBuilder.append("(");
+ final Class<?>[] parameterTypes = mMethod.getParameterTypes();
+ final Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
+ final Annotation[][] annotations = mMethod.getParameterAnnotations();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (i == 0) {
+ helpBuilder.append("\n ");
+ } else {
+ helpBuilder.append(",\n ");
+ }
+
+ helpBuilder.append(getHelpForParameter(genericParameterTypes[i], annotations[i]));
+ }
+ helpBuilder.append(")\n\n");
+ helpBuilder.append(rpcAnnotation.description());
+ if (!rpcAnnotation.returns().equals("")) {
+ helpBuilder.append("\n");
+ helpBuilder.append("\nReturns:\n ");
+ helpBuilder.append(rpcAnnotation.returns());
+ }
+
+ if (mMethod.isAnnotationPresent(RpcStartEvent.class)) {
+ String eventName = mMethod.getAnnotation(RpcStartEvent.class).value();
+ helpBuilder.append(String.format("\n\nGenerates \"%s\" events.", eventName));
+ }
+
+ if (mMethod.isAnnotationPresent(RpcDeprecated.class)) {
+ String replacedBy = mMethod.getAnnotation(RpcDeprecated.class).value();
+ String release = mMethod.getAnnotation(RpcDeprecated.class).release();
+ helpBuilder.append(String.format("\n\nDeprecated in %s! Please use %s instead.", release,
+ replacedBy));
+ }
+
+ return helpBuilder.toString();
+ }
+
+ /**
+ * Returns the help string for one particular parameter. This respects optional parameters.
+ *
+ * @param parameterType
+ * (generic) type of the parameter
+ * @param annotations
+ * annotations of the parameter, may be null
+ * @return string describing the parameter based on source code annotations
+ */
+ private static String getHelpForParameter(Type parameterType, Annotation[] annotations) {
+ StringBuilder result = new StringBuilder();
+
+ appendTypeName(result, parameterType);
+ result.append(" ");
+ result.append(getName(annotations));
+ if (hasDefaultValue(annotations)) {
+ result.append("[optional");
+ if (hasExplicitDefaultValue(annotations)) {
+ result.append(", default " + getDefaultValue(parameterType, annotations));
+ }
+ result.append("]");
+ }
+
+ String description = getDescription(annotations);
+ if (description.length() > 0) {
+ result.append(": ");
+ result.append(description);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Appends the name of the given type to the {@link StringBuilder}.
+ *
+ * @param builder
+ * string builder to append to
+ * @param type
+ * type whose name to append
+ */
+ private static void appendTypeName(final StringBuilder builder, final Type type) {
+ if (type instanceof Class<?>) {
+ builder.append(((Class<?>) type).getSimpleName());
+ } else {
+ ParameterizedType parametrizedType = (ParameterizedType) type;
+ builder.append(((Class<?>) parametrizedType.getRawType()).getSimpleName());
+ builder.append("<");
+
+ Type[] arguments = parametrizedType.getActualTypeArguments();
+ for (int i = 0; i < arguments.length; i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ appendTypeName(builder, arguments[i]);
+ }
+ builder.append(">");
+ }
+ }
+
+ /**
+ * Returns parameter descriptors suitable for the RPC call text representation.
+ *
+ * <p>
+ * Uses parameter value, default value or name, whatever is available first.
+ *
+ * @return an array of parameter descriptors
+ */
+ public ParameterDescriptor[] getParameterValues(String[] values) {
+ Type[] parameterTypes = mMethod.getGenericParameterTypes();
+ Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
+ ParameterDescriptor[] parameters = new ParameterDescriptor[parametersAnnotations.length];
+ for (int index = 0; index < parameters.length; index++) {
+ String value;
+ if (index < values.length) {
+ value = values[index];
+ } else if (hasDefaultValue(parametersAnnotations[index])) {
+ Object defaultValue = getDefaultValue(parameterTypes[index], parametersAnnotations[index]);
+ if (defaultValue == null) {
+ value = null;
+ } else {
+ value = String.valueOf(defaultValue);
+ }
+ } else {
+ value = getName(parametersAnnotations[index]);
+ }
+ parameters[index] = new ParameterDescriptor(value, parameterTypes[index]);
+ }
+ return parameters;
+ }
+
+ /**
+ * Returns parameter hints.
+ *
+ * @return an array of parameter hints
+ */
+ public String[] getParameterHints() {
+ Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
+ String[] hints = new String[parametersAnnotations.length];
+ for (int index = 0; index < hints.length; index++) {
+ String name = getName(parametersAnnotations[index]);
+ String description = getDescription(parametersAnnotations[index]);
+ String hint = "No paramenter description.";
+ if (!name.equals("") && !description.equals("")) {
+ hint = name + ": " + description;
+ } else if (!name.equals("")) {
+ hint = name;
+ } else if (!description.equals("")) {
+ hint = description;
+ }
+ hints[index] = hint;
+ }
+ return hints;
+ }
+
+ /**
+ * Extracts the formal parameter name from an annotation.
+ *
+ * @param annotations
+ * the annotations of the parameter
+ * @return the formal name of the parameter
+ */
+ private static String getName(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ if (a instanceof RpcParameter) {
+ return ((RpcParameter) a).name();
+ }
+ }
+ throw new IllegalStateException("No parameter name");
+ }
+
+ /**
+ * Extracts the parameter description from its annotations.
+ *
+ * @param annotations
+ * the annotations of the parameter
+ * @return the description of the parameter
+ */
+ private static String getDescription(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ if (a instanceof RpcParameter) {
+ return ((RpcParameter) a).description();
+ }
+ }
+ throw new IllegalStateException("No parameter description");
+ }
+
+ /**
+ * Returns the default value for a specific parameter.
+ *
+ * @param parameterType
+ * parameterType
+ * @param annotations
+ * annotations of the parameter
+ */
+ public static Object getDefaultValue(Type parameterType, Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ if (a instanceof RpcDefault) {
+ RpcDefault defaultAnnotation = (RpcDefault) a;
+ Converter<?> converter = converterFor(parameterType, defaultAnnotation.converter());
+ return converter.convert(defaultAnnotation.value());
+ } else if (a instanceof RpcOptional) {
+ return null;
+ }
+ }
+ throw new IllegalStateException("No default value for " + parameterType);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static Converter<?> converterFor(Type parameterType,
+ Class<? extends Converter> converterClass) {
+ if (converterClass == Converter.class) {
+ Converter<?> converter = sConverters.get(parameterType);
+ if (converter == null) {
+ throw new IllegalArgumentException("No predefined converter found for " + parameterType);
+ }
+ return converter;
+ }
+ try {
+ Constructor<?> constructor = converterClass.getConstructor(new Class<?>[0]);
+ return (Converter<?>) constructor.newInstance(new Object[0]);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot create converter from "
+ + converterClass.getCanonicalName());
+ }
+ }
+
+ /**
+ * Determines whether or not this parameter has default value.
+ *
+ * @param annotations
+ * annotations of the parameter
+ */
+ public static boolean hasDefaultValue(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ if (a instanceof RpcDefault || a instanceof RpcOptional) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the default value is specified for a specific parameter.
+ *
+ * @param annotations
+ * annotations of the parameter
+ */
+ //@VisibleForTesting
+ static boolean hasExplicitDefaultValue(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ if (a instanceof RpcDefault) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns the converters for {@code String}, {@code Integer} and {@code Boolean}. */
+ private static Map<Class<?>, Converter<?>> populateConverters() {
+ Map<Class<?>, Converter<?>> converters = new HashMap<Class<?>, Converter<?>>();
+ converters.put(String.class, new Converter<String>() {
+ @Override
+ public String convert(String value) {
+ return value;
+ }
+ });
+ converters.put(Integer.class, new Converter<Integer>() {
+ @Override
+ public Integer convert(String input) {
+ try {
+ return Integer.decode(input);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("'" + input + "' is not an integer");
+ }
+ }
+ });
+ converters.put(Boolean.class, new Converter<Boolean>() {
+ @Override
+ public Boolean convert(String input) {
+ if (input == null) {
+ return null;
+ }
+ input = input.toLowerCase();
+ if (input.equals("true")) {
+ return Boolean.TRUE;
+ }
+ if (input.equals("false")) {
+ return Boolean.FALSE;
+ }
+ throw new IllegalArgumentException("'" + input + "' is not a boolean");
+ }
+ });
+ return converters;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/ParameterDescriptor.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/ParameterDescriptor.java
new file mode 100644
index 0000000..55e0151
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/ParameterDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * 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.reflect.Type;
+
+/**
+ * RPC parameter description.
+ *
+ */
+public final class ParameterDescriptor {
+ private final String value;
+ private final Type type;
+
+ public ParameterDescriptor(String value, Type type) {
+ this.value = value;
+ this.type = type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Type getType() {
+ return type;
+ }
+}
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..31a5364
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java
@@ -0,0 +1,43 @@
+/*
+ * 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();
+
+ /**
+ * Gives a brief description of the functions return value (and the underlying data structure).
+ */
+ String returns() default "";
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java
new file mode 100644
index 0000000..bdf1d28
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Use this annotation to mark an RPC parameter that have a default value.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcDefault {
+ /** The default value of the RPC parameter. */
+ public String value();
+
+ @SuppressWarnings("rawtypes")
+ public Class<? extends Converter> converter() default Converter.class;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDeprecated.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDeprecated.java
new file mode 100644
index 0000000..bbf6c6c
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDeprecated.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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark RPC method as deprecated.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcDeprecated {
+ /** The method that replaced this one. */
+ public String value();
+
+ /** Release of SL4A when deprecation occurred. */
+ public String release() default "r4";
+}
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..0e49b87
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java
@@ -0,0 +1,26 @@
+/*
+ * 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..c7e6563
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java
@@ -0,0 +1,32 @@
+/*
+ * 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. */
+ public int value();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcName.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcName.java
new file mode 100644
index 0000000..02052ea
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcName.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Use this annotation to mark an RPC parameter that have a default value.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcName {
+ /** The default value of the RPC parameter. */
+ public String name();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java
new file mode 100644
index 0000000..4e6125b
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark RPC parameter as optional.
+ *
+ * <p>
+ * The parameter marked as optional has no explicit default value. {@code null} is used as default
+ * value.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcOptional {
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcParameter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcParameter.java
new file mode 100644
index 0000000..4752164
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcParameter.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * An annotation that is used to document the parameters of an RPC.
+ *
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcParameter {
+ /**
+ * The name of the formal parameter. This should be in agreement with the java code.
+ */
+ public String name();
+
+ /**
+ * Description of the RPC. This should be a short descriptive statement without a full stop, such
+ * as 'disables the WiFi mode'.
+ */
+ public String description() default "";
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiver.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiver.java
new file mode 100644
index 0000000..448aab8
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiver.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+public interface RpcReceiver {
+ /** Invoked when the receiver is shut down. */
+ void shutdown();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManager.java
new file mode 100644
index 0000000..6661ac6
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManager.java
@@ -0,0 +1,97 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.android.mobly.snippet.util.Log;
+
+public abstract class RpcReceiverManager {
+
+ private final Context mContext;
+ private final Map<Class<? extends RpcReceiver>, RpcReceiver> mReceivers;
+
+ /**
+ * A map of strings to known RPCs.
+ */
+ private final Map<String, MethodDescriptor> mKnownRpcs = new HashMap<String, MethodDescriptor>();
+
+ public RpcReceiverManager(
+ Context context, Collection<Class<? extends RpcReceiver>> classList) {
+ mContext = context;
+ mReceivers = new HashMap<>();
+ for (Class<? extends RpcReceiver> receiverClass : classList) {
+ mReceivers.put(receiverClass, null);
+ Collection<MethodDescriptor> methodList = MethodDescriptor.collectFrom(receiverClass);
+ for (MethodDescriptor m : methodList) {
+ if (mKnownRpcs.containsKey(m.getName())) {
+ // We already know an RPC of the same name. We don't catch this anywhere because
+ // this is a programming error.
+ throw new RuntimeException("An RPC with the name " + m.getName()
+ + " is already known.");
+ }
+ mKnownRpcs.put(m.getName(), m);
+ }
+ }
+ }
+
+ private RpcReceiver get(Class<? extends RpcReceiver> clazz) {
+ RpcReceiver object = mReceivers.get(clazz);
+ if (object != null) {
+ return object;
+ }
+
+ Constructor<? extends RpcReceiver> constructor;
+ try {
+ constructor = clazz.getConstructor(Context.class);
+ object = constructor.newInstance(mContext);
+ mReceivers.put(clazz, object);
+ } catch (Exception e) {
+ Log.e(e);
+ }
+
+ return object;
+ }
+
+ public MethodDescriptor getMethodDescriptor(String methodName) {
+ return mKnownRpcs.get(methodName);
+ }
+
+ public Object invoke(Class<? extends RpcReceiver> clazz, Method method, Object[] args)
+ throws Exception {
+ RpcReceiver object = get(clazz);
+ return method.invoke(object, args);
+ }
+
+ public void shutdown() {
+ for (RpcReceiver receiver : mReceivers.values()) {
+ try {
+ if (receiver != null) {
+ receiver.shutdown();
+ }
+ } catch (Exception e) {
+ Log.e("Failed to shut down an RpcReceiver", e);
+ }
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManagerFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManagerFactory.java
new file mode 100644
index 0000000..10135e0
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcReceiverManagerFactory.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;
+
+import java.util.Map;
+
+public interface RpcReceiverManagerFactory {
+ public RpcReceiverManager create(Integer UID);
+
+ public Map<Integer, RpcReceiverManager> getRpcReceiverManagers();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcStartEvent.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcStartEvent.java
new file mode 100644
index 0000000..f23a6a2
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcStartEvent.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Use this annotation to mark an RPC as one that starts generating events.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcStartEvent {
+ /** The name of the event that is generated. */
+ public String value();
+}
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..23a1e89
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java
@@ -0,0 +1,367 @@
+/*
+ * 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 org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.BindException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+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;
+
+/**
+ * A simple server.
+ */
+public abstract class SimpleServer {
+ private static int threadIndex = 0;
+ private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads =
+ new ConcurrentHashMap<Integer, ConnectionThread>();
+ private final List<SimpleServerObserver> mObservers = new ArrayList<>();
+ private volatile boolean mStopServer = false;
+ private ServerSocket mServer;
+ private Thread mServerThread;
+
+ public interface SimpleServerObserver {
+ public void onConnect();
+ public 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);
+ }
+ }
+ }
+ }
+
+ /** Returns the number of active connections to this server. */
+ public int getNumberOfConnections() {
+ return mConnectionThreads.size();
+ }
+
+ public static 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.
+ }
+
+ public static InetAddress getPublicInetAddress() 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) {
+ 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
+ *
+ * @return the port that the server is bound to
+ * @throws IOException
+ */
+ public InetSocketAddress startLocal(int port) {
+ InetAddress address;
+ try {
+ // address = InetAddress.getLocalHost();
+ address = getPrivateInetAddress();
+ mServer = new ServerSocket(port, 5, address);
+ } catch (BindException e) {
+ Log.e("Port " + port + " already in use.");
+ try {
+ address = getPrivateInetAddress();
+ mServer = new ServerSocket(0, 5, address);
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ return null;
+ }
+ } catch (Exception e) {
+ Log.e("Failed to start server.", e);
+ return null;
+ }
+ int boundPort = start();
+ return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+ }
+
+ /**
+ * data Starts the RPC server bound to the public facing address.
+ *
+ * @param port
+ * the port to bind to or 0 to pick any unused port
+ *
+ * @return the port that the server is bound to
+ */
+ public InetSocketAddress startPublic(int port) {
+ InetAddress address;
+ try {
+ // address = getPublicInetAddress();
+ address = null;
+ mServer = new ServerSocket(port, 5 /* backlog */, address);
+ } catch (Exception e) {
+ Log.e("Failed to start server.", e);
+ return null;
+ }
+ int boundPort = start();
+ return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+ }
+
+ /**
+ * data Starts the RPC server bound to all interfaces
+ *
+ * @param port
+ * the port to bind to or 0 to pick any unused port
+ *
+ * @return the port that the server is bound to
+ */
+ public InetSocketAddress startAllInterfaces(int port) {
+ try {
+ mServer = new ServerSocket(port, 5 /* backlog */);
+ } catch (Exception e) {
+ Log.e("Failed to start server.", e);
+ return null;
+ }
+ int boundPort = start();
+ return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+ }
+
+ private int 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());
+ return mServer.getLocalPort();
+ }
+
+ 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() {
+ // 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);
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/ForegroundService.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/ForegroundService.java
new file mode 100644
index 0000000..4681fb3
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/ForegroundService.java
@@ -0,0 +1,41 @@
+/*
+ * 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.service;
+
+import android.app.Notification;
+import android.app.Service;
+
+public abstract class ForegroundService extends Service {
+ private final int mNotificationId;
+
+ public ForegroundService(int id) {
+ mNotificationId = id;
+ }
+
+ protected abstract Notification createNotification();
+
+ @Override
+ public void onCreate() {
+ startForeground(mNotificationId, createNotification());
+ }
+
+ @Override
+ public void onDestroy() {
+ // Make sure our notification is gone.
+ stopForeground(true /* removeNotification */);
+ }
+} \ No newline at end of file
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/SnippetService.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/SnippetService.java
new file mode 100644
index 0000000..a53eca6
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/service/SnippetService.java
@@ -0,0 +1,119 @@
+/*
+ * 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.service;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.google.android.mobly.snippet.Constants;
+import com.google.android.mobly.snippet.rpc.AndroidProxy;
+import com.google.android.mobly.snippet.util.NotificationIdFactory;
+
+/**
+ * A service that allows scripts and the RPC server to run in the background.
+ *
+ */
+public class SnippetService extends ForegroundService {
+ private static final int NOTIFICATION_ID = NotificationIdFactory.create();
+
+ private final IBinder mBinder;
+ private NotificationManager mNotificationManager;
+ private Notification mNotification;
+
+ public class LocalBinder extends Binder {
+ public SnippetService getService() {
+ return SnippetService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public SnippetService() {
+ super(NOTIFICATION_ID);
+ mBinder = new LocalBinder();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ protected Notification createNotification() {
+ Notification.Builder builder = new Notification.Builder(this);
+ builder.setSmallIcon(android.R.drawable.btn_star)
+ .setTicker(null)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle("Snippet Service");
+ mNotification = builder.getNotification();
+ mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+ return mNotification;
+ }
+
+ private void updateNotification(String tickerText) {
+ if (tickerText.equals(mNotification.tickerText)) {
+ // Consequent notifications with the same ticker-text are displayed without any ticker-text.
+ // This is a way around. Alternatively, we can display process name and port.
+ tickerText = tickerText + " ";
+ }
+ Notification.Builder builder = new Notification.Builder(this);
+ builder.setContentTitle("Snippet Service")
+ .setWhen(mNotification.when)
+ .setTicker(tickerText);
+
+ mNotification = builder.getNotification();
+ mNotificationManager.notify(NOTIFICATION_ID, mNotification);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+ AndroidProxy proxy = null;
+ if (intent == null) {
+ return START_REDELIVER_INTENT;
+ } else if (intent.getAction().equals(Constants.ACTION_KILL)) {
+ stopSelf(startId);
+ return START_REDELIVER_INTENT;
+ } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_SERVER)) {
+ proxy = launchServer(intent);
+ } else {
+ updateNotification("Action not implemented: " + intent.getAction());
+ }
+ return START_REDELIVER_INTENT;
+ }
+
+ private AndroidProxy launchServer(Intent intent) {
+ AndroidProxy androidProxy = new AndroidProxy(this, false /* requiresHandshake */);
+ int servicePort = intent.getIntExtra(Constants.EXTRA_SERVICE_PORT, 0);
+ if (servicePort == 0) {
+ throw new IllegalArgumentException(
+ "Intent missing required extra: " + Constants.EXTRA_SERVICE_PORT);
+ }
+ if (androidProxy.startLocal(servicePort) == null) {
+ throw new RuntimeException("Failed to start server on port " + servicePort);
+ }
+ return androidProxy;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java
new file mode 100644
index 0000000..5f96647
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java
@@ -0,0 +1,200 @@
+/*
+ * 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.util;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public final class AndroidUtil {
+ private AndroidUtil() {}
+
+ // TODO(damonkohler): Pull this out into proper argument deserialization and support
+ // complex/nested types being passed in.
+ public static void putExtrasFromJsonObject(JSONObject extras,
+ Intent intent) throws JSONException {
+ JSONArray names = extras.names();
+ for (int i = 0; i < names.length(); i++) {
+ String name = names.getString(i);
+ Object data = extras.get(name);
+ if (data == null) {
+ continue;
+ }
+ if (data instanceof Integer) {
+ intent.putExtra(name, (Integer) data);
+ }
+ if (data instanceof Float) {
+ intent.putExtra(name, (Float) data);
+ }
+ if (data instanceof Double) {
+ intent.putExtra(name, (Double) data);
+ }
+ if (data instanceof Long) {
+ intent.putExtra(name, (Long) data);
+ }
+ if (data instanceof String) {
+ intent.putExtra(name, (String) data);
+ }
+ if (data instanceof Boolean) {
+ intent.putExtra(name, (Boolean) data);
+ }
+ // Nested JSONObject
+ if (data instanceof JSONObject) {
+ Bundle nestedBundle = new Bundle();
+ intent.putExtra(name, nestedBundle);
+ putNestedJSONObject((JSONObject) data, nestedBundle);
+ }
+ // Nested JSONArray. Doesn't support mixed types in single array
+ if (data instanceof JSONArray) {
+ // Empty array. No way to tell what type of data to pass on, so skipping
+ if (((JSONArray) data).length() == 0) {
+ Log.e("Empty array not supported in JSONObject, skipping");
+ continue;
+ }
+ // Integer
+ if (((JSONArray) data).get(0) instanceof Integer) {
+ Integer[] integerArrayData = new Integer[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ integerArrayData[j] = ((JSONArray) data).getInt(j);
+ }
+ intent.putExtra(name, integerArrayData);
+ }
+ // Double
+ if (((JSONArray) data).get(0) instanceof Double) {
+ Double[] doubleArrayData = new Double[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ doubleArrayData[j] = ((JSONArray) data).getDouble(j);
+ }
+ intent.putExtra(name, doubleArrayData);
+ }
+ // Long
+ if (((JSONArray) data).get(0) instanceof Long) {
+ Long[] longArrayData = new Long[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ longArrayData[j] = ((JSONArray) data).getLong(j);
+ }
+ intent.putExtra(name, longArrayData);
+ }
+ // String
+ if (((JSONArray) data).get(0) instanceof String) {
+ String[] stringArrayData = new String[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ stringArrayData[j] = ((JSONArray) data).getString(j);
+ }
+ intent.putExtra(name, stringArrayData);
+ }
+ // Boolean
+ if (((JSONArray) data).get(0) instanceof Boolean) {
+ Boolean[] booleanArrayData = new Boolean[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
+ }
+ intent.putExtra(name, booleanArrayData);
+ }
+ }
+ }
+ }
+
+ // Contributed by Emmanuel T
+ // Nested Array handling contributed by Sergey Zelenev
+ private static void putNestedJSONObject(JSONObject jsonObject, Bundle bundle)
+ throws JSONException {
+ JSONArray names = jsonObject.names();
+ for (int i = 0; i < names.length(); i++) {
+ String name = names.getString(i);
+ Object data = jsonObject.get(name);
+ if (data == null) {
+ continue;
+ }
+ if (data instanceof Integer) {
+ bundle.putInt(name, ((Integer) data).intValue());
+ }
+ if (data instanceof Float) {
+ bundle.putFloat(name, ((Float) data).floatValue());
+ }
+ if (data instanceof Double) {
+ bundle.putDouble(name, ((Double) data).doubleValue());
+ }
+ if (data instanceof Long) {
+ bundle.putLong(name, ((Long) data).longValue());
+ }
+ if (data instanceof String) {
+ bundle.putString(name, (String) data);
+ }
+ if (data instanceof Boolean) {
+ bundle.putBoolean(name, ((Boolean) data).booleanValue());
+ }
+ // Nested JSONObject
+ if (data instanceof JSONObject) {
+ Bundle nestedBundle = new Bundle();
+ bundle.putBundle(name, nestedBundle);
+ putNestedJSONObject((JSONObject) data, nestedBundle);
+ }
+ // Nested JSONArray. Doesn't support mixed types in single array
+ if (data instanceof JSONArray) {
+ // Empty array. No way to tell what type of data to pass on, so skipping
+ if (((JSONArray) data).length() == 0) {
+ Log.e("Empty array not supported in nested JSONObject, skipping");
+ continue;
+ }
+ // Integer
+ if (((JSONArray) data).get(0) instanceof Integer) {
+ int[] integerArrayData = new int[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ integerArrayData[j] = ((JSONArray) data).getInt(j);
+ }
+ bundle.putIntArray(name, integerArrayData);
+ }
+ // Double
+ if (((JSONArray) data).get(0) instanceof Double) {
+ double[] doubleArrayData = new double[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ doubleArrayData[j] = ((JSONArray) data).getDouble(j);
+ }
+ bundle.putDoubleArray(name, doubleArrayData);
+ }
+ // Long
+ if (((JSONArray) data).get(0) instanceof Long) {
+ long[] longArrayData = new long[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ longArrayData[j] = ((JSONArray) data).getLong(j);
+ }
+ bundle.putLongArray(name, longArrayData);
+ }
+ // String
+ if (((JSONArray) data).get(0) instanceof String) {
+ String[] stringArrayData = new String[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ stringArrayData[j] = ((JSONArray) data).getString(j);
+ }
+ bundle.putStringArray(name, stringArrayData);
+ }
+ // Boolean
+ if (((JSONArray) data).get(0) instanceof Boolean) {
+ boolean[] booleanArrayData = new boolean[((JSONArray) data).length()];
+ for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+ booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
+ }
+ bundle.putBooleanArray(name, booleanArrayData);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java
new file mode 100644
index 0000000..bb1ce10
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java
@@ -0,0 +1,182 @@
+/*
+ * 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.util;
+
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Toast;
+
+public class Log {
+ private Log() {
+ // Utility class.
+ }
+
+ private static String getTag() {
+ StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+ String fullClassName = stackTraceElements[4].getClassName();
+ String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1);
+ int lineNumber = stackTraceElements[4].getLineNumber();
+ return "sl4a." + className + ":" + lineNumber;
+ }
+
+ private static void toast(Context context, String message) {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+
+ public static void notify(Context context, String title, String contentTitle, String message) {
+ android.util.Log.v(getTag(), String.format("%s %s", contentTitle, message));
+
+ String packageName = context.getPackageName();
+ int iconId = context.getResources().getIdentifier("stat_sys_warning", "drawable", packageName);
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification.Builder builder = new Notification.Builder(context);
+ builder.setSmallIcon(iconId > 0 ? iconId : -1)
+ .setTicker(title)
+ .setWhen(0)
+ .setContentTitle(contentTitle)
+ .setContentText(message)
+ .setContentIntent(PendingIntent.getService(context, 0, null, 0));
+ Notification note = builder.getNotification();
+ note.contentView.getLayoutId();
+ notificationManager.notify(NotificationIdFactory.create(), note);
+ }
+
+ public static void showDialog(final Context context, final String title, final String message) {
+ android.util.Log.v(getTag(), String.format("%s %s", title, message));
+
+ MainThread.run(context, new Runnable() {
+ @Override
+ public void run() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(title);
+ builder.setMessage(message);
+
+ DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ };
+ builder.setPositiveButton("Ok", buttonListener);
+ builder.show();
+ }
+ });
+ }
+
+ public static void v(String message) {
+ android.util.Log.v(getTag(), message);
+ }
+
+ public static void v(String message, Throwable e) {
+ android.util.Log.v(getTag(), message, e);
+ }
+
+ public static void v(Context context, String message) {
+ toast(context, message);
+ android.util.Log.v(getTag(), message);
+ }
+
+ public static void v(Context context, String message, Throwable e) {
+ toast(context, message);
+ android.util.Log.v(getTag(), message, e);
+ }
+
+ public static void e(Throwable e) {
+ android.util.Log.e(getTag(), "Error", e);
+ }
+
+ public static void e(String message) {
+ android.util.Log.e(getTag(), message);
+ }
+
+ public static void e(String message, Throwable e) {
+ android.util.Log.e(getTag(), message, e);
+ }
+
+ public static void e(Context context, String message) {
+ toast(context, message);
+ android.util.Log.e(getTag(), message);
+ }
+
+ public static void e(Context context, String message, Throwable e) {
+ toast(context, message);
+ android.util.Log.e(getTag(), message, e);
+ }
+
+ public static void w(Throwable e) {
+ android.util.Log.w(getTag(), "Warning", e);
+ }
+
+ public static void w(String message) {
+ android.util.Log.w(getTag(), message);
+ }
+
+ public static void w(String message, Throwable e) {
+ android.util.Log.w(getTag(), message, e);
+ }
+
+ public static void w(Context context, String message) {
+ toast(context, message);
+ android.util.Log.w(getTag(), message);
+ }
+
+ public static void w(Context context, String message, Throwable e) {
+ toast(context, message);
+ android.util.Log.w(getTag(), message, e);
+ }
+
+ public static void d(String message) {
+ android.util.Log.d(getTag(), message);
+ }
+
+ public static void d(String message, Throwable e) {
+ android.util.Log.d(getTag(), message, e);
+ }
+
+ public static void d(Context context, String message) {
+ toast(context, message);
+ android.util.Log.d(getTag(), message);
+ }
+
+ public static void d(Context context, String message, Throwable e) {
+ toast(context, message);
+ android.util.Log.d(getTag(), message, e);
+ }
+
+ public static void i(String message) {
+ android.util.Log.i(getTag(), message);
+ }
+
+ public static void i(String message, Throwable e) {
+ android.util.Log.i(getTag(), message, e);
+ }
+
+ public static void i(Context context, String message) {
+ toast(context, message);
+ android.util.Log.i(getTag(), message);
+ }
+
+ public static void i(Context context, String message, Throwable e) {
+ toast(context, message);
+ android.util.Log.i(getTag(), message, e);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java
new file mode 100644
index 0000000..0e13e74
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.google.android.mobly.snippet.future.FutureResult;
+
+import java.util.concurrent.Callable;
+
+public class MainThread {
+
+ private MainThread() {
+ // Utility class.
+ }
+
+ /**
+ * Executed in the main thread, returns the result of an execution. Anything that runs here should
+ * finish quickly to avoid hanging the UI thread.
+ */
+ public static <T> T run(Context context, final Callable<T> task) {
+ final FutureResult<T> result = new FutureResult<T>();
+ Handler handler = new Handler(context.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ result.set(task.call());
+ } catch (Exception e) {
+ Log.e(e);
+ result.set(null);
+ }
+ }
+ });
+ try {
+ return result.get();
+ } catch (InterruptedException e) {
+ Log.e(e);
+ }
+ return null;
+ }
+
+ public static void run(Context context, final Runnable task) {
+ Handler handler = new Handler(context.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ task.run();
+ }
+ });
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java
new file mode 100644
index 0000000..d32977d
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.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.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Creates unique ids to identify the notifications created by the android scripting service and the
+ * trigger service.
+ *
+ *
+ */
+public final class NotificationIdFactory {
+ private static final AtomicInteger mNextId = new AtomicInteger(0);
+
+ public static int create() {
+ return mNextId.incrementAndGet();
+ }
+
+ private NotificationIdFactory() {
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java
new file mode 100644
index 0000000..65e543f
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.util;
+
+@SuppressWarnings("serial")
+public class SnippetLibException extends Exception {
+
+ public SnippetLibException(Exception e) {
+ super(e);
+ }
+
+ public SnippetLibException(String message) {
+ super(message);
+ }
+
+ public SnippetLibException(String message, Exception e) {
+ super(message, e);
+ }
+
+}