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
17package android.test;
18
19import android.util.Log;
20import dalvik.system.DexFile;
21
22import java.io.File;
23import java.io.IOException;
24import java.util.Collections;
25import java.util.Enumeration;
26import java.util.HashSet;
27import java.util.Set;
28import java.util.TreeSet;
29import java.util.regex.Pattern;
30
31/**
32 * Generate {@link ClassPathPackageInfo}s by scanning apk paths.
33 *
34 * {@hide} Not needed for 1.0 SDK.
35 */
36@Deprecated
37public class ClassPathPackageInfoSource {
38
39    private static final ClassLoader CLASS_LOADER
40            = ClassPathPackageInfoSource.class.getClassLoader();
41
42    private static String[] apkPaths;
43
44    private static ClassPathPackageInfoSource classPathSource;
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
57    private final ClassLoader classLoader;
58
59    private ClassPathPackageInfoSource(ClassLoader classLoader) {
60        this.classLoader = classLoader;
61        classPath = getClassPath();
62    }
63
64    static void setApkPaths(String[] apkPaths) {
65        ClassPathPackageInfoSource.apkPaths = apkPaths;
66    }
67
68    public static ClassPathPackageInfoSource forClassPath(ClassLoader classLoader) {
69        if (classPathSource == null) {
70            classPathSource = new ClassPathPackageInfoSource(classLoader);
71        }
72        return classPathSource;
73    }
74
75    public Set<Class<?>> getTopLevelClassesRecursive(String packageName) {
76        ClassPathPackageInfo packageInfo = cache.get(packageName);
77        return packageInfo.getTopLevelClassesRecursive();
78    }
79
80    private ClassPathPackageInfo createPackageInfo(String packageName) {
81        Set<String> subpackageNames = new TreeSet<String>();
82        Set<String> classNames = new TreeSet<String>();
83        Set<Class<?>> topLevelClasses = new HashSet<>();
84        findClasses(packageName, classNames, subpackageNames);
85        for (String className : classNames) {
86            if (className.endsWith(".R") || className.endsWith(".Manifest")) {
87                // Don't try to load classes that are generated. They usually aren't in test apks.
88                continue;
89            }
90
91            try {
92                // We get errors in the emulator if we don't use the caller's class loader.
93                topLevelClasses.add(Class.forName(className, false,
94                        (classLoader != null) ? classLoader : CLASS_LOADER));
95            } catch (ClassNotFoundException | NoClassDefFoundError e) {
96                // Should not happen unless there is a generated class that is not included in
97                // the .apk.
98                Log.w("ClassPathPackageInfoSource", "Cannot load class. "
99                        + "Make sure it is in your apk. Class name: '" + className
100                        + "'. Message: " + e.getMessage(), e);
101            }
102        }
103        return new ClassPathPackageInfo(packageName, subpackageNames,
104                topLevelClasses);
105    }
106
107    /**
108     * Finds all classes and sub packages that are below the packageName and
109     * add them to the respective sets. Searches the package on the whole class
110     * path.
111     */
112    private void findClasses(String packageName, Set<String> classNames,
113            Set<String> subpackageNames) {
114        for (String entryName : classPath) {
115            File classPathEntry = new File(entryName);
116
117            // Forge may not have brought over every item in the classpath. Be
118            // polite and ignore missing entries.
119            if (classPathEntry.exists()) {
120                try {
121                    if (entryName.endsWith(".apk")) {
122                        findClassesInApk(entryName, packageName, classNames, subpackageNames);
123                    } else {
124                        // scan the directories that contain apk files.
125                        for (String apkPath : apkPaths) {
126                            File file = new File(apkPath);
127                            scanForApkFiles(file, packageName, classNames, subpackageNames);
128                        }
129                    }
130                } catch (IOException e) {
131                    throw new AssertionError("Can't read classpath entry " +
132                            entryName + ": " + e.getMessage());
133                }
134            }
135        }
136    }
137
138    private void scanForApkFiles(File source, String packageName,
139            Set<String> classNames, Set<String> subpackageNames) throws IOException {
140        if (source.getPath().endsWith(".apk")) {
141            findClassesInApk(source.getPath(), packageName, classNames, subpackageNames);
142        } else {
143            File[] files = source.listFiles();
144            if (files != null) {
145                for (File file : files) {
146                    scanForApkFiles(file, packageName, classNames, subpackageNames);
147                }
148            }
149        }
150    }
151
152    /**
153     * Finds all classes and sub packages that are below the packageName and
154     * add them to the respective sets. Searches the package in a single apk file.
155     */
156    private void findClassesInApk(String apkPath, String packageName,
157            Set<String> classNames, Set<String> subpackageNames)
158            throws IOException {
159
160        DexFile dexFile = null;
161        try {
162            dexFile = new DexFile(apkPath);
163            Enumeration<String> apkClassNames = dexFile.entries();
164            while (apkClassNames.hasMoreElements()) {
165                String className = apkClassNames.nextElement();
166
167                if (className.startsWith(packageName)) {
168                    String subPackageName = packageName;
169                    int lastPackageSeparator = className.lastIndexOf('.');
170                    if (lastPackageSeparator > 0) {
171                        subPackageName = className.substring(0, lastPackageSeparator);
172                    }
173                    if (subPackageName.length() > packageName.length()) {
174                        subpackageNames.add(subPackageName);
175                    } else if (isToplevelClass(className)) {
176                        classNames.add(className);
177                    }
178                }
179            }
180        } catch (IOException e) {
181            if (false) {
182                Log.w("ClassPathPackageInfoSource",
183                        "Error finding classes at apk path: " + apkPath, e);
184            }
185        } finally {
186            if (dexFile != null) {
187                // Todo: figure out why closing causes a dalvik error resulting in vm shutdown.
188//                dexFile.close();
189            }
190        }
191    }
192
193    /**
194     * Checks if a given file name represents a toplevel class.
195     */
196    private static boolean isToplevelClass(String fileName) {
197        return fileName.indexOf('$') < 0;
198    }
199
200    /**
201     * Gets the class path from the System Property "java.class.path" and splits
202     * it up into the individual elements.
203     */
204    private static String[] getClassPath() {
205        String classPath = System.getProperty("java.class.path");
206        String separator = System.getProperty("path.separator", ":");
207        return classPath.split(Pattern.quote(separator));
208    }
209
210    /**
211     * The Package object doesn't allow you to iterate over the contained
212     * classes and subpackages of that package.  This is a version that does.
213     */
214    private class ClassPathPackageInfo {
215
216        private final String packageName;
217        private final Set<String> subpackageNames;
218        private final Set<Class<?>> topLevelClasses;
219
220        private ClassPathPackageInfo(String packageName,
221                Set<String> subpackageNames, Set<Class<?>> topLevelClasses) {
222            this.packageName = packageName;
223            this.subpackageNames = Collections.unmodifiableSet(subpackageNames);
224            this.topLevelClasses = Collections.unmodifiableSet(topLevelClasses);
225        }
226
227        private Set<ClassPathPackageInfo> getSubpackages() {
228            Set<ClassPathPackageInfo> info = new HashSet<>();
229            for (String name : subpackageNames) {
230                info.add(cache.get(name));
231            }
232            return info;
233        }
234
235        private Set<Class<?>> getTopLevelClassesRecursive() {
236            Set<Class<?>> set = new HashSet<>();
237            addTopLevelClassesTo(set);
238            return set;
239        }
240
241        private void addTopLevelClassesTo(Set<Class<?>> set) {
242            set.addAll(topLevelClasses);
243            for (ClassPathPackageInfo info : getSubpackages()) {
244                info.addTopLevelClassesTo(set);
245            }
246        }
247
248        @Override
249        public boolean equals(Object obj) {
250            if (obj instanceof ClassPathPackageInfo) {
251                ClassPathPackageInfo that = (ClassPathPackageInfo) obj;
252                return (this.packageName).equals(that.packageName);
253            }
254            return false;
255        }
256
257        @Override
258        public int hashCode() {
259            return packageName.hashCode();
260        }
261    }
262}
263