Home | History | Annotate | Download | only in test
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.test;
     18 
     19 import android.util.Log;
     20 import com.google.android.collect.Maps;
     21 import com.google.android.collect.Sets;
     22 import dalvik.system.DexFile;
     23 
     24 import java.io.File;
     25 import java.io.IOException;
     26 import java.util.Enumeration;
     27 import java.util.Map;
     28 import java.util.Set;
     29 import java.util.TreeSet;
     30 import java.util.regex.Pattern;
     31 import java.util.zip.ZipEntry;
     32 import java.util.zip.ZipFile;
     33 
     34 /**
     35  * Generate {@link ClassPathPackageInfo}s by scanning apk paths.
     36  *
     37  * {@hide} Not needed for 1.0 SDK.
     38  */
     39 public class ClassPathPackageInfoSource {
     40 
     41     private static final String CLASS_EXTENSION = ".class";
     42 
     43     private static final ClassLoader CLASS_LOADER
     44             = ClassPathPackageInfoSource.class.getClassLoader();
     45 
     46     private final SimpleCache<String, ClassPathPackageInfo> cache =
     47             new SimpleCache<String, ClassPathPackageInfo>() {
     48                 @Override
     49                 protected ClassPathPackageInfo load(String pkgName) {
     50                     return createPackageInfo(pkgName);
     51                 }
     52             };
     53 
     54     // The class path of the running application
     55     private final String[] classPath;
     56     private static String[] apkPaths;
     57 
     58     // A cache of jar file contents
     59     private final Map<File, Set<String>> jarFiles = Maps.newHashMap();
     60     private ClassLoader classLoader;
     61 
     62     ClassPathPackageInfoSource() {
     63         classPath = getClassPath();
     64     }
     65 
     66 
     67     public static void setApkPaths(String[] apkPaths) {
     68         ClassPathPackageInfoSource.apkPaths = apkPaths;
     69     }
     70 
     71     public ClassPathPackageInfo getPackageInfo(String pkgName) {
     72         return cache.get(pkgName);
     73     }
     74 
     75     private ClassPathPackageInfo createPackageInfo(String packageName) {
     76         Set<String> subpackageNames = new TreeSet<String>();
     77         Set<String> classNames = new TreeSet<String>();
     78         Set<Class<?>> topLevelClasses = Sets.newHashSet();
     79         findClasses(packageName, classNames, subpackageNames);
     80         for (String className : classNames) {
     81             if (className.endsWith(".R") || className.endsWith(".Manifest")) {
     82                 // Don't try to load classes that are generated. They usually aren't in test apks.
     83                 continue;
     84             }
     85 
     86             try {
     87                 // We get errors in the emulator if we don't use the caller's class loader.
     88                 topLevelClasses.add(Class.forName(className, false,
     89                         (classLoader != null) ? classLoader : CLASS_LOADER));
     90             } catch (ClassNotFoundException e) {
     91                 // Should not happen unless there is a generated class that is not included in
     92                 // the .apk.
     93                 Log.w("ClassPathPackageInfoSource", "Cannot load class. "
     94                         + "Make sure it is in your apk. Class name: '" + className
     95                         + "'. Message: " + e.getMessage(), e);
     96             }
     97         }
     98         return new ClassPathPackageInfo(this, packageName, subpackageNames,
     99                 topLevelClasses);
    100     }
    101 
    102     /**
    103      * Finds all classes and sub packages that are below the packageName and
    104      * add them to the respective sets. Searches the package on the whole class
    105      * path.
    106      */
    107     private void findClasses(String packageName, Set<String> classNames,
    108             Set<String> subpackageNames) {
    109         String packagePrefix = packageName + '.';
    110         String pathPrefix = packagePrefix.replace('.', '/');
    111 
    112         for (String entryName : classPath) {
    113             File classPathEntry = new File(entryName);
    114 
    115             // Forge may not have brought over every item in the classpath. Be
    116             // polite and ignore missing entries.
    117             if (classPathEntry.exists()) {
    118                 try {
    119                     if (entryName.endsWith(".apk")) {
    120                         findClassesInApk(entryName, packageName, classNames, subpackageNames);
    121                     } else {
    122                         // scan the directories that contain apk files.
    123                         for (String apkPath : apkPaths) {
    124                             File file = new File(apkPath);
    125                             scanForApkFiles(file, packageName, classNames, subpackageNames);
    126                         }
    127                     }
    128                 } catch (IOException e) {
    129                     throw new AssertionError("Can't read classpath entry " +
    130                             entryName + ": " + e.getMessage());
    131                 }
    132             }
    133         }
    134     }
    135 
    136     private void scanForApkFiles(File source, String packageName,
    137             Set<String> classNames, Set<String> subpackageNames) throws IOException {
    138         if (source.getPath().endsWith(".apk")) {
    139             findClassesInApk(source.getPath(), packageName, classNames, subpackageNames);
    140         } else {
    141             File[] files = source.listFiles();
    142             if (files != null) {
    143                 for (File file : files) {
    144                     scanForApkFiles(file, packageName, classNames, subpackageNames);
    145                 }
    146             }
    147         }
    148     }
    149 
    150     /**
    151      * Finds all classes and sub packages that are below the packageName and
    152      * add them to the respective sets. Searches the package in a class directory.
    153      */
    154     private void findClassesInDirectory(File classDir,
    155             String packagePrefix, String pathPrefix, Set<String> classNames,
    156             Set<String> subpackageNames)
    157             throws IOException {
    158         File directory = new File(classDir, pathPrefix);
    159 
    160         if (directory.exists()) {
    161             for (File f : directory.listFiles()) {
    162                 String name = f.getName();
    163                 if (name.endsWith(CLASS_EXTENSION) && isToplevelClass(name)) {
    164                     classNames.add(packagePrefix + getClassName(name));
    165                 } else if (f.isDirectory()) {
    166                     subpackageNames.add(packagePrefix + name);
    167                 }
    168             }
    169         }
    170     }
    171 
    172     /**
    173      * Finds all classes and sub packages that are below the packageName and
    174      * add them to the respective sets. Searches the package in a single jar file.
    175      */
    176     private void findClassesInJar(File jarFile, String pathPrefix,
    177             Set<String> classNames, Set<String> subpackageNames)
    178             throws IOException {
    179         Set<String> entryNames = getJarEntries(jarFile);
    180         // check if the Jar contains the package.
    181         if (!entryNames.contains(pathPrefix)) {
    182             return;
    183         }
    184         int prefixLength = pathPrefix.length();
    185         for (String entryName : entryNames) {
    186             if (entryName.startsWith(pathPrefix)) {
    187                 if (entryName.endsWith(CLASS_EXTENSION)) {
    188                     // check if the class is in the package itself or in one of its
    189                     // subpackages.
    190                     int index = entryName.indexOf('/', prefixLength);
    191                     if (index >= 0) {
    192                         String p = entryName.substring(0, index).replace('/', '.');
    193                         subpackageNames.add(p);
    194                     } else if (isToplevelClass(entryName)) {
    195                         classNames.add(getClassName(entryName).replace('/', '.'));
    196                     }
    197                 }
    198             }
    199         }
    200     }
    201 
    202     /**
    203      * Finds all classes and sub packages that are below the packageName and
    204      * add them to the respective sets. Searches the package in a single apk file.
    205      */
    206     private void findClassesInApk(String apkPath, String packageName,
    207             Set<String> classNames, Set<String> subpackageNames)
    208             throws IOException {
    209 
    210         DexFile dexFile = null;
    211         try {
    212             dexFile = new DexFile(apkPath);
    213             Enumeration<String> apkClassNames = dexFile.entries();
    214             while (apkClassNames.hasMoreElements()) {
    215                 String className = apkClassNames.nextElement();
    216 
    217                 if (className.startsWith(packageName)) {
    218                     String subPackageName = packageName;
    219                     int lastPackageSeparator = className.lastIndexOf('.');
    220                     if (lastPackageSeparator > 0) {
    221                         subPackageName = className.substring(0, lastPackageSeparator);
    222                     }
    223                     if (subPackageName.length() > packageName.length()) {
    224                         subpackageNames.add(subPackageName);
    225                     } else if (isToplevelClass(className)) {
    226                         classNames.add(className);
    227                     }
    228                 }
    229             }
    230         } catch (IOException e) {
    231             if (false) {
    232                 Log.w("ClassPathPackageInfoSource",
    233                         "Error finding classes at apk path: " + apkPath, e);
    234             }
    235         } finally {
    236             if (dexFile != null) {
    237                 // Todo: figure out why closing causes a dalvik error resulting in vm shutdown.
    238 //                dexFile.close();
    239             }
    240         }
    241     }
    242 
    243     /**
    244      * Gets the class and package entries from a Jar.
    245      */
    246     private Set<String> getJarEntries(File jarFile)
    247             throws IOException {
    248         Set<String> entryNames = jarFiles.get(jarFile);
    249         if (entryNames == null) {
    250             entryNames = Sets.newHashSet();
    251             ZipFile zipFile = new ZipFile(jarFile);
    252             Enumeration<? extends ZipEntry> entries = zipFile.entries();
    253             while (entries.hasMoreElements()) {
    254                 String entryName = entries.nextElement().getName();
    255                 if (entryName.endsWith(CLASS_EXTENSION)) {
    256                     // add the entry name of the class
    257                     entryNames.add(entryName);
    258 
    259                     // add the entry name of the classes package, i.e. the entry name of
    260                     // the directory that the class is in. Used to quickly skip jar files
    261                     // if they do not contain a certain package.
    262                     //
    263                     // Also add parent packages so that a JAR that contains
    264                     // pkg1/pkg2/Foo.class will be marked as containing pkg1/ in addition
    265                     // to pkg1/pkg2/ and pkg1/pkg2/Foo.class.  We're still interested in
    266                     // JAR files that contains subpackages of a given package, even if
    267                     // an intermediate package contains no direct classes.
    268                     //
    269                     // Classes in the default package will cause a single package named
    270                     // "" to be added instead.
    271                     int lastIndex = entryName.lastIndexOf('/');
    272                     do {
    273                         String packageName = entryName.substring(0, lastIndex + 1);
    274                         entryNames.add(packageName);
    275                         lastIndex = entryName.lastIndexOf('/', lastIndex - 1);
    276                     } while (lastIndex > 0);
    277                 }
    278             }
    279             jarFiles.put(jarFile, entryNames);
    280         }
    281         return entryNames;
    282     }
    283 
    284     /**
    285      * Checks if a given file name represents a toplevel class.
    286      */
    287     private static boolean isToplevelClass(String fileName) {
    288         return fileName.indexOf('$') < 0;
    289     }
    290 
    291     /**
    292      * Given the absolute path of a class file, return the class name.
    293      */
    294     private static String getClassName(String className) {
    295         int classNameEnd = className.length() - CLASS_EXTENSION.length();
    296         return className.substring(0, classNameEnd);
    297     }
    298 
    299     /**
    300      * Gets the class path from the System Property "java.class.path" and splits
    301      * it up into the individual elements.
    302      */
    303     private static String[] getClassPath() {
    304         String classPath = System.getProperty("java.class.path");
    305         String separator = System.getProperty("path.separator", ":");
    306         return classPath.split(Pattern.quote(separator));
    307     }
    308 
    309     public void setClassLoader(ClassLoader classLoader) {
    310         this.classLoader = classLoader;
    311     }
    312 }
    313