diff options
Diffstat (limited to 'third_party/sl4a/src/main/java')
5 files changed, 178 insertions, 35 deletions
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()); } } |