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 @Deprecated 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 | NoClassDefFoundError 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 { 123 // scan the directories that contain apk files. 124 for (String apkPath : apkPaths) { 125 File file = new File(apkPath); 126 scanForApkFiles(file, packageName, classNames, subpackageNames); 127 } 128 } 129 } catch (IOException e) { 130 throw new AssertionError("Can't read classpath entry " + 131 entryName + ": " + e.getMessage()); 132 } 133 } 134 } 135 } 136 137 private void scanForApkFiles(File source, String packageName, 138 Set<String> classNames, Set<String> subpackageNames) throws IOException { 139 if (source.getPath().endsWith(".apk")) { 140 findClassesInApk(source.getPath(), packageName, classNames, subpackageNames); 141 } else { 142 File[] files = source.listFiles(); 143 if (files != null) { 144 for (File file : files) { 145 scanForApkFiles(file, packageName, classNames, subpackageNames); 146 } 147 } 148 } 149 } 150 151 /** 152 * Finds all classes and sub packages that are below the packageName and 153 * add them to the respective sets. Searches the package in a class directory. 154 */ 155 private void findClassesInDirectory(File classDir, 156 String packagePrefix, String pathPrefix, Set<String> classNames, 157 Set<String> subpackageNames) 158 throws IOException { 159 File directory = new File(classDir, pathPrefix); 160 161 if (directory.exists()) { 162 for (File f : directory.listFiles()) { 163 String name = f.getName(); 164 if (name.endsWith(CLASS_EXTENSION) && isToplevelClass(name)) { 165 classNames.add(packagePrefix + getClassName(name)); 166 } else if (f.isDirectory()) { 167 subpackageNames.add(packagePrefix + name); 168 } 169 } 170 } 171 } 172 173 /** 174 * Finds all classes and sub packages that are below the packageName and 175 * add them to the respective sets. Searches the package in a single jar file. 176 */ 177 private void findClassesInJar(File jarFile, String pathPrefix, 178 Set<String> classNames, Set<String> subpackageNames) 179 throws IOException { 180 Set<String> entryNames = getJarEntries(jarFile); 181 // check if the Jar contains the package. 182 if (!entryNames.contains(pathPrefix)) { 183 return; 184 } 185 int prefixLength = pathPrefix.length(); 186 for (String entryName : entryNames) { 187 if (entryName.startsWith(pathPrefix)) { 188 if (entryName.endsWith(CLASS_EXTENSION)) { 189 // check if the class is in the package itself or in one of its 190 // subpackages. 191 int index = entryName.indexOf('/', prefixLength); 192 if (index >= 0) { 193 String p = entryName.substring(0, index).replace('/', '.'); 194 subpackageNames.add(p); 195 } else if (isToplevelClass(entryName)) { 196 classNames.add(getClassName(entryName).replace('/', '.')); 197 } 198 } 199 } 200 } 201 } 202 203 /** 204 * Finds all classes and sub packages that are below the packageName and 205 * add them to the respective sets. Searches the package in a single apk file. 206 */ 207 private void findClassesInApk(String apkPath, String packageName, 208 Set<String> classNames, Set<String> subpackageNames) 209 throws IOException { 210 211 DexFile dexFile = null; 212 try { 213 dexFile = new DexFile(apkPath); 214 Enumeration<String> apkClassNames = dexFile.entries(); 215 while (apkClassNames.hasMoreElements()) { 216 String className = apkClassNames.nextElement(); 217 218 if (className.startsWith(packageName)) { 219 String subPackageName = packageName; 220 int lastPackageSeparator = className.lastIndexOf('.'); 221 if (lastPackageSeparator > 0) { 222 subPackageName = className.substring(0, lastPackageSeparator); 223 } 224 if (subPackageName.length() > packageName.length()) { 225 subpackageNames.add(subPackageName); 226 } else if (isToplevelClass(className)) { 227 classNames.add(className); 228 } 229 } 230 } 231 } catch (IOException e) { 232 if (false) { 233 Log.w("ClassPathPackageInfoSource", 234 "Error finding classes at apk path: " + apkPath, e); 235 } 236 } finally { 237 if (dexFile != null) { 238 // Todo: figure out why closing causes a dalvik error resulting in vm shutdown. 239 // dexFile.close(); 240 } 241 } 242 } 243 244 /** 245 * Gets the class and package entries from a Jar. 246 */ 247 private Set<String> getJarEntries(File jarFile) 248 throws IOException { 249 Set<String> entryNames = jarFiles.get(jarFile); 250 if (entryNames == null) { 251 entryNames = Sets.newHashSet(); 252 ZipFile zipFile = new ZipFile(jarFile); 253 Enumeration<? extends ZipEntry> entries = zipFile.entries(); 254 while (entries.hasMoreElements()) { 255 String entryName = entries.nextElement().getName(); 256 if (entryName.endsWith(CLASS_EXTENSION)) { 257 // add the entry name of the class 258 entryNames.add(entryName); 259 260 // add the entry name of the classes package, i.e. the entry name of 261 // the directory that the class is in. Used to quickly skip jar files 262 // if they do not contain a certain package. 263 // 264 // Also add parent packages so that a JAR that contains 265 // pkg1/pkg2/Foo.class will be marked as containing pkg1/ in addition 266 // to pkg1/pkg2/ and pkg1/pkg2/Foo.class. We're still interested in 267 // JAR files that contains subpackages of a given package, even if 268 // an intermediate package contains no direct classes. 269 // 270 // Classes in the default package will cause a single package named 271 // "" to be added instead. 272 int lastIndex = entryName.lastIndexOf('/'); 273 do { 274 String packageName = entryName.substring(0, lastIndex + 1); 275 entryNames.add(packageName); 276 lastIndex = entryName.lastIndexOf('/', lastIndex - 1); 277 } while (lastIndex > 0); 278 } 279 } 280 jarFiles.put(jarFile, entryNames); 281 } 282 return entryNames; 283 } 284 285 /** 286 * Checks if a given file name represents a toplevel class. 287 */ 288 private static boolean isToplevelClass(String fileName) { 289 return fileName.indexOf('$') < 0; 290 } 291 292 /** 293 * Given the absolute path of a class file, return the class name. 294 */ 295 private static String getClassName(String className) { 296 int classNameEnd = className.length() - CLASS_EXTENSION.length(); 297 return className.substring(0, classNameEnd); 298 } 299 300 /** 301 * Gets the class path from the System Property "java.class.path" and splits 302 * it up into the individual elements. 303 */ 304 private static String[] getClassPath() { 305 String classPath = System.getProperty("java.class.path"); 306 String separator = System.getProperty("path.separator", ":"); 307 return classPath.split(Pattern.quote(separator)); 308 } 309 310 public void setClassLoader(ClassLoader classLoader) { 311 this.classLoader = classLoader; 312 } 313 } 314