mobly-snippet-lib: Allow snippet install on R devices am: a7badbfa0c

Original change: https://googleplex-android-review.googlesource.com/c/platform/external/mobly-snippet-lib/+/23483183

Change-Id: Icd6851f9a93ba2c3cd0c2db9f97aecd40fea1f27
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/CHANGELOG b/CHANGELOG
index 489a2f1..3a87f30 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,8 @@
+1.4.0:
+  - Support RpcDefault and RpcOptional
+  - Deprecate non-primitive array types and enable checked warnings in JsonBuilder 
+  - Setting class loader before unmarshalling bundle to prevent java.lang.ClassNotFoundException
+
 1.3.1:
   - Migrate from android.support to androidx library
   - Add a default close() method
diff --git a/METADATA b/METADATA
index ee44373..7318c3b 100644
--- a/METADATA
+++ b/METADATA
@@ -11,7 +11,7 @@
     type: GIT
     value: "https://github.com/google/mobly-snippet-lib"
   }
-  version: "1.3.1"
-  last_upgrade_date { year: 2022 month: 1 day: 4 }
+  version: "1.4.0"
+  last_upgrade_date { year: 2023 month: 5 day: 23 }
   license_type: NOTICE
 }
diff --git a/OWNERS b/OWNERS
index eb86f14..b21d896 100644
--- a/OWNERS
+++ b/OWNERS
@@ -6,3 +6,5 @@
 # Mobly team - use for mobly bugs
 [email protected]
 [email protected]
[email protected]
[email protected]
diff --git a/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md
index 5d68e34..b6d12d7 100644
--- a/examples/ex1_standalone_app/README.md
+++ b/examples/ex1_standalone_app/README.md
@@ -12,7 +12,7 @@
 
     ```
     dependencies {
-      implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+      implementation 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     }
     ```
 
