Async rpc updates. (#43)
* Fix a bug where help() fails if AsyncRpc exists.
* Use Bundle instead of JSONObject to hold data in SnippetEvent to
simplify exception handling for adding data.
* Add a dedicated example for async rpc.
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
index 97a8b27..665d03b 100644
--- a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
@@ -18,13 +18,8 @@
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.event.EventCache;
-import com.google.android.mobly.snippet.event.SnippetEvent;
-import com.google.android.mobly.snippet.rpc.AsyncRpc;
import com.google.android.mobly.snippet.rpc.Rpc;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.IOException;
public class ExampleSnippet2 implements Snippet {
@@ -41,35 +36,6 @@
throw new IOException("Example exception from throwSomething()");
}
- /**
- * An Rpc method demonstrating the async event mechanism.
- *
- * Expect to see an event on the client side that looks like:
- * {
- * 'name': 'ExampleEvent',
- * 'time': <timestamp>,
- * 'data': {
- * 'exampleData': "Here's a simple event.",
- * 'secret': 42.24,
- * 'isSecretive': True
- * }
- * }
- *
- * @param eventId
- * @throws JSONException
- */
- @AsyncRpc(description = "This call puts an event in the event queue.")
- public void tryEvent(String eventId) throws JSONException {
- SnippetEvent event = new SnippetEvent(eventId, "ExampleEvent");
- event.addData("exampleData", "Here's a simple event.");
- event.addData("secret", 42.24);
- event.addData("isSecretive", true);
- JSONObject moreData = new JSONObject();
- moreData.put("evenMoreData", "More Data!");
- event.addData("moreData", moreData);
- mEventQueue.postEvent(event);
- }
-
@Override
public void shutdown() {}
}
diff --git a/examples/ex3_async_event/README.md b/examples/ex3_async_event/README.md
new file mode 100644
index 0000000..91cba49
--- /dev/null
+++ b/examples/ex3_async_event/README.md
@@ -0,0 +1,41 @@
+# Async Event Rpc Example
+
+This example shows you how to use the @AsyncRpc of Mobly snippet lib
+to handle asynchornous callbacks.
+
+See the source code ExampleAsyncSnippet.java for details.
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex3_async_event:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex3_async_event/build/outputs/apk/ex3_async_event-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `tryEvent()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example3
+
+ >>> handler = s.tryEvent(42)
+ >>> print("Not blocked, can do stuff here")
+ >>> event = handler.waitAndGet('AsyncTaskResult') # Blocks until the event is received
+
+ Now let's see the content of the event
+
+ >>> import pprint
+ >>> pprint.pprint(event)
+ {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'exampleData': "Here's a simple event.",
+ 'successful': True,
+ 'secretNumber': 12
+ }
+ }
diff --git a/examples/ex3_async_event/build.gradle b/examples/ex3_async_event/build.gradle
new file mode 100644
index 0000000..fa7947c
--- /dev/null
+++ b/examples/ex3_async_event/build.gradle
@@ -0,0 +1,21 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 24
+ buildToolsVersion "24.0.3"
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example3"
+ minSdkVersion 11
+ targetSdkVersion 24
+ versionCode 1
+ versionName "0.0.1"
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular 'compile' dep instead:
+ // compile 'com.google.android.mobly:mobly-snippet-lib:1.0.1'
+ compile project(':mobly-snippet-lib')
+}
diff --git a/examples/ex3_async_event/src/main/AndroidManifest.xml b/examples/ex3_async_event/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8e2887c
--- /dev/null
+++ b/examples/ex3_async_event/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example3">
+
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example3.ExampleAsyncSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example3" />
+
+</manifest>
diff --git a/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
new file mode 100644
index 0000000..a2b22fa
--- /dev/null
+++ b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
@@ -0,0 +1,98 @@
+/*
+ * 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.example3;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.util.Log;
+
+public class ExampleAsyncSnippet implements Snippet {
+
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ /**
+ * This is a sample asynchronous task.
+ *
+ * In real world use cases, it can be a {@link android.content.BroadcastReceiver}, a Listener,
+ * or any other kind asynchronous callback class.
+ */
+ public class AsyncTask implements Runnable {
+
+ private final String mCallbackId;
+ private final int mSecretNumber;
+
+ public AsyncTask(String callbackId, int secreteNumber) {
+ this.mCallbackId = callbackId;
+ this.mSecretNumber = secreteNumber;
+ }
+
+ /**
+ * Sleeps for 10s then post a {@link SnippetEvent} with some data.
+ *
+ * If the sleep is interrupted, a {@link SnippetEvent} signaling failure will be posted instead.
+ */
+ public void run() {
+ Log.d("Sleeping for 10s before posting an event.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, "AsyncTaskResult");
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ event.getData().putBoolean("successful", false);
+ event.getData().putString("reason", "Sleep was interrupted.");
+ mEventCache.postEvent(event);
+ }
+ event.getData().putBoolean("successful", true);
+ event.getData().putString("exampleData", "Here's a simple event.");
+ event.getData().putInt("secretNumber", mSecretNumber);
+ mEventCache.postEvent(event);
+ }
+ }
+
+ /**
+ * An Rpc method demonstrating the async event mechanism.
+ *
+ * This call returns immediately, but starts a task in a separate thread which will post an
+ * event 10s after the task was started.
+ *
+ * Expect to see an event on the client side that looks like:
+ *
+ * {
+ * 'callbackId': '2-1',
+ * 'name': 'AsyncTaskResult',
+ * 'time': 20460228696,
+ * 'data': {
+ * 'exampleData': "Here's a simple event.",
+ * 'successful': True,
+ * 'secretNumber': 12
+ * }
+ * }
+ *
+ * @param callbackId The ID that should be used to tag {@link SnippetEvent} objects triggered by
+ * this method.
+ * @throws InterruptedException
+ */
+ @AsyncRpc(description = "This triggers an async event and returns.")
+ public void tryEvent(String callbackId, int secretNumber) throws InterruptedException {
+ Runnable asyncTask = new AsyncTask(callbackId, secretNumber);
+ Thread thread = new Thread(asyncTask);
+ thread.start();
+ }
+ @Override
+ public void shutdown() {}
+}
diff --git a/settings.gradle b/settings.gradle
index 6ff866b..4c14229 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
-include ':mobly-snippet-lib', ':examples:ex1_standalone_app', ':examples:ex2_espresso'
+include ':mobly-snippet-lib', ':examples:ex1_standalone_app', ':examples:ex2_espresso', ':examples:ex3_async_event'
project(":mobly-snippet-lib").projectDir = file('third_party/sl4a')
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
index 3db2906..a90d9eb 100644
--- 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
@@ -16,6 +16,8 @@
package com.google.android.mobly.snippet.event;
+import android.os.Bundle;
+import com.google.android.mobly.snippet.rpc.JsonBuilder;
import org.json.JSONException;
import org.json.JSONObject;
@@ -26,8 +28,10 @@
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();
+ // The content of this event. We use Android's Bundle because it adheres to Android convention
+ // and adding data to it does not throw checked exceptions, which makes the world a better
+ // place.
+ private final Bundle mData = new Bundle();
private final long mCreationTime;
@@ -62,20 +66,14 @@
}
/**
- * Add serializable data to the Event.
+ * Get the internal bundle of this 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.
+ * <p>This is the only way to add data to the event, because we can't inherit Bundle type and we
+ * don't want to dup all the getter and setters of {@link Bundle}.
*
- * @param name Name of the data set.
- * @param data Content of the data.
- * @throws JSONException
+ * @return The Bundle that holds user data for this {@link SnippetEvent}.
*/
- public void addData(String name, Object data) throws JSONException {
- mData.put(name, data);
- }
-
- private JSONObject getData() {
+ public Bundle getData() {
return mData;
}
@@ -88,7 +86,7 @@
result.put("callbackId", getCallbackId());
result.put("name", getName());
result.put("time", getCreationTime());
- result.put("data", getData());
+ result.put("data", JsonBuilder.build(mData));
return result;
}
}
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 6c268a0..4c04bb6 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
@@ -197,6 +197,15 @@
public boolean isAsync() {
return mMethod.isAnnotationPresent(AsyncRpc.class);
}
+
+ private String getAnnotationDescription() {
+ if (isAsync()) {
+ AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class);
+ return annotation.description();
+ }
+ Rpc annotation = mMethod.getAnnotation(Rpc.class);
+ return annotation.description();
+ }
/**
* Returns a human-readable help text for this RPC, based on annotations in the source code.
*
@@ -211,14 +220,13 @@
}
paramBuilder.append(parameterTypes[i].getSimpleName());
}
- Rpc rpcAnnotation = mMethod.getAnnotation(Rpc.class);
String help =
String.format(
"%s(%s) returns %s // %s",
mMethod.getName(),
paramBuilder,
mMethod.getReturnType().getSimpleName(),
- rpcAnnotation.description());
+ getAnnotationDescription());
return help;
}
}