diff options
author | Alexander Dorokhine <adorokhine@google.com> | 2016-11-09 18:34:39 -0800 |
---|---|---|
committer | Alexander Dorokhine <adorokhine@google.com> | 2016-11-09 19:07:34 -0800 |
commit | 146e4a6479ec8e841fb8fc56c7771c3ec75c8b0b (patch) | |
tree | b6d497a7baea1cb4ca4808aa7369fcd387c92c60 /third_party/sl4a/src | |
parent | 30e605fb891e2d2f16c4cb356acdca1b5fda9765 (diff) | |
download | mobly-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')
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); + } + +} |