diff options
author | Kolin Lu <kolinlu@google.com> | 2023-04-29 20:37:37 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-29 20:37:37 -0700 |
commit | 52ea7ce5d0b68560fecf876c3eb73da2f33eb41f (patch) | |
tree | 25a556875c5b60e31c25f76d65a60a4db929f496 | |
parent | 42c24baf4a3efc9d9d5d91430c5907df8490f04f (diff) | |
download | mobly-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
```
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); +} |