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 
ClassPathPackageInfoSource()62     ClassPathPackageInfoSource() {
63         classPath = getClassPath();
64     }
65 
66 
setApkPaths(String[] apkPaths)67     public static void setApkPaths(String[] apkPaths) {
68         ClassPathPackageInfoSource.apkPaths = apkPaths;
69     }
70 
getPackageInfo(String pkgName)71     public ClassPathPackageInfo getPackageInfo(String pkgName) {
72         return cache.get(pkgName);
73     }
74 
createPackageInfo(String packageName)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 | NoClassDefFoundError 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      */
findClasses(String packageName, Set<String> classNames, Set<String> subpackageNames)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 
scanForApkFiles(File source, String packageName, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInDirectory(File classDir, String packagePrefix, String pathPrefix, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInJar(File jarFile, String pathPrefix, Set<String> classNames, Set<String> subpackageNames)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      */
findClassesInApk(String apkPath, String packageName, Set<String> classNames, Set<String> subpackageNames)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      */
getJarEntries(File jarFile)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      */
isToplevelClass(String fileName)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      */
getClassName(String className)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      */
getClassPath()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 
setClassLoader(ClassLoader classLoader)309     public void setClassLoader(ClassLoader classLoader) {
310         this.classLoader = classLoader;
311     }
312 }
313