From 143bf6e3f2a62acd9cd1b7be6ee3fb113b7e180c Mon Sep 17 00:00:00 2001 From: Xianyuan Jia Date: Tue, 19 Oct 2021 19:11:32 -0700 Subject: Update development version to 1.3.2 --- third_party/sl4a/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/third_party/sl4a/gradle.properties b/third_party/sl4a/gradle.properties index 86f8068..ebbf2fe 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=1030200 +VERSION_NAME=1.3.2-SNAPSHOT GROUP_ID=com.google.android.mobly ARTIFACT_ID=mobly-snippet-lib -- cgit v1.2.3 From 6d6bc2b88444c00b594bb9dd694592306acdca93 Mon Sep 17 00:00:00 2001 From: Xianyuan Jia Date: Mon, 8 Aug 2022 17:38:38 -0700 Subject: Support primitive array types in JsonBuilder --- .../android/mobly/snippet/rpc/JsonBuilder.java | 51 +++++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) 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..42629d8 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 @@ -104,15 +104,8 @@ public class JsonBuilder { 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); @@ -136,10 +129,44 @@ public class JsonBuilder { 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; } -- cgit v1.2.3 From 1d3fa077dd023d0174ce282bdbf9d2c9bd9f4b83 Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Thu, 27 Apr 2023 16:17:19 -0700 Subject: Support `@RpcOptional` With this annotation, arguments which are not present when calling will be assign `null` value by default. Usage: ```java @Rpc(description = "Returns true if value is null, false otherwise.") public boolean isValueNull(@RpcOptional Integer value) { return value == null; } ``` ```python assert mbs.isValueNull()==True assert mbs.isValueNull(0)==False ``` --- .../mobly/snippet/rpc/MethodDescriptor.java | 39 ++++++++++++++++++++++ .../android/mobly/snippet/rpc/RpcOptional.java | 34 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java 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..506fae9 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,6 +22,7 @@ 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.Method; import java.lang.reflect.Type; import java.util.ArrayList; @@ -68,6 +69,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 +81,8 @@ public final class MethodDescriptor { final Type parameterType = parameterTypes[i]; if (i < parameters.length()) { args[i] = convertParameter(parameters, i, parameterType); + } else if (MethodDescriptor.hasDefaultValue(annotations[i])) { + args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]); } else { throw new RpcError("Argument " + (i + 1) + " is not present"); } @@ -218,6 +222,10 @@ public final class MethodDescriptor { return mClass; } + public Annotation[][] getParameterAnnotations() { + return mMethod.getParameterAnnotations(); + } + private String getAnnotationDescription() { if (isAsync()) { AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class); @@ -226,6 +234,7 @@ public final class MethodDescriptor { Rpc annotation = mMethod.getAnnotation(Rpc.class); return annotation.description(); } + /** * Returns a human-readable help text for this RPC, based on annotations in the source code. * @@ -249,4 +258,34 @@ public final class MethodDescriptor { mMethod.getReturnType().getSimpleName(), getAnnotationDescription()); } + + /** + * Returns the default value for a specific parameter. + * + * @param parameterType parameterType + * @param annotations annotations of the parameter + */ + public static Object getDefaultValue(Type parameterType, Annotation[] annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcOptional) { + return null; + } + } + throw new IllegalStateException("No default value for " + parameterType); + } + + /** + * Determines whether or not this parameter has default value. + * + * @param annotations annotations of the parameter + */ + public static boolean hasDefaultValue(Annotation[] annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcOptional) { + return true; + } + } + return false; + } + } 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. + * + *

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 {} -- cgit v1.2.3 From 42c24baf4a3efc9d9d5d91430c5907df8490f04f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 29 Apr 2023 03:00:47 +0200 Subject: Update README.md (#117) Use double instead of single quotes for Rpc description (doesn't compile with single quotes) --- examples/ex1_standalone_app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md index 5d68e34..1a2d23e 100644 --- a/examples/ex1_standalone_app/README.md +++ b/examples/ex1_standalone_app/README.md @@ -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; } -- cgit v1.2.3 From 52ea7ce5d0b68560fecf876c3eb73da2f33eb41f Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Sat, 29 Apr 2023 20:37:37 -0700 Subject: 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 ``` --- examples/ex7_default_and_optional_rpc/README.md | 51 +++++++++ examples/ex7_default_and_optional_rpc/build.gradle | 27 +++++ .../src/main/AndroidManifest.xml | 16 +++ .../ExampleDefaultAndOptionalRpcSnippet.java | 70 ++++++++++++ .../mobly/snippet/rpc/MethodDescriptor.java | 123 ++++++++++++++++++++- .../android/mobly/snippet/rpc/RpcDefault.java | 37 +++++++ .../android/mobly/snippet/rpc/TypeConverter.java | 27 +++++ 7 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 examples/ex7_default_and_optional_rpc/README.md create mode 100644 examples/ex7_default_and_optional_rpc/build.gradle create mode 100644 examples/ex7_default_and_optional_rpc/src/main/AndroidManifest.xml create mode 100644 examples/ex7_default_and_optional_rpc/src/main/java/com/google/android/mobly/snippet/example7/ExampleDefaultAndOptionalRpcSnippet.java create mode 100644 third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java create mode 100644 third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java 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 @@ + + + + + + + + + + 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, TypeConverter> typeConverters = populateConverters(); + private final Method mMethod; private final Class 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,12 +267,30 @@ 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,12 +299,48 @@ public final class MethodDescriptor { throw new IllegalStateException("No default value for " + parameterType); } + @SuppressWarnings("rawtypes") + private static TypeConverter converterFor( + Type parameterType, Class 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(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, TypeConverter> populateConverters() { + Map, TypeConverter> converters = new HashMap<>(); + converters.put(String.class, new TypeConverter() { + @Override + public String convert(String value) { + return value; + } + }); + converters.put(Integer.class, new TypeConverter() { + @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() { + @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() { + @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 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 { + + /** Convert a string into type T. */ + T convert(String value); +} -- cgit v1.2.3 From 67e3941ddeccf87fed519e0f9cd8679c826888f2 Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Mon, 1 May 2023 16:50:20 -0700 Subject: Fix Java style, lint, and best practices warnings. (#124) 1. Java Style - MixedArrayDimensions. C-style array declarations should not be used 2. Java Lint - UnusedException. New exception to be thrown should include the caught exception as its cause. 3. Java Api Best Practices - AvoidObjectArrays. Avoid accepting a Annotation[]; consider an Iterable instead --- .../mobly/snippet/rpc/MethodDescriptor.java | 24 +++++++++++++--------- .../google/android/mobly/snippet/rpc/RpcError.java | 4 ++++ 2 files changed, 18 insertions(+), 10 deletions(-) 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 55a73ce..467053d 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 @@ -27,6 +27,7 @@ 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; @@ -74,7 +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 Annotation[][] annotations = getParameterAnnotations(); final Type[] parameterTypes = getGenericParameterTypes(); final Object[] args = new Object[parameterTypes.length]; @@ -86,10 +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(annotations[i])) { - args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]); - } else if (MethodDescriptor.isOptional(annotations[i])) { - args[i] = MethodDescriptor.getOptionalValue(parameterType, annotations[i]); + } 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"); } @@ -176,7 +179,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); } } @@ -272,7 +276,7 @@ public final class MethodDescriptor { * @param parameterType parameterType * @param annotations annotations of the parameter */ - public static Object getDefaultValue(Type parameterType, Annotation[] annotations) { + public static Object getDefaultValue(Type parameterType, Iterable annotations) { for (Annotation a : annotations) { if (a instanceof RpcDefault) { RpcDefault defaultAnnotation = (RpcDefault) a; @@ -290,7 +294,7 @@ public final class MethodDescriptor { * @param parameterType parameterType * @param annotations annotations of the parameter */ - public static Object getOptionalValue(Type parameterType, Annotation[] annotations) { + public static Object getOptionalValue(Type parameterType, Iterable annotations) { for (Annotation a : annotations) { if (a instanceof RpcOptional) { return null; @@ -326,7 +330,7 @@ public final class MethodDescriptor { * * @param annotations annotations of the parameter */ - public static boolean hasDefaultValue(Annotation[] annotations) { + public static boolean hasDefaultValue(Iterable annotations) { for (Annotation a : annotations) { if (a instanceof RpcDefault) { return true; @@ -340,7 +344,7 @@ public final class MethodDescriptor { * * @param annotations annotations of the parameter */ - public static boolean isOptional(Annotation[] annotations) { + public static boolean isOptional(Iterable annotations) { for (Annotation a : annotations) { if (a instanceof RpcOptional) { return true; 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); + } } -- cgit v1.2.3 From c38e059a7750ee7af53f37e495d96889a9b4718f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 4 May 2023 08:17:26 +0200 Subject: Update README.md (#118) Fix ServerRunner vs. SnippetRunner --- examples/ex1_standalone_app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md index 1a2d23e..36b4d6e 100644 --- a/examples/ex1_standalone_app/README.md +++ b/examples/ex1_standalone_app/README.md @@ -58,7 +58,7 @@ a snippet app that controls (instruments) another app under test, please see package="com.my.app"> ... ``` -- cgit v1.2.3 From 22ce03c84d3dc5b5697302a2ef4a2ea9f96c6eeb Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Mon, 15 May 2023 18:15:10 -0700 Subject: Deprecate non-primitive array types and enable checked warnings in JsonBuilder --- .../android/mobly/snippet/rpc/JsonBuilder.java | 53 +++------------------- .../mobly/snippet/rpc/MethodDescriptor.java | 4 -- .../android/mobly/snippet/util/AndroidUtil.java | 2 - 3 files changed, 7 insertions(+), 52 deletions(-) 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 42629d8..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,21 +86,11 @@ public class JsonBuilder { } if (data instanceof Map) { // TODO(damonkohler): I would like to make this a checked cast if possible. - return buildJsonMap((Map) 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.getClass().isArray()) { return buildJSONArray(data); } @@ -115,20 +102,6 @@ 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 { JSONArray result = new JSONArray(); if (data instanceof int[]) { @@ -204,25 +177,13 @@ public class JsonBuilder { return result; } - private static JSONObject buildJsonMap(Map map) throws JSONException { + private static JSONObject buildJsonMap(Map map) throws JSONException { JSONObject result = new JSONObject(); - for (Entry 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 467053d..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 @@ -102,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 { 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(); -- cgit v1.2.3 From 5b9289c7391fbec9d7205c9b38c26593eda8b19b Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Mon, 15 May 2023 18:29:09 -0700 Subject: Snippet lib release v1.4.0 --- CHANGELOG | 5 +++++ examples/ex1_standalone_app/README.md | 2 +- examples/ex1_standalone_app/build.gradle | 2 +- examples/ex2_espresso/README.md | 2 +- examples/ex2_espresso/build.gradle | 2 +- examples/ex3_async_event/build.gradle | 2 +- examples/ex4_uiautomator/build.gradle | 2 +- examples/ex5_schedule_rpc/build.gradle | 2 +- examples/ex6_complex_type_conversion/build.gradle | 2 +- examples/ex7_default_and_optional_rpc/build.gradle | 2 +- third_party/sl4a/gradle.properties | 4 ++-- 11 files changed, 16 insertions(+), 11 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/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md index 36b4d6e..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' } ``` 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/build.gradle b/examples/ex7_default_and_optional_rpc/build.gradle index 04112c0..35e3925 100644 --- a/examples/ex7_default_and_optional_rpc/build.gradle +++ b/examples/ex7_default_and_optional_rpc/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/third_party/sl4a/gradle.properties b/third_party/sl4a/gradle.properties index ebbf2fe..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=1030200 -VERSION_NAME=1.3.2-SNAPSHOT +VERSION_CODE=1040099 +VERSION_NAME=1.4.0 GROUP_ID=com.google.android.mobly ARTIFACT_ID=mobly-snippet-lib -- cgit v1.2.3 From 21ad43df2a6b2229679586cea8bd730d7d776be3 Mon Sep 17 00:00:00 2001 From: Kolin Lu Date: Wed, 24 May 2023 21:50:21 -0700 Subject: Add xianyuanjia@ and kolinlu@ to OWNERS Change-Id: I78767cd770b6a18f943ef75573872d9a7421e2eb --- OWNERS | 2 ++ 1 file changed, 2 insertions(+) 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 -- cgit v1.2.3 From 489e6a590bd97cb0d267dcff161b96d984d78551 Mon Sep 17 00:00:00 2001 From: "Nate(Qiang) Jiang" Date: Wed, 31 May 2023 00:51:32 +0000 Subject: mobly-snippet-lib: Allow snippet install on R devices Bug: 284416663 Test: atest GtsWifiDirectTestCases on R devices Merged-In: Ia7df616d0aff82e68fd185b9691959a7832c844b Change-Id: I3b3fb9b7d9c393e142b407ceade1038a67123eb4 --- Android.bp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android.bp b/Android.bp index 436cd74..7ffced2 100644 --- a/Android.bp +++ b/Android.bp @@ -26,5 +26,5 @@ android_library { ], manifest: "third_party/sl4a/src/main/AndroidManifest.xml", sdk_version: "current", - min_sdk_version: "31", + min_sdk_version: "30", } -- cgit v1.2.3