diff options
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java')
-rw-r--r-- | third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java | 290 |
1 files changed, 290 insertions, 0 deletions
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 new file mode 100644 index 0000000..7707de2 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java @@ -0,0 +1,290 @@ +/* + * 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.manager; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +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; +import com.google.android.mobly.snippet.rpc.RunOnUiThread; +import com.google.android.mobly.snippet.schedulerpc.ScheduleRpcSnippet; +import com.google.android.mobly.snippet.util.Log; +import com.google.android.mobly.snippet.util.MainThread; +import com.google.android.mobly.snippet.util.SnippetLibException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Callable; + +public class SnippetManager { + /** + * 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; + + private static SnippetManager sInstance = null; + private boolean mShutdown = false; + + private SnippetManager(Collection<Class<? extends Snippet>> classList) { + // Synchronized for multiple connections on the same session. Can't use ConcurrentHashMap + // because we have to put in a value of 'null' before the class is constructed, but + // ConcurrentHashMap does not allow null values. + mSnippets = Collections.synchronizedMap(new HashMap<Class<? extends Snippet>, Snippet>()); + Map<String, MethodDescriptor> knownRpcs = new HashMap<>(); + for (Class<? extends Snippet> receiverClass : classList) { + mSnippets.put(receiverClass, null); + Collection<MethodDescriptor> methodList = MethodDescriptor.collectFrom(receiverClass); + for (MethodDescriptor m : methodList) { + if (knownRpcs.containsKey(m.getName())) { + // We already know an RPC of the same name. We don't catch this anywhere because + // this is a programming error. + throw new RuntimeException( + "An RPC with the name " + m.getName() + " is already known."); + } + knownRpcs.put(m.getName(), m); + } + } + // Does not need to be concurrent because this map is read only, so it is safe to access + // from multiple threads. Wrap in an unmodifiableMap to enforce this. + mKnownRpcs = Collections.unmodifiableMap(knownRpcs); + } + + public static synchronized SnippetManager initSnippetManager(Context context) { + if (sInstance != null) { + throw new IllegalStateException("SnippetManager should not be re-initialized"); + } + // Add custom object converter if user provided one. + Class<? extends SnippetObjectConverter> converterClazz = + findSnippetObjectConverterFromMetadata(context); + if (converterClazz != null) { + Log.d("Found custom converter class, adding..."); + SnippetObjectConverterManager.addConverter(converterClazz); + } + Collection<Class<? extends Snippet>> classList = findSnippetClassesFromMetadata(context); + sInstance = new SnippetManager(classList); + return sInstance; + } + + public static SnippetManager getInstance() { + if (sInstance == null) { + throw new IllegalStateException("getInstance() called before init()"); + } + if (sInstance.isShutdown()) { + throw new IllegalStateException("shutdown() called before getInstance()"); + } + return sInstance; + } + + public MethodDescriptor getMethodDescriptor(String methodName) { + return mKnownRpcs.get(methodName); + } + + public SortedSet<String> getMethodNames() { + return new TreeSet<>(mKnownRpcs.keySet()); + } + + public Object invoke(Class<? extends Snippet> clazz, Method method, Object[] args) + throws Throwable { + if (method.isAnnotationPresent(RpcMinSdk.class)) { + int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value(); + if (Build.VERSION.SDK_INT < requiredSdkLevel) { + throw new SnippetLibException( + String.format( + Locale.US, + "%s requires API level %d, current level is %d", + method.getName(), + requiredSdkLevel, + Build.VERSION.SDK_INT)); + } + } + Snippet object; + try { + object = get(clazz); + return invoke(object, method, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + public void shutdown() throws Exception { + for (final Entry<Class<? extends Snippet>, Snippet> entry : mSnippets.entrySet()) { + if (entry.getValue() == null) { + continue; + } + Method method = entry.getKey().getMethod("shutdown"); + if (method.isAnnotationPresent(RunOnUiThread.class)) { + Log.d("Shutting down " + entry.getKey().getName() + " on the main thread"); + MainThread.run( + new Callable<Void>() { + @Override + public Void call() throws Exception { + entry.getValue().shutdown(); + return null; + } + }); + } else { + Log.d("Shutting down " + entry.getKey().getName()); + entry.getValue().shutdown(); + } + } + mSnippets.clear(); + mKnownRpcs.clear(); + mShutdown = true; + } + + public boolean isShutdown() { + return mShutdown; + } + + private static Bundle findMetadata(Context context) { + ApplicationInfo appInfo; + try { + appInfo = + context.getPackageManager() + .getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException( + "Failed to find ApplicationInfo with package name: " + + context.getPackageName()); + } + 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.forName(className).asSubclass(SnippetObjectConverter.class); + } catch (ClassNotFoundException | ClassCastException 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=\"" + + TAG_NAME_SNIPPET_LIST + + "\""); + } + String[] snippetClassNames = snippets.split("\\s*,\\s*"); + Set<Class<? extends Snippet>> receiverSet = new HashSet<>(); + /** Add the event snippet class which is provided within the Snippet Lib. */ + receiverSet.add(EventSnippet.class); + /** Add the schedule RPC snippet class which is provided within the Snippet Lib. */ + receiverSet.add(ScheduleRpcSnippet.class); + for (String snippetClassName : snippetClassNames) { + try { + Log.i("Trying to load Snippet class: " + snippetClassName); + Class<? extends Snippet> snippetClass = + Class.forName(snippetClassName).asSubclass(Snippet.class); + receiverSet.add(snippetClass); + } catch (ClassNotFoundException | ClassCastException e) { + Log.e("Failed to find class " + snippetClassName); + throw new RuntimeException(e); + } + } + if (receiverSet.isEmpty()) { + throw new IllegalStateException("Found no subclasses of Snippet."); + } + return receiverSet; + } + + private Snippet get(Class<? extends Snippet> clazz) throws Exception { + Snippet snippetImpl = mSnippets.get(clazz); + if (snippetImpl == null) { + // First time calling an RPC for this snippet; construct an instance under lock. + synchronized (clazz) { + snippetImpl = mSnippets.get(clazz); + if (snippetImpl == null) { + final Constructor<? extends Snippet> constructor = clazz.getConstructor(); + if (constructor.isAnnotationPresent(RunOnUiThread.class)) { + Log.d("Constructing " + clazz + " on the main thread"); + snippetImpl = + MainThread.run( + new Callable<Snippet>() { + @Override + public Snippet call() throws Exception { + return constructor.newInstance(); + } + }); + } else { + Log.d("Constructing " + clazz); + snippetImpl = constructor.newInstance(); + } + mSnippets.put(clazz, snippetImpl); + } + } + } + return snippetImpl; + } + + private Object invoke(final Snippet snippetImpl, final Method method, final Object[] args) + throws Exception { + if (method.isAnnotationPresent(RunOnUiThread.class)) { + Log.d( + "Invoking RPC method " + + method.getDeclaringClass() + + "#" + + method.getName() + + " on the main thread"); + return MainThread.run( + new Callable<Object>() { + @Override + public Object call() throws Exception { + return method.invoke(snippetImpl, args); + } + }); + } else { + Log.d("Invoking RPC method " + method.getDeclaringClass() + "#" + method.getName()); + return method.invoke(snippetImpl, args); + } + } +} |