aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAng Li <angli@google.com>2018-01-12 11:54:03 -0800
committerGitHub <noreply@github.com>2018-01-12 11:54:03 -0800
commitc2d947efa173968571cbcc11376159647a9ff356 (patch)
treec8e06f22c7dffb5ffed4f7824120fb0dff8a5354
parent4d02684ade1a312b39591a94a92328dc0b65b883 (diff)
downloadmobly-snippet-lib-c2d947efa173968571cbcc11376159647a9ff356.tar.gz
Support custom converters of non-primitive types. (#86)
Now users can supply custom logic for object serialization/de-serialization through a centralized class, and snippet lib will automatically use the custom converters. Added a new example to demonstrate this feature.
-rw-r--r--README.md3
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java30
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet2.java47
-rw-r--r--examples/ex6_complex_type_conversion/README.md108
-rw-r--r--examples/ex6_complex_type_conversion/build.gradle27
-rw-r--r--examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml23
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java21
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java36
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java57
-rw-r--r--settings.gradle3
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java39
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java42
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java65
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java21
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java46
15 files changed, 532 insertions, 36 deletions
diff --git a/README.md b/README.md
index 7e13f84..99aec9e 100644
--- a/README.md
+++ b/README.md
@@ -61,3 +61,6 @@ mobly snippet lib along with detailed tutorials.
* [ex5_schedule_rpc](examples/ex5_schedule_rpc): Example of how to use the
'scheduleRpc' RPC to execute another RPC at a later time, potentially after
device disconnection.
+* [ex6_complex_type_conversion](examples/ex6_complex_type_conversion): Example of how to pass a
+ non-primitive type to the Rpc methods and return non-primitive type from Rpc methods by
+ supplying a type converter.
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
new file mode 100644
index 0000000..6f5af65
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
@@ -0,0 +1,30 @@
+/*
+ * 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.example6;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Returns the given integer with the prefix \"foo\"")
+ public String getFoo(Integer input) {
+ return "foo " + input;
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet2.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet2.java
new file mode 100644
index 0000000..10a259e
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet2.java
@@ -0,0 +1,47 @@
+/*
+ * 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.example6;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+import com.google.android.mobly.snippet.rpc.RunOnUiThread;
+import java.io.IOException;
+
+public class ExampleSnippet2 implements Snippet {
+ @Rpc(description = "Returns the given string with the prefix \"bar\"")
+ public String getBar(String input) {
+ return "bar " + input;
+ }
+
+ @Rpc(description = "Throws an exception")
+ public String throwSomething() throws IOException {
+ throw new IOException("Example exception from throwSomething()");
+ }
+
+ @Rpc(description = "Throws an exception from the main thread")
+ // @RunOnUiThread makes this method execute on the main thread, but only has effect when
+ // invoked as an RPC. It does not affect how this method executes if invoked directly in Java.
+ // This annotation can also be applied to the constructor and the shutdown() method.
+ @RunOnUiThread
+ public String throwSomethingFromMainThread() throws IOException {
+ throw new IOException("Example exception from throwSomethingFromMainThread()");
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex6_complex_type_conversion/README.md b/examples/ex6_complex_type_conversion/README.md
new file mode 100644
index 0000000..70d0289
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/README.md
@@ -0,0 +1,108 @@
+# Complex Type Conversion in Snippet Example
+
+This tutorial shows you how to use a custom object in Snippet Lib.
+
+This example assumes basic familiarity with Snippet Lib as demonstrated in
+[Example 1](../ex1_standalone_app/README.md).
+
+## Tutorial
+
+1. Use Android Studio to create a new app project, similar to
+ [Example 1](../ex1_standalone_app/README.md).
+
+1. Create a complex type in Java:
+ ```java
+ public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+ }
+ ```
+1. Create a Java class implementing `SnippetObjectConverter`, which defines how the complex type
+ should be converted against `JSONObject`:
+ ```java
+ public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+ }
+ ```
+1. Write a Java class implementing `Snippet` and add Rpc methods that takes your complex type as
+ a parameter and another Rpc method that returns the complext type directly.
+
+ ```java
+ package com.my.app;
+ ...
+ public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. In `AndroidManifest.xml`, specify the converter class as a `meta-data` named
+ `mobly-object-converter`
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.my.app.ExampleObjectConverter" />
+ ...
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex6_complex_type_conversion:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex6_complex_type_conversion/build/outputs/apk/ex6_complex_type_conversion-debug.apk
+
+1. Use Mobly's `snippet_shell` from mobly to trigger the Rpc methods:
+
+ snippet_shell.py com.google.android.mobly.snippet.example6
+
+ >>> s.passComplexTypeToSnippet({'Value': 'Hello'})
+ 'The value in CustomType is: Hello'
+ >>> s.returnComplexTypeFromSnippet('Bye')
+ {'Value': 'Bye'}
diff --git a/examples/ex6_complex_type_conversion/build.gradle b/examples/ex6_complex_type_conversion/build.gradle
new file mode 100644
index 0000000..40d45bc
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 26
+ buildToolsVersion '26.0.2'
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example6"
+ minSdkVersion 16
+ targetSdkVersion 26
+ 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.2.0'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1548e7
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example6">
+
+ <application>
+ <!-- Required: list of all classes with @Rpc methods. -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example6.ExampleSnippet" />
+ <!-- Optional: a class used for converting Java objects to/from JSON. -->
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.google.android.mobly.snippet.example6.ExampleObjectConverter" />
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="MoblySnippetLibExample6" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example6" />
+</manifest>
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
new file mode 100644
index 0000000..223b63e
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
@@ -0,0 +1,21 @@
+package com.google.android.mobly.snippet.example6;
+
+/**
+ * A data class that defines a non-primitive type.
+ *
+ * This type is used to demonstrate serialization and de-serialization of complex type objects in
+ * Mobly Snippet Lib for Android.
+ */
+public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
new file mode 100644
index 0000000..5fc4f22
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
@@ -0,0 +1,36 @@
+package com.google.android.mobly.snippet.example6;
+
+import android.os.Bundle;
+
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+
+
+/**
+ * Example showing how to supply custom object converter to Mobly Snippet Lib.
+ */
+
+public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
new file mode 100644
index 0000000..3306f85
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
@@ -0,0 +1,57 @@
+/*
+ * 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.example6;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Example snippet showing converting complex type objects using custom logic.
+ *
+ * For complex types in Java, one can supply a custom object converter to Snippet Lib to specify how
+ * each complex type should be serialized/de-serialized. With this, users don't have to explicitly
+ * call a serializer or de-serializer in every single Rpc method, which simplifies code.
+ */
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+
+ /**
+ * Demonstrates serialization/de-serialization of a collection of custom type objects.
+ */
+ @Rpc(description = "Update values for multiple CustomType objects.")
+ public ArrayList<CustomType> updateValues(ArrayList<CustomType> objects, String newValue) {
+ for (CustomType obj : objects) {
+ obj.setMyValue(newValue);
+ }
+ return objects;
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/settings.gradle b/settings.gradle
index 7b845bc..08025db 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,5 +4,6 @@ include (
':examples:ex2_espresso',
':examples:ex3_async_event',
':examples:ex4_uiautomator',
- ':examples:ex5_schedule_rpc')
+ ':examples:ex5_schedule_rpc',
+ ':examples:ex6_complex_type_conversion')
project(":mobly-snippet-lib").projectDir = file('third_party/sl4a')
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java
new file mode 100644
index 0000000..b9a101b
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java
@@ -0,0 +1,39 @@
+package com.google.android.mobly.snippet;
+
+import java.lang.reflect.Type;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Interface for a converter that serializes and de-serializes objects.
+ *
+ * <p>Classes implementing this interface are meant to provide custom serialization/de-serialization
+ * logic for complex types.
+ *
+ * <p>Serialization here means converting a Java object to {@link JSONObject}, which can be
+ * transported over Snippet's Rpc protocol. De-serialization is this process in reverse.
+ */
+public interface SnippetObjectConverter {
+ /**
+ * Serializes a complex type object to {@link JSONObject}.
+ *
+ * <p>Return null to signify the complex type is not supported.
+ *
+ * @param object The object to convert to "serialize".
+ * @return A JSONObject representation of the input object, or `null` if the input object type
+ * is not supported.
+ * @throws JSONException
+ */
+ JSONObject serialize(Object object) throws JSONException;
+
+ /**
+ * Deserializes a {@link JSONObject} to a Java complex type object.
+ *
+ * @param jsonObject A {@link JSONObject} passed from the Rpc client.
+ * @param type The expected {@link Type} of the Java object.
+ * @return A Java object of the specified {@link Type}, or `null` if the {@link Type} is not
+ * supported.
+ * @throws JSONException
+ */
+ Object deserialize(JSONObject jsonObject, Type type) throws JSONException;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java
index 3fceb36..9632b8b 100644
--- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.SnippetObjectConverter;
import com.google.android.mobly.snippet.event.EventSnippet;
import com.google.android.mobly.snippet.rpc.MethodDescriptor;
import com.google.android.mobly.snippet.rpc.RpcMinSdk;
@@ -46,10 +47,21 @@ import java.util.TreeSet;
import java.util.concurrent.Callable;
public class SnippetManager {
- private static final String METADATA_TAG_NAME = "mobly-snippets";
+ /**
+ * Name of the XML tag specifying what snippet classes to look for RPCs in.
+ *
+ * <p>Comma delimited list of full package names for classes that implements the Snippet
+ * interface.
+ */
+ private static final String TAG_NAME_SNIPPET_LIST = "mobly-snippets";
+ /** Name of the XML tag specifying the custom object converter class to use. */
+ private static final String TAG_NAME_OBJECT_CONVERTER = "mobly-object-converter";
+
private final Map<Class<? extends Snippet>, Snippet> mSnippets;
/** A map of strings to known RPCs. */
private final Map<String, MethodDescriptor> mKnownRpcs;
+ /** The converter used to serialize and deserialize objects. */
+ private SnippetObjectConverter mObjectConverter;
private static SnippetManager sInstance = null;
private boolean mShutdown = false;
@@ -82,6 +94,8 @@ public class SnippetManager {
if (sInstance != null) {
throw new IllegalStateException("SnippetManager should not be re-initialized");
}
+ // Add custom object converter if user provided one.
+ SnippetObjectConverterManager.addConverter(findSnippetObjectConverterFromMetadata(context));
Collection<Class<? extends Snippet>> classList = findSnippetClassesFromMetadata(context);
sInstance = new SnippetManager(classList);
return sInstance;
@@ -158,7 +172,7 @@ public class SnippetManager {
return mShutdown;
}
- private static Set<Class<? extends Snippet>> findSnippetClassesFromMetadata(Context context) {
+ private static Bundle findMetadata(Context context) {
ApplicationInfo appInfo;
try {
appInfo =
@@ -170,13 +184,31 @@ public class SnippetManager {
"Failed to find ApplicationInfo with package name: "
+ context.getPackageName());
}
- Bundle metadata = appInfo.metaData;
- String snippets = metadata.getString(METADATA_TAG_NAME);
+ return appInfo.metaData;
+ }
+
+ private static Class<? extends SnippetObjectConverter> findSnippetObjectConverterFromMetadata(
+ Context context) {
+ String className = findMetadata(context).getString(TAG_NAME_OBJECT_CONVERTER);
+ if (className == null) {
+ Log.i("No object converter provided.");
+ return null;
+ }
+ try {
+ return (Class<? extends SnippetObjectConverter>) Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ Log.e("Failed to find class " + className);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Set<Class<? extends Snippet>> findSnippetClassesFromMetadata(Context context) {
+ String snippets = findMetadata(context).getString(TAG_NAME_SNIPPET_LIST);
if (snippets == null) {
throw new IllegalStateException(
"AndroidManifest.xml does not contain a <metadata> tag with "
+ "name=\""
- + METADATA_TAG_NAME
+ + TAG_NAME_SNIPPET_LIST
+ "\"");
}
String[] snippetClassNames = snippets.split("\\s*,\\s*");
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java
new file mode 100644
index 0000000..df8af53
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java
@@ -0,0 +1,65 @@
+package com.google.android.mobly.snippet.manager;
+
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Manager for classes that implement {@link SnippetObjectConverter}.
+ *
+ * <p>This class is created to separate how Snippet Lib handles object conversion internally from
+ * how the conversion scheme for complex types is defined for users.
+ *
+ * <p>Snippet Lib can pull in the custom serializers and deserializers through here in various
+ * stages of execution, whereas users can have a clean interface for supplying these methods without
+ * worrying about internal states of Snippet Lib.
+ *
+ * <p>This gives us the flexibility of changing Snippet Lib internal structure or expanding support
+ * without impacting users. E.g. we can support multiple converter classes in the future.
+ */
+public class SnippetObjectConverterManager {
+ private static SnippetObjectConverter mConverter;
+ private static volatile SnippetObjectConverterManager mManager;
+
+ private SnippetObjectConverterManager() {}
+
+ public static synchronized SnippetObjectConverterManager getInstance() {
+ if (mManager == null) {
+ mManager = new SnippetObjectConverterManager();
+ }
+ return mManager;
+ }
+
+ static void addConverter(Class<? extends SnippetObjectConverter> converterClass) {
+ if (mConverter != null) {
+ throw new RuntimeException("A converter has been added, cannot add again.");
+ }
+ try {
+ mConverter = converterClass.getConstructor().newInstance();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("No default constructor found for the converter class.");
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Object objectToJson(Object object) throws JSONException {
+ if (mConverter == null) {
+ return null;
+ }
+ return mConverter.serialize(object);
+ }
+
+ public Object jsonToObject(JSONObject jsonObject, Type type) throws JSONException {
+ if (mConverter == null) {
+ return null;
+ }
+ return mConverter.deserialize(jsonObject, type);
+ }
+}
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 d005530..5b0065a 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,6 +21,7 @@ import android.content.Intent;
import android.net.Uri;
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;
@@ -36,6 +37,8 @@ import org.json.JSONObject;
public class JsonBuilder {
+ private JsonBuilder() {}
+
@SuppressWarnings("unchecked")
public static Object build(Object data) throws JSONException {
if (data == null) {
@@ -69,11 +72,11 @@ public class JsonBuilder {
return data;
}
if (data instanceof Set<?>) {
- List<Object> items = new ArrayList<Object>((Set<?>) data);
+ List<Object> items = new ArrayList<>((Set<?>) data);
return buildJsonList(items);
}
if (data instanceof Collection<?>) {
- List<Object> items = new ArrayList<Object>((Collection<?>) data);
+ List<Object> items = new ArrayList<>((Collection<?>) data);
return buildJsonList(items);
}
if (data instanceof List<?>) {
@@ -86,13 +89,13 @@ public class JsonBuilder {
return buildJsonIntent((Intent) data);
}
if (data instanceof Map<?, ?>) {
- // TODO(damonkohler): I would like to make this a checked cast if
- // possible.
+ // TODO(damonkohler): I would like to make this a checked cast if possible.
return buildJsonMap((Map<String, ?>) 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);
}
@@ -112,7 +115,11 @@ public class JsonBuilder {
if (data instanceof Object[]) {
return buildJSONArray((Object[]) data);
}
-
+ // Try with custom converter provided by user.
+ Object result = SnippetObjectConverterManager.getInstance().objectToJson(data);
+ if (result != null) {
+ return result;
+ }
return data.toString();
}
@@ -195,8 +202,4 @@ public class JsonBuilder {
private static JSONObject buildUri(Uri uri) throws JSONException {
return new JSONObject().put("Uri", build((uri != null) ? uri.toString() : ""));
}
-
- private JsonBuilder() {
- // This is a utility class.
- }
}
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 185b7be..ca836fd 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
@@ -20,6 +20,7 @@ import android.content.Intent;
import android.net.Uri;
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.reflect.Method;
import java.lang.reflect.Type;
@@ -36,7 +37,7 @@ public final class MethodDescriptor {
private final Method mMethod;
private final Class<? extends Snippet> mClass;
- public MethodDescriptor(Class<? extends Snippet> clazz, Method method) {
+ private MethodDescriptor(Class<? extends Snippet> clazz, Method method) {
mClass = clazz;
mMethod = method;
}
@@ -86,16 +87,12 @@ public final class MethodDescriptor {
return manager.invoke(mClass, mMethod, args);
}
- /**
- * Converts a parameter from JSON into a Java Object.
- *
- * @return TODO
- */
+ /** 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
- static Object convertParameter(final JSONArray parameters, int index, Type type)
+ private static Object convertParameter(final JSONArray parameters, int index, Type type)
throws JSONException, RpcError {
try {
// We must handle null and numbers explicitly because we cannot magically cast them. We
@@ -116,6 +113,8 @@ public final class MethodDescriptor {
return parameters.getInt(index);
} else if (type == Intent.class) {
return buildIntent(parameters.getJSONObject(index));
+ } else if (type == String.class) {
+ return parameters.getString(index);
} else if (type == Integer[].class || type == int[].class) {
JSONArray list = parameters.getJSONArray(index);
Integer[] result = new Integer[list.length()];
@@ -140,6 +139,13 @@ public final class MethodDescriptor {
} else if (type == JSONObject.class) {
return parameters.getJSONObject(index);
} else {
+ // Try any custom converter provided.
+ Object object =
+ SnippetObjectConverterManager.getInstance()
+ .jsonToObject(parameters.getJSONObject(index), type);
+ if (object != null) {
+ return object;
+ }
// Magically cast the parameter to the right Java type.
return ((Class<?>) type).cast(parameters.get(index));
}
@@ -154,7 +160,7 @@ public final class MethodDescriptor {
}
}
- public static Object buildIntent(JSONObject jsonObject) throws JSONException {
+ private static Object buildIntent(JSONObject jsonObject) throws JSONException {
Intent intent = new Intent();
if (jsonObject.has("action")) {
intent.setAction(jsonObject.getString("action"));
@@ -191,7 +197,7 @@ public final class MethodDescriptor {
return mMethod.getName();
}
- public Type[] getGenericParameterTypes() {
+ private Type[] getGenericParameterTypes() {
return mMethod.getGenericParameterTypes();
}
@@ -199,7 +205,7 @@ public final class MethodDescriptor {
return mMethod.isAnnotationPresent(AsyncRpc.class);
}
- public Class<? extends Snippet> getSnippetClass() {
+ Class<? extends Snippet> getSnippetClass() {
return mClass;
}
@@ -216,7 +222,7 @@ public final class MethodDescriptor {
*
* @return derived help string
*/
- public String getHelp() {
+ String getHelp() {
StringBuilder paramBuilder = new StringBuilder();
Class<?>[] parameterTypes = mMethod.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
@@ -225,15 +231,13 @@ public final class MethodDescriptor {
}
paramBuilder.append(parameterTypes[i].getSimpleName());
}
- String help =
- String.format(
- Locale.US,
- "%s %s(%s) returns %s // %s",
- isAsync() ? "@AsyncRpc" : "@Rpc",
- mMethod.getName(),
- paramBuilder,
- mMethod.getReturnType().getSimpleName(),
- getAnnotationDescription());
- return help;
+ return String.format(
+ Locale.US,
+ "%s %s(%s) returns %s // %s",
+ isAsync() ? "@AsyncRpc" : "@Rpc",
+ mMethod.getName(),
+ paramBuilder,
+ mMethod.getReturnType().getSimpleName(),
+ getAnnotationDescription());
}
}