diff options
author | Ang Li <angli@google.com> | 2017-02-22 13:07:00 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-22 13:07:00 -0800 |
commit | b1f23a3dd1fce48791758fc57b0a35f8da6fe961 (patch) | |
tree | 65086d01ea136cfeed2de6e0d448f6d30142221a /third_party/sl4a/src | |
parent | 0f9ab75ee996dbf8f3f1e9f400c71068df5bfe62 (diff) | |
download | mobly-snippet-lib-b1f23a3dd1fce48791758fc57b0a35f8da6fe961.tar.gz |
Add support for asynchronous Rpc (#38)
* Add @AsyncRpc annotation to mark Rpc methods that trigger async events.
* Add `EventCache`, which is the repo of events.
* Add `EventSnippet`, which has Rpc methods for client to poll events.
* Add `SnippetEvent` type to represent an event.
* Add `callbackId` field to Rpc protocol's server resp msg.
Diffstat (limited to 'third_party/sl4a/src')
10 files changed, 371 insertions, 17 deletions
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java new file mode 100644 index 0000000..3d8775f --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.event; + +import com.google.android.mobly.snippet.util.Log; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Manage the event queue. + * + * <p>EventCache APIs interact with the SnippetEvent cache - a data structure that holds {@link + * SnippetEvent} objects posted from snippet classes. The SnippetEvent cache provides a useful means + * of recording background events (such as sensor data) when the phone is busy with foreground + * activities. + */ +public class EventCache { + private static final String EVENT_DEQUE_ID_TEMPLATE = "%s|%s"; + private static final int EVENT_DEQUE_MAX_SIZE = 1024; + + // A Map with each value being the queue for a particular type of event, and the key being the + // unique ID of the queue. The ID is composed of a callback ID and an event's name. + private final Map<String, LinkedBlockingDeque<SnippetEvent>> mEventDeques = new HashMap<>(); + + private static volatile EventCache mEventCache; + + private EventCache() {} + + public static synchronized EventCache getInstance() { + if (mEventCache == null) { + mEventCache = new EventCache(); + } + return mEventCache; + } + + public static String getQueueId(String callbackId, String name) { + return String.format(EVENT_DEQUE_ID_TEMPLATE, callbackId, name); + } + + public LinkedBlockingDeque<SnippetEvent> getEventDeque(String qId) { + synchronized (mEventDeques) { + LinkedBlockingDeque<SnippetEvent> eventDeque = mEventDeques.get(qId); + if (eventDeque == null) { + eventDeque = new LinkedBlockingDeque<>(EVENT_DEQUE_MAX_SIZE); + mEventDeques.put(qId, eventDeque); + } + return eventDeque; + } + } + + /** + * Post an {@link SnippetEvent} object to the Event cache. + * + * <p>Snippet classes should use this method to post events. If EVENT_DEQUE_MAX_SIZE is reached, + * the oldest elements will be retired until the new event could be posted. + * + * @param snippetEvent The snippetEvent to post to {@link EventCache}. + */ + public void postEvent(SnippetEvent snippetEvent) { + String qId = getQueueId(snippetEvent.getCallbackId(), snippetEvent.getName()); + Deque<SnippetEvent> q = getEventDeque(qId); + synchronized (q) { + while (!q.offer(snippetEvent)) { + SnippetEvent retiredEvent = q.removeFirst(); + Log.v( + String.format( + "Retired event %s due to deque reaching the size limit (%s).", + retiredEvent, EVENT_DEQUE_MAX_SIZE)); + } + } + Log.v(String.format("Posted event(%s)", qId)); + } + + /** Clears all cached events. */ + public void clearAll() { + synchronized (mEventDeques) { + mEventDeques.clear(); + } + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java new file mode 100644 index 0000000..1a29b39 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.event; + +import android.support.annotation.Nullable; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import org.json.JSONException; +import org.json.JSONObject; + +public class EventSnippet implements Snippet { + + private static class EventSnippetException extends Exception { + public EventSnippetException(String msg) { + super(msg); + } + } + + private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; + private final EventCache mEventCache = EventCache.getInstance(); + + @Rpc( + description = + "Blocks until an event of a specified type has been received. The returned event is removed from the cache. Default timeout is 60s." + ) + public JSONObject eventWaitAndGet( + String callbackId, String eventName, @Nullable Integer timeout) + throws InterruptedException, JSONException, EventSnippetException { + // The server side should never wait forever, so we'll use a default timeout is one is not + // provided. + if (timeout == null) { + timeout = DEFAULT_TIMEOUT_MILLISECOND; + } + String qId = EventCache.getQueueId(callbackId, eventName); + LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId); + SnippetEvent result = q.pollFirst(timeout, TimeUnit.MILLISECONDS); + if (result == null) { + throw new EventSnippetException("timeout."); + } + return result.toJson(); + } + + @Rpc( + description = + "Gets and removes all the events of a certain name that have been received so far. " + + "Non-blocking. Potentially racey since it does not guarantee no event of " + + "the same name will occur after the call." + ) + public List<JSONObject> eventGetAll(String callbackId, String eventName) + throws InterruptedException, JSONException { + String qId = EventCache.getQueueId(callbackId, eventName); + LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId); + ArrayList<JSONObject> results = new ArrayList<>(q.size()); + ArrayList<SnippetEvent> buffer = new ArrayList<>(q.size()); + q.drainTo(buffer); + for (SnippetEvent snippetEvent : buffer) { + results.add(snippetEvent.toJson()); + } + if (results.size() == 0) { + return Collections.emptyList(); + } + return results; + } + + @Override + public void shutdown() { + mEventCache.clearAll(); + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java new file mode 100644 index 0000000..3db2906 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.event; + +import org.json.JSONException; +import org.json.JSONObject; + +/** Class used to store information from a callback event. */ +public class SnippetEvent { + + // The ID used to associate an event to a callback object on the client side. + private final String mCallbackId; + // The name of this event, e.g. startXxxServiceOnSuccess. + private final String mName; + // The content of this event. + private final JSONObject mData = new JSONObject(); + + private final long mCreationTime; + + /** + * Constructs an {@link SnippetEvent} object. + * + * <p>The object is used to store information from a callback method associated with a call to + * an {@link com.google.android.mobly.snippet.rpc.AsyncRpc} method. + * + * @param callbackId The callbackId passed to the {@link + * com.google.android.mobly.snippet.rpc.AsyncRpc} method. + * @param name The name of the event. + */ + public SnippetEvent(String callbackId, String name) { + if (callbackId == null) { + throw new IllegalArgumentException("SnippetEvent's callback ID shall not be null."); + } + if (name == null) { + throw new IllegalArgumentException("SnippetEvent's name shall not be null."); + } + mCallbackId = callbackId; + mName = name; + mCreationTime = System.currentTimeMillis(); + } + + public String getCallbackId() { + return mCallbackId; + } + + public String getName() { + return mName; + } + + /** + * Add serializable data to the Event. + * + * <p>This is usually for information passed by the original callback API. The data has to be + * JSON serializable so it can be transferred to the client side. + * + * @param name Name of the data set. + * @param data Content of the data. + * @throws JSONException + */ + public void addData(String name, Object data) throws JSONException { + mData.put(name, data); + } + + private JSONObject getData() { + return mData; + } + + public long getCreationTime() { + return mCreationTime; + } + + public JSONObject toJson() throws JSONException { + JSONObject result = new JSONObject(); + result.put("callbackId", getCallbackId()); + result.put("name", getName()); + result.put("time", getCreationTime()); + result.put("data", getData()); + return result; + } +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/ReflectionSnippetManagerFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/ReflectionSnippetManagerFactory.java index db90f32..5e4259d 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/ReflectionSnippetManagerFactory.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/ReflectionSnippetManagerFactory.java @@ -19,9 +19,9 @@ package com.google.android.mobly.snippet.manager; 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.Snippet; +import com.google.android.mobly.snippet.event.EventSnippet; import com.google.android.mobly.snippet.util.Log; import java.util.Collections; import java.util.HashMap; @@ -44,7 +44,6 @@ public class ReflectionSnippetManagerFactory implements SnippetManagerFactory { @Override public SnippetManager create(Integer UID) { - int sdkLevel = Build.VERSION.SDK_INT; SnippetManager manager = new SnippetManager(mClasses); mSnippetManagers.put(UID, manager); return manager; @@ -78,6 +77,8 @@ public class ReflectionSnippetManagerFactory implements SnippetManagerFactory { } String[] snippetClassNames = snippets.split("\\s*,\\s*"); Set<Class<? extends Snippet>> receiverSet = new HashSet<>(); + /** Add the event snippet class which is provided within the Snippet Lib. */ + receiverSet.add(EventSnippet.class); for (String snippetClassName : snippetClassNames) { try { Log.i("Trying to load Snippet class: " + snippetClassName); diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java index da5a56d..8944a84 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java @@ -98,7 +98,6 @@ public class SnippetManager { if (object != null) { return object; } - Constructor<? extends Snippet> constructor; constructor = clazz.getConstructor(); object = constructor.newInstance(); diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java new file mode 100644 index 0000000..6bdd8ca --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@link AsyncRpc} annotation is used to annotate server-side implementations of RPCs that + * trigger asynchronous events. This behaves generally the same as {@link Rpc}, but methods that are + * annotated with {@link AsyncRpc} are expected to take the extra parameter which is the ID to use + * when posting async events. + * + * <p>Sample Usage: + * + * <pre>{@code + * {@literal @}AsyncRpc(description = "An example showing the usage of AsyncRpc") + * public void doSomethingAsync(String callbackId, ...) { + * // start some async operation and post a Snippet Event object with the given callbackId. + * } + * }</pre> + * + * AsyncRpc methods can still return serializable values, which will be transported in the regular + * return value field of the Rpc protocol. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface AsyncRpc { + /** Returns brief description of the function. Should be limited to one or two sentences. */ + String description(); +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java index 739eadf..de8537c 100644 --- 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 @@ -24,7 +24,7 @@ import org.json.JSONObject; /** * Represents a JSON RPC result. * - * @see http://json-rpc.org/wiki/specification + * @see <a href="http://json-rpc.org/wiki/specification">http://json-rpc.org/wiki/specification</a> */ public class JsonRpcResult { @@ -36,6 +36,7 @@ public class JsonRpcResult { JSONObject json = new JSONObject(); json.put("id", id); json.put("result", JSONObject.NULL); + json.put("callback", JSONObject.NULL); json.put("error", JSONObject.NULL); return json; } @@ -44,6 +45,16 @@ public class JsonRpcResult { JSONObject json = new JSONObject(); json.put("id", id); json.put("result", JsonBuilder.build(data)); + json.put("callback", JSONObject.NULL); + json.put("error", JSONObject.NULL); + return json; + } + + public static JSONObject callback(int id, Object data, String callbackId) throws JSONException { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("result", JsonBuilder.build(data)); + json.put("callback", callbackId); json.put("error", JSONObject.NULL); return json; } @@ -54,10 +65,10 @@ public class JsonRpcResult { t.printStackTrace(new PrintWriter(stackTraceWriter)); stackTraceWriter.write("----------------------------------------------"); String stackTrace = stackTraceWriter.toString(); - JSONObject json = new JSONObject(); json.put("id", id); json.put("result", JSONObject.NULL); + json.put("callback", JSONObject.NULL); json.put("error", stackTrace); 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 index 6542aad..e031dcb 100644 --- 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 @@ -100,7 +100,20 @@ public class JsonRpcServer extends SimpleServer { continue; } try { - send(writer, JsonRpcResult.result(id, rpc.invoke(receiverManager, params)), UID); + /** If calling an {@link AsyncRpc}, put the message ID as the first param. */ + if (rpc.isAsync()) { + String callbackId = String.format("%d-%d", UID, id); + JSONArray newParams = new JSONArray(); + newParams.put(callbackId); + for (int i = 0; i < params.length(); i++) { + newParams.put(params.get(i)); + } + Object returnValue = rpc.invoke(receiverManager, newParams); + send(writer, JsonRpcResult.callback(id, returnValue, callbackId), UID); + } else { + Object returnValue = rpc.invoke(receiverManager, params); + send(writer, JsonRpcResult.result(id, returnValue), UID); + } } catch (Throwable t) { Log.e("Invocation error.", t); send(writer, JsonRpcResult.error(id, t), UID); 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 index c64fdd4..6c268a0 100644 --- 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 @@ -49,7 +49,8 @@ public final class MethodDescriptor { public static Collection<MethodDescriptor> collectFrom(Class<? extends Snippet> clazz) { List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>(); for (Method method : clazz.getMethods()) { - if (method.isAnnotationPresent(Rpc.class)) { + if (method.isAnnotationPresent(Rpc.class) + || method.isAnnotationPresent(AsyncRpc.class)) { descriptors.add(new MethodDescriptor(clazz, method)); } } @@ -100,28 +101,28 @@ public final class MethodDescriptor { // also need to convert implicitly from numbers to bools. if (parameters.isNull(index)) { return null; - } else if (type == Boolean.class) { + } else if (type == Boolean.class || type == boolean.class) { try { return parameters.getBoolean(index); } catch (JSONException e) { return new Boolean(parameters.getInt(index) != 0); } - } else if (type == Long.class) { + } else if (type == Long.class || type == long.class) { return parameters.getLong(index); - } else if (type == Double.class) { + } else if (type == Double.class || type == double.class) { return parameters.getDouble(index); - } else if (type == Integer.class) { + } else if (type == Integer.class || type == int.class) { return parameters.getInt(index); } else if (type == Intent.class) { return buildIntent(parameters.getJSONObject(index)); - } else if (type == Integer[].class) { + } else if (type == Integer[].class || type == int[].class) { JSONArray list = parameters.getJSONArray(index); Integer[] result = new Integer[list.length()]; for (int i = 0; i < list.length(); i++) { result[i] = list.getInt(i); } return result; - } else if (type == byte[].class) { + } else if (type == Byte.class || type == byte[].class) { JSONArray list = parameters.getJSONArray(index); byte[] result = new byte[list.length()]; for (int i = 0; i < list.length(); i++) { @@ -147,7 +148,8 @@ public final class MethodDescriptor { + (index + 1) + " should be of type " + ((Class<?>) type).getSimpleName() - + "."); + + ", but is of type " + + parameters.get(index).getClass().getSimpleName()); } } @@ -192,6 +194,9 @@ public final class MethodDescriptor { return mMethod.getGenericParameterTypes(); } + public boolean isAsync() { + return mMethod.isAnnotationPresent(AsyncRpc.class); + } /** * Returns a human-readable help text for this RPC, based on annotations in the source code. * 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 index 5b59129..61880be 100644 --- 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 @@ -191,9 +191,7 @@ public abstract class SimpleServer { * @throws IOException */ public void startLocal(int port) throws IOException { - InetAddress address; - // address = InetAddress.getLocalHost(); - address = getPrivateInetAddress(); + InetAddress address = getPrivateInetAddress(); mServer = new ServerSocket(port, 5, address); start(); } |