@@ -23,7 +23,7 @@
     package com.my.app;
     ...
     public class ExampleSnippet implements Snippet {
-      @Rpc(description='Returns a string containing the given number.')
+      @Rpc(description="Returns a string containing the given number.")
       public String getFoo(Integer input) {
         return "foo " + input;
       }
@@ -58,7 +58,7 @@
         package="com.my.app">
       <application>...</application>
       <instrumentation
-          android:name="com.google.android.mobly.snippet.ServerRunner"
+          android:name="com.google.android.mobly.snippet.SnippetRunner"
           android:targetPackage="com.my.app" />
     </manifest>
     ```
diff --git a/examples/ex1_standalone_app/build.gradle b/examples/ex1_standalone_app/build.gradle
index ee60dc1..58c1ed1 100644
--- a/examples/ex1_standalone_app/build.gradle
+++ b/examples/ex1_standalone_app/build.gradle
@@ -22,6 +22,6 @@
     // The 'implementation project' dep is to compile against the snippet lib source in
     // this repo. For your own snippets, you'll want to use the regular
     // 'implementation' dep instead:
-    //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+    //implementation 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     implementation project(':mobly-snippet-lib')
 }
diff --git a/examples/ex2_espresso/README.md b/examples/ex2_espresso/README.md
index c9422a5..0041453 100644
--- a/examples/ex2_espresso/README.md
+++ b/examples/ex2_espresso/README.md
@@ -33,7 +33,7 @@
 
     ```
     dependencies {
-      snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+      snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     }
     ```
 
diff --git a/examples/ex2_espresso/build.gradle b/examples/ex2_espresso/build.gradle
index 4479e1b..b1d48c2 100644
--- a/examples/ex2_espresso/build.gradle
+++ b/examples/ex2_espresso/build.gradle
@@ -47,7 +47,7 @@
     // The 'snippetCompile project' dep is to compile against the snippet lib
     // source in this repo. For your own snippets, you'll want to use the
     // regular 'snippetCompile' dep instead:
-    //snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+    //snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     snippetImplementation project(':mobly-snippet-lib')
 
     snippetImplementation 'androidx.annotation:annotation:1.2.0'
diff --git a/examples/ex3_async_event/build.gradle b/examples/ex3_async_event/build.gradle
index 7327fc4..cbe14b8 100644
--- a/examples/ex3_async_event/build.gradle
+++ b/examples/ex3_async_event/build.gradle
@@ -21,6 +21,6 @@
 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.3.1'
+    // compile 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     implementation project(':mobly-snippet-lib')
 }
diff --git a/examples/ex4_uiautomator/build.gradle b/examples/ex4_uiautomator/build.gradle
index b071e1b..24188d9 100644
--- a/examples/ex4_uiautomator/build.gradle
+++ b/examples/ex4_uiautomator/build.gradle
@@ -23,7 +23,7 @@
     // 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.3.1'
+    //compile 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     implementation project(':mobly-snippet-lib')
     implementation 'junit:junit:4.13.2'
     implementation 'androidx.test:runner:1.4.0'
diff --git a/examples/ex5_schedule_rpc/build.gradle b/examples/ex5_schedule_rpc/build.gradle
index f5aa15c..8023128 100644
--- a/examples/ex5_schedule_rpc/build.gradle
+++ b/examples/ex5_schedule_rpc/build.gradle
@@ -21,7 +21,7 @@
 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.3.1'
+    // compile 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     implementation project(':mobly-snippet-lib')
     implementation 'androidx.test:runner:1.4.0'
 }
diff --git a/examples/ex6_complex_type_conversion/build.gradle b/examples/ex6_complex_type_conversion/build.gradle
index b6039b0..073a437 100644
--- a/examples/ex6_complex_type_conversion/build.gradle
+++ b/examples/ex6_complex_type_conversion/build.gradle
@@ -22,6 +22,6 @@
     // The 'implementation project' dep is to compile against the snippet lib source in
     // this repo. For your own snippets, you'll want to use the regular
     // 'implementation' dep instead:
-    //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+    //implementation 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
     implementation project(':mobly-snippet-lib')
 }
diff --git a/examples/ex7_default_and_optional_rpc/README.md b/examples/ex7_default_and_optional_rpc/README.md
new file mode 100644
index 0000000..c4b89b3
--- /dev/null
+++ b/examples/ex7_default_and_optional_rpc/README.md
@@ -0,0 +1,51 @@
+# Default and Optional RPCs Example
+
+This example shows you how to use `RpcDefault` and `RpcOptional` which is built
+into Mobly snippet lib to annotate RPC's parameters.
+
+## Why this is needed?
+
+These annotations can be used to specify the default and optional parameters for
+RPC methods, which allows developers to create more flexible and reusable RPC
+methods.
+
+Here are some additional benefits of using `RpcDefault` and `RpcOptional`:
+
+  - Improve the readability and maintainability of RPC methods.
+  - Prevent errors caused by missing or invalid parameters.
+  - Make it easier to test RPC methods.
+
+See the source code ExampleDefaultAndOptionalRpcSnippet.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:ex7_default_and_optional_rpc:assembleDebug
+
+1.  Install the apk on your phone
+
+        adb install -r ./examples/ex7_default_and_optional_rpc/build/outputs/apk/debug/ex7_default_and_optional_rpc-debug.apk
+
+1.  Use `snippet_shell` from mobly to trigger `makeToast()`:
+
+        snippet_shell.py com.google.android.mobly.snippet.example7
+
+        >>> s.makeToast('Hello')
+
+        Wait for `Hello, bool:true` message to show up on the screen. Here we
+        didn't provide a Boolean to the RPC, so a default value, true, is used.
+
+        >>> s.makeToast('Hello', False)
+
+        Wait for `Hello, bool:false` message to show up on the screen. Here we
+        provide a Boolean to the RPC, so the value is used instead of using
+        default value.
+
+        >>> s.makeToast('Hello', False, 1)
+
+        Wait for `Hello, bool:false, number: 1` message to show up on the
+        screen. The number is an optional parameter, it only shows up when we
+        pass a value to the RPC.
diff --git a/examples/ex7_default_and_optional_rpc/build.gradle b/examples/ex7_default_and_optional_rpc/build.gradle
new file mode 100644
index 0000000..35e3925
--- /dev/null
+++ b/examples/ex7_default_and_optional_rpc/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 31
+    buildToolsVersion '31.0.0'
+
+    defaultConfig {
+        applicationId "com.google.android.mobly.snippet.example7"
+        minSdkVersion 26
+        targetSdkVersion 31
+        versionCode 1
+        versionName "0.0.1"
+    }
+    lintOptions {
+        abortOnError true
+        checkAllWarnings true
+        warningsAsErrors true
+    }
+}
+
+dependencies {
+    // The 'implementation project' dep is to compile against the snippet lib source in
+    // this repo. For your own snippets, you'll want to use the regular
+    // 'implementation' dep instead:
+    //implementation 'com.google.android.mobly:mobly-snippet-lib:1.4.0'
+    implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex7_default_and_optional_rpc/src/main/AndroidManifest.xml b/examples/ex7_default_and_optional_rpc/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4896efc
--- /dev/null
+++ b/examples/ex7_default_and_optional_rpc/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.example7">
+
+    <application android:allowBackup="false">
+        <meta-data
+            android:name="mobly-snippets"
+            android:value="com.google.android.mobly.snippet.example7.ExampleDefaultAndOptionalRpcSnippet" />
+    </application>
+
+    <instrumentation
+        android:name="com.google.android.mobly.snippet.SnippetRunner"
+        android:targetPackage="com.google.android.mobly.snippet.example7" />
+
+</manifest>
diff --git a/examples/ex7_default_and_optional_rpc/src/main/java/com/google/android/mobly/snippet/example7/ExampleDefaultAndOptionalRpcSnippet.java b/examples/ex7_default_and_optional_rpc/src/main/java/com/google/android/mobly/snippet/example7/ExampleDefaultAndOptionalRpcSnippet.java
new file mode 100644
index 0000000..cef9343
--- /dev/null
+++ b/examples/ex7_default_and_optional_rpc/src/main/java/com/google/android/mobly/snippet/example7/ExampleDefaultAndOptionalRpcSnippet.java
@@ -0,0 +1,70 @@
+/*
+ * 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.example7;
+
+import android.content.Context;
+import android.os.Handler;
+import android.widget.Toast;
+import androidx.test.InstrumentationRegistry;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcDefault;
+import com.google.android.mobly.snippet.rpc.RpcOptional;
+
+/** Demonstrates how to mark an RPC has default value or optional. */
+public class ExampleDefaultAndOptionalRpcSnippet implements Snippet {
+
+    private final Context mContext;
+    private final EventCache mEventCache = EventCache.getInstance();
+
+    /**
+     * Since the APIs here deal with UI, most of them have to be called in a thread that has called
+     * looper.
+     */
+    private final Handler mHandler;
+
+    public ExampleDefaultAndOptionalRpcSnippet() {
+        mContext = InstrumentationRegistry.getContext();
+        mHandler = new Handler(mContext.getMainLooper());
+    }
+
+    @Rpc(description = "Make a toast on screen.")
+    public String makeToast(
+            String message, @RpcDefault("true") Boolean bool, @RpcOptional Integer number)
+            throws InterruptedException {
+        if (number == null) {
+            showToast(String.format("%s, bool:%b", message, bool));
+        } else {
+            showToast(String.format("%s, bool:%b, number:%d", message, bool, number));
+        }
+        return "OK";
+    }
+
+    @Override
+    public void shutdown() {}
+
+    private void showToast(final String message) {
+        mHandler.post(
+            new Runnable() {
+                @Override
+                public void run() {
+                    Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+                }
+            });
+    }
+}
diff --git a/third_party/sl4a/gradle.properties b/third_party/sl4a/gradle.properties
index 86f8068..731ecf8 100644
--- a/third_party/sl4a/gradle.properties
+++ b/third_party/sl4a/gradle.properties
@@ -1,6 +1,6 @@
 # This version code implements the versioning recommendations in:
 # https://blog.jayway.com/2015/03/11/automatic-versioncode-generation-in-android-gradle/
-VERSION_CODE=1030199
-VERSION_NAME=1.3.1
+VERSION_CODE=1040099
+VERSION_NAME=1.4.0
 GROUP_ID=com.google.android.mobly
 ARTIFACT_ID=mobly-snippet-lib
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
index a1d3425..f543b62 100644
--- 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
@@ -21,9 +21,6 @@
 import android.os.Bundle;
 import android.os.ParcelUuid;
 import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -34,11 +31,11 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
+/** Builds the result for JSON RPC. */
 public class JsonBuilder {
 
     private JsonBuilder() {}
 
-    @SuppressWarnings("unchecked")
     public static Object build(Object data) throws JSONException {
         if (data == null) {
             return JSONObject.NULL;
@@ -89,30 +86,13 @@
         }
         if (data instanceof Map<?, ?>) {
             // TODO(damonkohler): I would like to make this a checked cast if possible.
-            return buildJsonMap((Map<String, ?>) data);
+            return buildJsonMap((Map<?, ?>) data);
         }
         if (data instanceof ParcelUuid) {
             return data.toString();
         }
-        // TODO(xpconanfan): Deprecate the following default non-primitive type builders.
-        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);
+        if (data.getClass().isArray()) {
+            return buildJSONArray(data);
         }
         // Try with custom converter provided by user.
         Object result = SnippetObjectConverterManager.getInstance().objectToJson(data);
@@ -122,24 +102,44 @@
         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 {
+    private static JSONArray buildJSONArray(Object data) throws JSONException {
         JSONArray result = new JSONArray();
-        for (Object o : data) {
-            result.put(build(o));
+        if (data instanceof int[]) {
+            for (int i : (int []) data) {
+                result.put(i);
+            }
+        } else if (data instanceof short[]) {
+            for (short s : (short[]) data) {
+                result.put(s);
+            }
+        } else if (data instanceof long[]) {
+            for (long l : (long[]) data) {
+                result.put(l);
+            }
+        } else if (data instanceof float[]) {
+            for (float f : (float[]) data) {
+                result.put(f);
+            }
+        } else if (data instanceof double[]) {
+            for (double d : (double[]) data) {
+                result.put(d);
+            }
+        } else if (data instanceof boolean[]) {
+            for (boolean b : (boolean[]) data) {
+                result.put(b);
+            }
+        } else if (data instanceof char[]) {
+            for (char c : (char[]) data) {
+                result.put(c);
+            }
+        } else if (data instanceof byte[]) {
+            for (byte b : (byte[]) data) {
+                result.put(b & 0xFF);
+            }
+        } else {
+            for (Object o : (Object[]) data) {
+                result.put(build(o));
+            }
         }
         return result;
     }
@@ -177,25 +177,13 @@
         return result;
     }
 
-    private static JSONObject buildJsonMap(Map<String, ?> map) throws JSONException {
+    private static JSONObject buildJsonMap(Map<?, ?> 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()));
+        for (Entry<?, ?> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            String keyStr = key == null ? "" : key.toString();
+            result.put(keyStr, 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;
-    }
 }
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 b9c8a7a..214ffe7 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
@@ -22,18 +22,25 @@
 import com.google.android.mobly.snippet.manager.SnippetManager;
 import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
 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.Type;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+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<?>, TypeConverter<?>> typeConverters = populateConverters();
+
     private final Method mMethod;
     private final Class<? extends Snippet> mClass;
 
@@ -68,6 +75,7 @@
      * @throws Throwable the exception raised from executing the RPC method.
      */
     public Object invoke(SnippetManager manager, final JSONArray parameters) throws Throwable {
+        final Annotation[][] annotations = getParameterAnnotations();
         final Type[] parameterTypes = getGenericParameterTypes();
         final Object[] args = new Object[parameterTypes.length];
 
@@ -79,6 +87,12 @@
             final Type parameterType = parameterTypes[i];
             if (i < parameters.length()) {
                 args[i] = convertParameter(parameters, i, parameterType);
+            } else if (MethodDescriptor.hasDefaultValue(Arrays.asList(annotations[i]))) {
+                args[i] = MethodDescriptor.getDefaultValue(
+                        parameterType, Arrays.asList(annotations[i]));
+            } else if (MethodDescriptor.isOptional(Arrays.asList(annotations[i]))) {
+                args[i] = MethodDescriptor.getOptionalValue(
+                        parameterType, Arrays.asList(annotations[i]));
             } else {
                 throw new RpcError("Argument " + (i + 1) + " is not present");
             }
@@ -88,10 +102,6 @@
     }
 
     /** Converts a parameter from JSON into a Java Object. */
-    // 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
     private static Object convertParameter(final JSONArray parameters, int index, Type type)
             throws JSONException, RpcError {
         try {
@@ -165,7 +175,8 @@
                             + " should be of type "
                             + ((Class<?>) type).getSimpleName()
                             + ", but is of type "
-                            + parameters.get(index).getClass().getSimpleName());
+                            + parameters.get(index).getClass().getSimpleName(),
+                    e);
         }
     }
 
@@ -226,6 +237,11 @@
         Rpc annotation = mMethod.getAnnotation(Rpc.class);
         return annotation.description();
     }
+
+    public Annotation[][] getParameterAnnotations() {
+        return mMethod.getParameterAnnotations();
+    }
+
     /**
      * Returns a human-readable help text for this RPC, based on annotations in the source code.
      *
@@ -249,4 +265,140 @@
                 mMethod.getReturnType().getSimpleName(),
                 getAnnotationDescription());
     }
+
+    /**
+     * Returns the default value for a parameter which has a default value.
+     *
+     * @param parameterType parameterType
+     * @param annotations   annotations of the parameter
+     */
+    public static Object getDefaultValue(Type parameterType, Iterable<Annotation> annotations) {
+        for (Annotation a : annotations) {
+            if (a instanceof RpcDefault) {
+                RpcDefault defaultAnnotation = (RpcDefault) a;
+                TypeConverter<?> converter =
+                        converterFor(parameterType, defaultAnnotation.converter());
+                return converter.convert(defaultAnnotation.value());
+            }
+        }
+        throw new IllegalStateException("No default value for " + parameterType);
+    }
+
+    /**
+     * Returns null for an optional parameter.
+     *
+     * @param parameterType parameterType
+     * @param annotations   annotations of the parameter
+     */
+    public static Object getOptionalValue(Type parameterType, Iterable<Annotation> annotations) {
+        for (Annotation a : annotations) {
+            if (a instanceof RpcOptional) {
+                return null;
+            }
+        }
+        throw new IllegalStateException("No default value for " + parameterType);
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static TypeConverter<?> converterFor(
+            Type parameterType, Class<? extends TypeConverter> converterClass) {
+        if (converterClass == TypeConverter.class) {
+            TypeConverter<?> converter = typeConverters.get(parameterType);
+            if (converter == null) {
+                throw new IllegalArgumentException(
+                        String.format("No predefined converter found for %s", parameterType));
+            }
+            return converter;
+        }
+        try {
+            Constructor<?> constructor = converterClass.getConstructor(new Class<?>[0]);
+            return (TypeConverter<?>) constructor.newInstance(new Object[0]);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Cannot create converter from %s", converterClass.getCanonicalName()),
+                    e);
+        }
+    }
+
+    /**
+     * Determines whether or not this parameter has default value.
+     *
+     * @param annotations annotations of the parameter
+     */
+    public static boolean hasDefaultValue(Iterable<Annotation> annotations) {
+        for (Annotation a : annotations) {
+            if (a instanceof RpcDefault) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether or not this parameter is optional.
+     *
+     * @param annotations annotations of the parameter
+     */
+    public static boolean isOptional(Iterable<Annotation> annotations) {
+        for (Annotation a : annotations) {
+            if (a instanceof RpcOptional) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the converters for {@code String}, {@code Integer}, {@code Long},
+     * and {@code Boolean}.
+     */
+    private static Map<Class<?>, TypeConverter<?>> populateConverters() {
+        Map<Class<?>, TypeConverter<?>> converters = new HashMap<>();
+        converters.put(String.class, new TypeConverter<String>() {
+            @Override
+            public String convert(String value) {
+                return value;
+            }
+        });
+        converters.put(Integer.class, new TypeConverter<Integer>() {
+            @Override
+            public Integer convert(String input) {
+                try {
+                    return Integer.decode(input);
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException(
+                            String.format("'%s' is not a Integer", input), e);
+                }
+            }
+        });
+        converters.put(Long.class, new TypeConverter<Long>() {
+            @Override
+            public Long convert(String input) {
+                try {
+                    return Long.decode(input);
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException(
+                            String.format("'%s' is not a Long", input), e);
+                }
+            }
+        });
+        converters.put(Boolean.class, new TypeConverter<Boolean>() {
+            @Override
+            public Boolean convert(String input) {
+                if (input == null) {
+                    return null;
+                }
+                input = input.toLowerCase(Locale.ROOT);
+                if (input.equals("true")) {
+                    return Boolean.TRUE;
+                }
+                if (input.equals("false")) {
+                    return Boolean.FALSE;
+                }
+                throw new IllegalArgumentException(String.format("'%s' is not a Boolean", input));
+            }
+        });
+        return converters;
+    }
 }
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..bc98f7e
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.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 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. */
+    String value();
+
+    @SuppressWarnings("rawtypes")
+    Class<? extends TypeConverter> converter() default TypeConverter.class;
+}
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
index 0862673..03038ee 100644
--- 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
@@ -22,4 +22,8 @@
     public RpcError(String message) {
         super(message);
     }
+
+    public RpcError(String message, Throwable cause) {
+        super(message, cause);
+    }
 }
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..b4b43aa
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.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.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/TypeConverter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java
new file mode 100644
index 0000000..396e526
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java
@@ -0,0 +1,27 @@
+/*
+ * 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 TypeConverter<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/util/AndroidUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java
index 46c4940..69d5cf6 100644
--- 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
@@ -25,8 +25,6 @@
 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();