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