aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKolin Lu <kolinlu@google.com>2023-04-29 20:37:37 -0700
committerGitHub <noreply@github.com>2023-04-29 20:37:37 -0700
commit52ea7ce5d0b68560fecf876c3eb73da2f33eb41f (patch)
tree25a556875c5b60e31c25f76d65a60a4db929f496
parent42c24baf4a3efc9d9d5d91430c5907df8490f04f (diff)
downloadmobly-snippet-lib-52ea7ce5d0b68560fecf876c3eb73da2f33eb41f.tar.gz
Support RpcDefault for RPC arguments (#123)
With this annotation, arguments which are not present when calling will be assigned a default value. Support types: String, Integer, Long, and Boolean. Usage: ```java @Rpc(description = "Returns true if value is Hello, false otherwise.") public boolean isStringHello(@RpcDefault("Hello") String value) { return value.equals("Hello"); } @Rpc(description = "Returns true if value is 1, false otherwise.") public boolean isNumerOne(@RpcDefault("1") Integer value) { return value == 1; } @Rpc(description = "Returns true if value is true, false otherwise.") public boolean isBooleanTrue(@RpcDefault("true") Boolean value) { return value == true; } ``` ```python assert mbs.isStringHello()==True assert mbs.isStringHello('Hello')==True assert mbs.isStringHello('World')==False assert mbs.isNumerOne()==True assert mbs.isNumerOne(1)==True assert mbs.isNumerOne(0)==False assert mbs.isBooleanTrue()==True assert mbs.isBooleanTrue(True)==True assert mbs.isBooleanTrue(False)==False ```
-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/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java123
-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/TypeConverter.java27
7 files changed, 346 insertions, 5 deletions
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..04112c0
--- /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.3.1'
+ 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/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 506fae9..55a73ce 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
@@ -23,18 +23,23 @@ 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.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;
@@ -83,6 +88,8 @@ public final class MethodDescriptor {
args[i] = convertParameter(parameters, i, parameterType);
} else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
+ } else if (MethodDescriptor.isOptional(annotations[i])) {
+ args[i] = MethodDescriptor.getOptionalValue(parameterType, annotations[i]);
} else {
throw new RpcError("Argument " + (i + 1) + " is not present");
}
@@ -222,10 +229,6 @@ public final class MethodDescriptor {
return mClass;
}
- public Annotation[][] getParameterAnnotations() {
- return mMethod.getParameterAnnotations();
- }
-
private String getAnnotationDescription() {
if (isAsync()) {
AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class);
@@ -235,6 +238,10 @@ public final class MethodDescriptor {
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.
*
@@ -260,13 +267,31 @@ public final class MethodDescriptor {
}
/**
- * Returns the default value for a specific parameter.
+ * 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, 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, Annotation[] annotations) {
+ for (Annotation a : annotations) {
if (a instanceof RpcOptional) {
return null;
}
@@ -274,6 +299,28 @@ public final class MethodDescriptor {
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.
*
@@ -281,6 +328,20 @@ public final class MethodDescriptor {
*/
public static boolean hasDefaultValue(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(Annotation[] annotations) {
+ for (Annotation a : annotations) {
if (a instanceof RpcOptional) {
return true;
}
@@ -288,4 +349,56 @@ public final class MethodDescriptor {
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/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);
+}