• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ClassPathPackageInfoSource()63     ClassPathPackageInfoSource() {
64         classPath = getClassPath();
65     }
66 
67 
setApkPaths(String[] apkPaths)68     public static void setApkPaths(String[] apkPaths) {
69         ClassPathPackageInfoSource.apkPaths = apkPaths;
70     }
71 
getPackageInfo(String pkgName)72     public ClassPathPackageInfo getPackageInfo(String pkgName) {
73         return cache.get(pkgName);
74     }
75 
createPackageInfo(String packageName)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      */
findClasses(String packageName, Set<String> classNames, Set<String> subpackageNames)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 
scanForApkFiles(File source, String packageName, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInDirectory(File classDir, String packagePrefix, String pathPrefix, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInJar(File jarFile, String pathPrefix, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInApk(String apkPath, String packageName, Set<String> classNames, Set<String> subpackageNames)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      */
getJarEntries(File jarFile)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      */
isToplevelClass(String fileName)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      */
getClassName(String className)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      */
getClassPath()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 
setClassLoader(ClassLoader classLoader)320     public void setClassLoader(ClassLoader classLoader) {
321         this.classLoader = classLoader;
322     }
323 }
324