16

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();
        }
   }
user210504
  • 1,749
  • 2
  • 17
  • 36
  • Is it acceptable to aapt the apk first, and then parse the dex file? If this is acceptable in your case, I could write an answer. – StarPinkER Mar 16 '13 at 06:52
  • Nah, I am trying to expose some methods to plugins dynamically and I wont know what classes are present before hand. – user210504 Mar 16 '13 at 14:08
  • Can you elaborate? not quite understand your scenario – StarPinkER Mar 17 '13 at 00:38
  • I want to load classes from a package and make them available based on policy configuration as received from the server. But I want to do it in a generic way such that I dont have to know the classes which are part of the app. – user210504 Mar 17 '13 at 01:11

7 Answers7

25

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.

tao
  • 299
  • 2
  • 8
16

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

Community
  • 1
  • 1
Sergey Shustikov
  • 15,377
  • 12
  • 67
  • 119
  • You should return the complete name of the class like `packagename.ClassName` because the person using this would want to use it in such ways `Class.forName(className)`. – lovesh Jul 19 '15 at 17:44
  • 1
    @lovesh it depends from your goal. Actually you can change code for your needs. – Sergey Shustikov Jul 19 '15 at 19:49
  • 1
    Holy cow. I wish I could find more ways to award points to this answer. I searched for **days** for this and only found it because someone linked to it from https://github.com/ronmamo/reflections/issues/115. And it's so easy! I'm thinking of going back to all the other SO posts and linking to here. By the way, for those who care, this method can return all of the classes for _all_ packages in the application if you set `packageName` to "com.myapp", assuming all packages start with that. It's also easy to modify to return fully qualified names and skip inner classes (for `Class.forName()`). – Mark Cramer Mar 15 '16 at 05:45
  • 3
    DexFile was deprecated: This API will be removed in a future Android release. https://developer.android.com/reference/dalvik/system/DexFile.html – hector6872 Aug 18 '17 at 09:57
  • Is there a modern solution that works for Gradle 2+? – Joshua Pinter Jul 04 '19 at 04:22
3

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();
fantouch
  • 1,159
  • 14
  • 12
2

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) }
konakid
  • 656
  • 5
  • 7
1

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);
    }
  }
}
armansimonyan13
  • 956
  • 9
  • 15
0

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?

Stefano Liboni
  • 149
  • 2
  • 11
-1

try this..

   Reflections reflections = new Reflections("my.project.prefix");

   Set<Class<? extends Object>> allClasses 
                        = reflections.getSubTypesOf(Object.class);
stinepike
  • 54,068
  • 14
  • 92
  • 112
Mahi
  • 1,754
  • 2
  • 16
  • 37