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