1package com.xtremelabs.robolectric;
2
3import android.app.Application;
4import com.xtremelabs.robolectric.internal.ClassNameResolver;
5import org.w3c.dom.Document;
6import org.w3c.dom.Node;
7import org.w3c.dom.NodeList;
8
9import javax.xml.parsers.DocumentBuilder;
10import javax.xml.parsers.DocumentBuilderFactory;
11import java.io.File;
12import java.io.FileNotFoundException;
13import java.util.ArrayList;
14import java.util.List;
15
16import static android.content.pm.ApplicationInfo.*;
17
18public class RobolectricConfig {
19    private final File androidManifestFile;
20    private final File resourceDirectory;
21    private final File assetsDirectory;
22    private String rClassName;
23    private String packageName;
24    private String processName;
25    private String applicationName;
26    private boolean manifestIsParsed = false;
27    private int sdkVersion;
28    private int minSdkVersion;
29    private boolean sdkVersionSpecified = true;
30    private boolean minSdkVersionSpecified = true;
31    private int applicationFlags;
32    private final List<ReceiverAndIntentFilter> receivers = new ArrayList<ReceiverAndIntentFilter>();
33    private boolean strictI18n = false;
34    private String locale = "";
35    private String oldLocale = "";
36
37    /**
38     * Creates a Robolectric configuration using default Android files relative to the specified base directory.
39     * <p/>
40     * The manifest will be baseDir/AndroidManifest.xml, res will be baseDir/res, and assets in baseDir/assets.
41     *
42     * @param baseDir the base directory of your Android project
43     */
44    public RobolectricConfig(final File baseDir) {
45        this(new File(baseDir, "AndroidManifest.xml"), new File(baseDir, "res"), new File(baseDir, "assets"));
46    }
47
48    public RobolectricConfig(final File androidManifestFile, final File resourceDirectory) {
49        this(androidManifestFile, resourceDirectory, new File(resourceDirectory.getParent(), "assets"));
50    }
51
52    /**
53     * Creates a Robolectric configuration using specified locations.
54     *
55     * @param androidManifestFile location of the AndroidManifest.xml file
56     * @param resourceDirectory   location of the res directory
57     * @param assetsDirectory     location of the assets directory
58     */
59    public RobolectricConfig(final File androidManifestFile, final File resourceDirectory, final File assetsDirectory) {
60        this.androidManifestFile = androidManifestFile;
61        this.resourceDirectory = resourceDirectory;
62        this.assetsDirectory = assetsDirectory;
63    }
64
65    public String getRClassName() throws Exception {
66        parseAndroidManifest();
67        return rClassName;
68    }
69
70    public void validate() throws FileNotFoundException {
71        if (!androidManifestFile.exists() || !androidManifestFile.isFile()) {
72            throw new FileNotFoundException(androidManifestFile.getAbsolutePath() + " not found or not a file; it should point to your project's AndroidManifest.xml");
73        }
74
75        if (!getResourceDirectory().exists() || !getResourceDirectory().isDirectory()) {
76            throw new FileNotFoundException(getResourceDirectory().getAbsolutePath() + " not found or not a directory; it should point to your project's res directory");
77        }
78    }
79
80    private void parseAndroidManifest() {
81        if (manifestIsParsed) {
82            return;
83        }
84        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
85        try {
86            DocumentBuilder db = dbf.newDocumentBuilder();
87            Document manifestDocument = db.parse(androidManifestFile);
88
89            packageName = getTagAttributeText(manifestDocument, "manifest", "package");
90            rClassName = packageName + ".R";
91            applicationName = getTagAttributeText(manifestDocument, "application", "android:name");
92            Integer minSdkVer = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:minSdkVersion");
93            Integer sdkVer = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:targetSdkVersion");
94            if (minSdkVer == null) {
95                minSdkVersion = 10;
96                minSdkVersionSpecified = false;
97            } else {
98                minSdkVersion = minSdkVer;
99            }
100            if (sdkVer == null) {
101                sdkVersion = 10;
102                sdkVersionSpecified = false;
103            } else {
104                sdkVersion = sdkVer;
105            }
106
107            processName = getTagAttributeText(manifestDocument, "application", "android:process");
108            if (processName == null) {
109            	processName = packageName;
110            }
111
112            parseApplicationFlags(manifestDocument);
113            parseReceivers(manifestDocument, packageName);
114        } catch (Exception ignored) {
115        }
116        manifestIsParsed = true;
117    }
118
119    private void parseReceivers(final Document manifestDocument, String packageName) {
120        Node application = manifestDocument.getElementsByTagName("application").item(0);
121        if (application == null) {
122            return;
123        }
124        for (Node receiverNode : getChildrenTags(application, "receiver")) {
125            Node namedItem = receiverNode.getAttributes().getNamedItem("android:name");
126            if (namedItem == null) {
127                continue;
128            }
129            String receiverName = namedItem.getTextContent();
130            if (receiverName.startsWith(".")) {
131                receiverName = packageName + receiverName;
132            }
133            for (Node intentFilterNode : getChildrenTags(receiverNode, "intent-filter")) {
134                List<String> actions = new ArrayList<String>();
135                for (Node actionNode : getChildrenTags(intentFilterNode, "action")) {
136                    Node nameNode = actionNode.getAttributes().getNamedItem("android:name");
137                    if (nameNode != null) {
138                        actions.add(nameNode.getTextContent());
139                    }
140                }
141                receivers.add(new ReceiverAndIntentFilter(receiverName, actions));
142            }
143        }
144    }
145
146    private List<Node> getChildrenTags(final Node node, final String tagName) {
147        List<Node> children = new ArrayList<Node>();
148        for (int i = 0; i < node.getChildNodes().getLength(); i++) {
149            Node childNode = node.getChildNodes().item(i);
150            if (childNode.getNodeName().equalsIgnoreCase(tagName)) {
151                children.add(childNode);
152            }
153        }
154        return children;
155    }
156
157    private void parseApplicationFlags(final Document manifestDocument) {
158        applicationFlags = getApplicationFlag(manifestDocument, "android:allowBackup", FLAG_ALLOW_BACKUP);
159        applicationFlags += getApplicationFlag(manifestDocument, "android:allowClearUserData", FLAG_ALLOW_CLEAR_USER_DATA);
160        applicationFlags += getApplicationFlag(manifestDocument, "android:allowTaskReparenting", FLAG_ALLOW_TASK_REPARENTING);
161        applicationFlags += getApplicationFlag(manifestDocument, "android:debuggable", FLAG_DEBUGGABLE);
162        applicationFlags += getApplicationFlag(manifestDocument, "android:hasCode", FLAG_HAS_CODE);
163        applicationFlags += getApplicationFlag(manifestDocument, "android:killAfterRestore", FLAG_KILL_AFTER_RESTORE);
164        applicationFlags += getApplicationFlag(manifestDocument, "android:persistent", FLAG_PERSISTENT);
165        applicationFlags += getApplicationFlag(manifestDocument, "android:resizeable", FLAG_RESIZEABLE_FOR_SCREENS);
166        applicationFlags += getApplicationFlag(manifestDocument, "android:restoreAnyVersion", FLAG_RESTORE_ANY_VERSION);
167        applicationFlags += getApplicationFlag(manifestDocument, "android:largeScreens", FLAG_SUPPORTS_LARGE_SCREENS);
168        applicationFlags += getApplicationFlag(manifestDocument, "android:normalScreens", FLAG_SUPPORTS_NORMAL_SCREENS);
169        applicationFlags += getApplicationFlag(manifestDocument, "android:anyDensity", FLAG_SUPPORTS_SCREEN_DENSITIES);
170        applicationFlags += getApplicationFlag(manifestDocument, "android:smallScreens", FLAG_SUPPORTS_SMALL_SCREENS);
171        applicationFlags += getApplicationFlag(manifestDocument, "android:testOnly", FLAG_TEST_ONLY);
172        applicationFlags += getApplicationFlag(manifestDocument, "android:vmSafeMode", FLAG_VM_SAFE_MODE);
173    }
174
175    private int getApplicationFlag(final Document doc, final String attribute, final int attributeValue) {
176    	String flagString = getTagAttributeText(doc, "application", attribute);
177    	return "true".equalsIgnoreCase(flagString) ? attributeValue : 0;
178    }
179
180    private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute) {
181        return getTagAttributeIntValue(doc, tag, attribute, null);
182    }
183
184    private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute, final Integer defaultValue) {
185        String valueString = getTagAttributeText(doc, tag, attribute);
186        if (valueString != null) {
187            return Integer.parseInt(valueString);
188        }
189        return defaultValue;
190    }
191
192    public String getApplicationName() {
193        parseAndroidManifest();
194        return applicationName;
195    }
196
197    public String getPackageName() {
198        parseAndroidManifest();
199        return packageName;
200    }
201
202    public int getMinSdkVersion() {
203    	parseAndroidManifest();
204		return minSdkVersion;
205	}
206
207    public int getSdkVersion() {
208        parseAndroidManifest();
209        return sdkVersion;
210    }
211
212    public int getApplicationFlags() {
213    	parseAndroidManifest();
214    	return applicationFlags;
215    }
216
217    public String getProcessName() {
218		parseAndroidManifest();
219		return processName;
220	}
221
222    public File getResourceDirectory() {
223        return resourceDirectory;
224    }
225
226    public File getAssetsDirectory() {
227        return assetsDirectory;
228    }
229
230    public int getReceiverCount() {
231        parseAndroidManifest();
232        return receivers.size();
233    }
234
235    public String getReceiverClassName(final int receiverIndex) {
236        parseAndroidManifest();
237        return receivers.get(receiverIndex).getBroadcastReceiverClassName();
238    }
239
240    public List<String> getReceiverIntentFilterActions(final int receiverIndex) {
241        parseAndroidManifest();
242        return receivers.get(receiverIndex).getIntentFilterActions();
243    }
244
245    public boolean getStrictI18n() {
246    	return strictI18n;
247    }
248
249    public void setStrictI18n(boolean strict) {
250    	strictI18n = strict;
251    }
252
253    public void setLocale( String locale ){
254    	this.oldLocale = this.locale;
255    	this.locale = locale;
256    }
257
258    public String getLocale() {
259    	return this.locale;
260    }
261
262    public boolean isLocaleChanged() {
263    	return !locale.equals( oldLocale );
264    }
265
266    private static String getTagAttributeText(final Document doc, final String tag, final String attribute) {
267        NodeList elementsByTagName = doc.getElementsByTagName(tag);
268        for (int i = 0; i < elementsByTagName.getLength(); ++i) {
269            Node item = elementsByTagName.item(i);
270            Node namedItem = item.getAttributes().getNamedItem(attribute);
271            if (namedItem != null) {
272                return namedItem.getTextContent();
273            }
274        }
275        return null;
276    }
277
278    private static Application newApplicationInstance(final String packageName, final String applicationName) {
279        Application application;
280        try {
281            Class<? extends Application> applicationClass =
282                    new ClassNameResolver<Application>(packageName, applicationName).resolve();
283            application = applicationClass.newInstance();
284        } catch (Exception e) {
285            throw new RuntimeException(e);
286        }
287        return application;
288    }
289
290    @Override
291    public boolean equals(final Object o) {
292        if (this == o) {
293            return true;
294        }
295        if (o == null || getClass() != o.getClass()) {
296            return false;
297        }
298
299        RobolectricConfig that = (RobolectricConfig) o;
300
301        if (androidManifestFile != null ? !androidManifestFile.equals(that.androidManifestFile) : that.androidManifestFile != null) {
302            return false;
303        }
304        if (getAssetsDirectory() != null ? !getAssetsDirectory().equals(that.getAssetsDirectory()) : that.getAssetsDirectory() != null) {
305            return false;
306        }
307        if (getResourceDirectory() != null ? !getResourceDirectory().equals(that.getResourceDirectory()) : that.getResourceDirectory() != null) {
308            return false;
309        }
310
311        return true;
312    }
313
314    @Override
315    public int hashCode() {
316        int result = androidManifestFile != null ? androidManifestFile.hashCode() : 0;
317        result = 31 * result + (getResourceDirectory() != null ? getResourceDirectory().hashCode() : 0);
318        result = 31 * result + (getAssetsDirectory() != null ? getAssetsDirectory().hashCode() : 0);
319        return result;
320    }
321
322    public int getRealSdkVersion() {
323        parseAndroidManifest();
324        if (sdkVersionSpecified) {
325            return sdkVersion;
326        }
327        if (minSdkVersionSpecified) {
328            return minSdkVersion;
329        }
330        return sdkVersion;
331    }
332
333    private static class ReceiverAndIntentFilter {
334        private final List<String> intentFilterActions;
335        private final String broadcastReceiverClassName;
336
337        public ReceiverAndIntentFilter(final String broadcastReceiverClassName, final List<String> intentFilterActions) {
338            this.broadcastReceiverClassName = broadcastReceiverClassName;
339            this.intentFilterActions = intentFilterActions;
340        }
341
342        public String getBroadcastReceiverClassName() {
343            return broadcastReceiverClassName;
344        }
345
346        public List<String> getIntentFilterActions() {
347            return intentFilterActions;
348        }
349    }
350}
351