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;
     }
 }