blob: 3ac9246f18de215dd4c09b8cd4ca42be78ece636 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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 android.platform.test.helpers;
import android.app.Instrumentation;
import android.content.Context;
import android.util.Log;
import dalvik.system.DexFile;
import java.io.File;
import java.io.IOException;
import java.lang.ClassLoader;
import java.lang.ClassNotFoundException;
import java.lang.IllegalAccessException;
import java.lang.InstantiationException;
import java.lang.NoSuchMethodException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* The HelperManager class is used to remove any explicit or hard-coded dependencies on app helper
* implementations. Instead, it provides a method for abstracting helper instantiation by specifying
* a base class for which you require an implementation. The manager effectively searches for a
* suitable implementation using runtime class loading.
* <p>
* The class provides two means for finding the necessary classes:
* <ol>
* <li> Static inclusion - if all the code is included in the final APK, the Context can be used to
* generate a HelperManager and to instantiate implementations.
* <li> Dexed file inclusion - if this and the helper implementations are bundled into a dexed
* file, such as a dexed JAR, that gets added after compilation, e.g. how the automated user
* profile test suites are built, the path can be used to generate a HelperManager and to
* instantiate implementations.
* </ol>
* <p>
* Including and using this strategy will prune the explicit dependency tree for the App Helper
* Library and provide a more robust library for use across the Android source tree.
*/
public class HelperManager {
private static final String LOG_TAG = HelperManager.class.getSimpleName();
private static HelperManager sInstance;
/**
* Returns an instance of the HelperManager that searches the supplied application context for
* classes to instantiate to helper implementations.
*
* @param context the application context to search
* @param instr the active instrumentation
* @return a new instance of the HelperManager class
*/
public static HelperManager getInstance(Context context, Instrumentation instr) {
if (sInstance == null) {
// Input checks
if (context == null) {
throw new NullPointerException("Cannot pass in a null context.");
}
if (instr == null) {
throw new NullPointerException(
String.format("Cannot pass in null instrumentation."));
}
// Instantiation
sInstance = new HelperManager(context.getPackageCodePath(), instr);
}
return sInstance;
}
/**
* Returns an instance of the HelperManager that searches the supplied location for classes to
* instantiate to helper implementations.
*
* @param path the dexed file where the classes are included
* @param instr the active instrumentation
* @throws IllegalArgumentException if the path is not a valid file
* @return a new instance of the HelperManager class
*/
public static HelperManager getInstance(String path, Instrumentation instr) {
if (sInstance == null) {
// Input checks
if (!(new File(path)).exists()) {
throw new IllegalArgumentException(
String.format("No file found at path: %s.", path));
}
if (instr == null) {
throw new NullPointerException(
String.format("Cannot pass in null instrumentation."));
}
// Instantiation
sInstance = new HelperManager(path, instr);
}
return sInstance;
}
private Instrumentation mInstrumentation;
private List<String> mClasses;
private HelperManager(String path, Instrumentation instr) {
mInstrumentation = instr;
// Collect all of the available classes
try {
DexFile dex = new DexFile(path);
mClasses = Collections.list(dex.entries());
} catch (IOException e) {
throw new RuntimeException("Failed to retrieve the dex file.");
}
}
/**
* Returns a concrete implementation of the helper interface supplied, if available.
*
* @param base the interface base class to find an implementation for
* @throws RuntimeException if no implementation is found
* @return a concrete implementation of base
*/
public <T extends AbstractStandardAppHelper> T get(Class<T> base) {
ClassLoader loader = HelperManager.class.getClassLoader();
// Iterate and search for the implementation
for (String className : mClasses) {
Class<?> clazz = null;
try {
clazz = loader.loadClass(className);
} catch (ClassNotFoundException e) {
Log.w(LOG_TAG, String.format("Class not found: %s", className));
}
if (base.isAssignableFrom(clazz) && !clazz.equals(base)) {
// Instantiate the implementation class and return
try {
Constructor<?> constructor = clazz.getConstructor(Instrumentation.class);
return (T)constructor.newInstance(mInstrumentation);
} catch (NoSuchMethodException e) {
Log.w(LOG_TAG, String.format("Failed to find a matching constructor for %s",
className), e);
} catch (IllegalAccessException e) {
Log.w(LOG_TAG, String.format("Failed to access the constructor %s",
className), e);
} catch (InstantiationException e) {
Log.w(LOG_TAG, String.format("Failed to instantiate %s",
className), e);
} catch (InvocationTargetException e) {
Log.w(LOG_TAG, String.format("Exception encountered instantiating %s",
className), e);
}
}
}
throw new RuntimeException(
String.format("Failed to find an implementation for %s", base.toString()));
}
}