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