aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-08-01 23:08:30 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-08-01 23:08:30 +0000
commit28e906960e1be7ccbdbc1a873c9a766756dd904b (patch)
treef66c05fc4ee0c136e252ed7c5d75fb0f70b02f1c
parent763a963ece2d8358d9681f940b4b91e0b005f09c (diff)
parent169a6ac6e325a9260e42786528d9ebba558c2daa (diff)
downloadmobly-snippet-lib-android14-qpr1-s2-release.tar.gz
Change-Id: I94e1be316a4ef1f495ed158300d9042cc36f9f8c
-rw-r--r--CHANGELOG5
-rw-r--r--METADATA4
-rw-r--r--OWNERS2
-rw-r--r--examples/ex1_standalone_app/README.md6
-rw-r--r--examples/ex1_standalone_app/build.gradle2
-rw-r--r--examples/ex2_espresso/README.md2
-rw-r--r--examples/ex2_espresso/build.gradle2
-rw-r--r--examples/ex3_async_event/build.gradle2
-rw-r--r--examples/ex4_uiautomator/build.gradle2
-rw-r--r--examples/ex5_schedule_rpc/build.gradle2
-rw-r--r--examples/ex6_complex_type_conversion/build.gradle2
-rw-r--r--examples/ex7_default_and_optional_rpc/README.md51
-rw-r--r--examples/ex7_default_and_optional_rpc/build.gradle27
-rw-r--r--examples/ex7_default_and_optional_rpc/src/main/AndroidManifest.xml16
-rw-r--r--examples/ex7_default_and_optional_rpc/src/main/java/com/google/android/mobly/snippet/example7/ExampleDefaultAndOptionalRpcSnippet.java70
-rw-r--r--third_party/sl4a/gradle.properties4
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java104
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java162
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java37
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java4
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java34
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java27
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java2
23 files changed, 490 insertions, 79 deletions
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 @@ third_party {
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 @@ murj@google.com
# Mobly team - use for mobly bugs
angli@google.com
lancefluger@google.com
+xianyuanjia@google.com
+kolinlu@google.com
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 @@ a snippet app that controls (instruments) another app under test, please see
```
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 @@ a snippet app that controls (instruments) another app under test, please see
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 @@ a snippet app that controls (instruments) another app under test, please see
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 @@ 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.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 @@ The snippet code cannot run from a regular test apk because it requires a custom
```
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 @@ dependencies {
// 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 @@ android {
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 @@ 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 '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 @@ android {
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 @@ 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.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.content.Intent;
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.JSONArray;
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 @@ public class JsonBuilder {
}
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 @@ public class JsonBuilder {
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 @@ public class JsonBuilder {
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.Snippet;
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 @@ public final class MethodDescriptor {
* @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 @@ public final class MethodDescriptor {
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 @@ public final class MethodDescriptor {
}
/** 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 @@ public final class MethodDescriptor {
+ " 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 @@ public final class MethodDescriptor {
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 @@ public final class MethodDescriptor {
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 class RpcError extends Exception {
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 @@ 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();