Reputation: 1759
How can i find all classes inside a package on Android? I use PathClassLoader, but it always returns an empty enumeration?
Additional info
Tried the suggested Reflections approach. Couple of important points about reflections library. The library available through maven central is not compatible with Android and gives dex errors. I had to include source and compile dom4j, java-assist.
The problem with reflections, and my original solution is that PathClassLoader in android returns an empty enumeration for package.
The issue with approach is that getResource in Android is always returning empty enumeration.
final String resourceName = aClass.getName().replace(".", "/") + ".class";
for (ClassLoader classLoader : loaders) {
try {
final URL url = classLoader.getResource(resourceName);
if (url != null) {
final String normalizedUrl = url.toExternalForm().substring(0, url.toExternalForm().lastIndexOf(aClass.getPackage().getName().replace(".", "/")));
return new URL(normalizedUrl);
}
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
Upvotes: 16
Views: 24435
Reputation: 656
These approaches are generally outdated and don't work with multi dex apps. To support multidex you can get all of the DexFiles like this
internal fun getDexFiles(context: Context): Sequence<DexFile> {
// Here we do some reflection to access the dex files from the class loader. These implementation details vary by platform version,
// so we have to be a little careful, but not a huge deal since this is just for testing. It should work on 21+.
// The source for reference is at:
// https://android.googlesource.com/platform/libcore/+/oreo-release/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
val classLoader = context.classLoader as BaseDexClassLoader
val pathListField = field("dalvik.system.BaseDexClassLoader", "pathList")
val pathList = pathListField.get(classLoader) // Type is DexPathList
val dexElementsField = field("dalvik.system.DexPathList", "dexElements")
@Suppress("UNCHECKED_CAST")
val dexElements = dexElementsField.get(pathList) as Array<Any> // Type is Array<DexPathList.Element>
val dexFileField = field("dalvik.system.DexPathList\$Element", "dexFile")
return dexElements.map {
dexFileField.get(it) as DexFile
}.asSequence()
}
private fun field(className: String, fieldName: String): Field {
val clazz = Class.forName(className)
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
return field
}
From there you can use it like this to get all classes in a package
getDexFiles(context)
.flatMap { it.entries().asSequence() }
.filter { it.startsWith("my.package.name") }
.map { context.classLoader.loadClass(it) }
Upvotes: 2
Reputation: 149
I found the same solution of Sergey Shustikov, with the package name derived form the context and handles inner classes. Unfortunally while it's working on Galaxy Nexus, it's not working on Nexus 6 and Nexus 6p (not tested on other devices).
Here's the code:
private HashMap<String, String> loadClassFullnames() {
final HashMap<String, String> ret = new HashMap<>();
try {
final String thisPackage = context.getPackageName();
final DexFile tmp = new DexFile(context.getPackageCodePath());
for (Enumeration<String> iter = tmp.entries(); iter.hasMoreElements(); ) {
final String classFullname = iter.nextElement();
if (classFullname.startsWith(thisPackage)) {
// TODO filter out also anonymous classes (es Class1$51)
final int index = classFullname.lastIndexOf(".");
final String c = (index >= 0) ? classFullname.substring(index + 1) : classFullname;
ret.put(c.replace('$', '.'), classFullname);
}
}
} catch (Throwable ex) {
Debug.w("Unable to collect class fullnames. Reason: " + ex.getCause() + "\n\rStack Trace:\n\r" + ((ex.getCause() != null)? ex.getCause().getStackTrace(): "none"));
}
return ret;
}
It seams that on Galaxy Nexus, the DexFile contains also the application packages (the one we are trying to find!), while on Nexus 6[p] no package from matching thisPackage are found while some other packages are there... Does anybody have the same problem?
Upvotes: 0
Reputation: 15821
Actually i found solution. Thanks for tao reply.
private String[] getClassesOfPackage(String packageName) {
ArrayList<String> classes = new ArrayList<String>();
try {
String packageCodePath = getPackageCodePath();
DexFile df = new DexFile(packageCodePath);
for (Enumeration<String> iter = df.entries(); iter.hasMoreElements(); ) {
String className = iter.nextElement();
if (className.contains(packageName)) {
classes.add(className.substring(className.lastIndexOf(".") + 1, className.length()));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes.toArray(new String[classes.size()]);
}
Tested on Android 5.0 Lolipop
Upvotes: 17
Reputation: 1169
Here's a solution that bring you not only the class name,
but also the Class<?>
object.
And while PathClassLoader.class.getDeclaredField("mDexs")
fails frequently,
new DexFile(getContext().getPackageCodePath())
seems much more stable.
public abstract class ClassScanner {
private static final String TAG = "ClassScanner";
private Context mContext;
public ClassScanner(Context context) {
mContext = context;
}
public Context getContext() {
return mContext;
}
void scan() throws IOException, ClassNotFoundException, NoSuchMethodException {
long timeBegin = System.currentTimeMillis();
PathClassLoader classLoader = (PathClassLoader) getContext().getClassLoader();
//PathClassLoader classLoader = (PathClassLoader) Thread.currentThread().getContextClassLoader();//This also works good
DexFile dexFile = new DexFile(getContext().getPackageCodePath());
Enumeration<String> classNames = dexFile.entries();
while (classNames.hasMoreElements()) {
String className = classNames.nextElement();
if (isTargetClassName(className)) {
//Class<?> aClass = Class.forName(className);//java.lang.ExceptionInInitializerError
//Class<?> aClass = Class.forName(className, false, classLoader);//tested on 魅蓝Note(M463C)_Android4.4.4 and Mi2s_Android5.1.1
Class<?> aClass = classLoader.loadClass(className);//tested on 魅蓝Note(M463C)_Android4.4.4 and Mi2s_Android5.1.1
if (isTargetClass(aClass)) {
onScanResult(aClass);
}
}
}
long timeEnd = System.currentTimeMillis();
long timeElapsed = timeEnd - timeBegin;
Log.d(TAG, "scan() cost " + timeElapsed + "ms");
}
protected abstract boolean isTargetClassName(String className);
protected abstract boolean isTargetClass(Class clazz);
protected abstract void onScanResult(Class clazz);
}
and this is a example how to use:
new ClassScanner(context) {
@Override
protected boolean isTargetClassName(String className) {
return className.startsWith(getContext().getPackageName())//I want classes under my package
&& !className.contains("$");//I don't need none-static inner classes
}
@Override
protected boolean isTargetClass(Class clazz) {
return AbsFactory.class.isAssignableFrom(clazz)//I want subclasses of AbsFactory
&& !Modifier.isAbstract(clazz.getModifiers());//I don't want abstract classes
}
@Override
protected void onScanResult(Class clazz) {
Constructor constructor = null;
try {
constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}.scan();
Upvotes: 3
Reputation: 966
This is taken from http://mindtherobot.com/
Code example
Say we have an annotation called Foo and few classes decorated with it:
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
String value();
}
@Foo("person")
public class Person {
}
@Foo("order")
public class Order {
}
Here’s a simple class that goes over all classes in your app in runtime, finds those marked with @Foo and get the value of the annotation. Do not copy and paste this code into your app – it has a lot to be fixed and added, as described below the code.
public class ClasspathScanner {
private static final String TAG = ClasspathScanner.class.getSimpleName();
private static Field dexField;
static {
try {
dexField = PathClassLoader.class.getDeclaredField("mDexs");
dexField.setAccessible(true);
} catch (Exception e) {
// TODO (1): handle this case gracefully - nobody promised that this field will always be there
Log.e(TAG, "Failed to get mDexs field");
}
}
public void run() {
try {
// TODO (2): check here - in theory, the class loader is not required to be a PathClassLoader
PathClassLoader classLoader = (PathClassLoader) Thread.currentThread().getContextClassLoader();
DexFile[] dexs = (DexFile[]) dexField.get(classLoader);
for (DexFile dex : dexs) {
Enumeration<String> entries = dex.entries();
while (entries.hasMoreElements()) {
// (3) Each entry is a class name, like "foo.bar.MyClass"
String entry = entries.nextElement();
Log.d(TAG, "Entry: " + entry);
// (4) Load the class
Class<?> entryClass = dex.loadClass(entry, classLoader);
if (entryClass != null) {
Foo annotation = entryClass.getAnnotation(Foo.class);
if (annotation != null) {
Log.d(TAG, entry + ": " + annotation.value());
}
}
}
}
} catch (Exception e) {
// TODO (5): more precise error handling
Log.e(TAG, "Error", e);
}
}
}
Upvotes: 1
Reputation: 299
Using DexFile to list all classes in your apk:
try {
DexFile df = new DexFile(context.getPackageCodePath());
for (Enumeration<String> iter = df.entries(); iter.hasMoreElements();) {
String s = iter.nextElement();
}
} catch (IOException e) {
e.printStackTrace();
}
the rest is using regexp or something else to filter out your desired class.
Upvotes: 25
Reputation: 1784
try this..
Reflections reflections = new Reflections("my.project.prefix");
Set<Class<? extends Object>> allClasses
= reflections.getSubTypesOf(Object.class);
Upvotes: -1