• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 com.android.ide.eclipse.adt.internal.editors.manifest;
18 
19 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_STYLE;
20 import static com.android.sdklib.SdkConstants.NS_RESOURCES;
21 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_ICON;
22 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_LABEL;
23 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION;
24 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME;
25 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
26 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION;
27 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_THEME;
28 import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY;
29 import static com.android.sdklib.xml.AndroidManifest.NODE_USES_SDK;
30 import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES;
31 
32 import com.android.annotations.NonNull;
33 import com.android.annotations.Nullable;
34 import com.android.ide.eclipse.adt.AdtPlugin;
35 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
36 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
37 import com.android.ide.eclipse.adt.io.IFolderWrapper;
38 import com.android.io.IAbstractFile;
39 import com.android.io.StreamException;
40 import com.android.resources.ScreenSize;
41 import com.android.sdklib.IAndroidTarget;
42 import com.android.sdklib.SdkConstants;
43 import com.android.sdklib.xml.AndroidManifest;
44 import com.android.util.Pair;
45 
46 import org.eclipse.core.resources.IFile;
47 import org.eclipse.core.resources.IProject;
48 import org.eclipse.core.resources.IResource;
49 import org.eclipse.core.resources.IWorkspace;
50 import org.eclipse.core.resources.ResourcesPlugin;
51 import org.eclipse.core.runtime.CoreException;
52 import org.eclipse.core.runtime.IPath;
53 import org.eclipse.core.runtime.NullProgressMonitor;
54 import org.eclipse.core.runtime.QualifiedName;
55 import org.eclipse.jdt.core.IField;
56 import org.eclipse.jdt.core.IJavaElement;
57 import org.eclipse.jdt.core.IJavaProject;
58 import org.eclipse.jdt.core.IMethod;
59 import org.eclipse.jdt.core.IPackageFragment;
60 import org.eclipse.jdt.core.IPackageFragmentRoot;
61 import org.eclipse.jdt.core.IType;
62 import org.eclipse.jdt.core.search.IJavaSearchScope;
63 import org.eclipse.jdt.core.search.SearchEngine;
64 import org.eclipse.jdt.core.search.SearchMatch;
65 import org.eclipse.jdt.core.search.SearchParticipant;
66 import org.eclipse.jdt.core.search.SearchPattern;
67 import org.eclipse.jdt.core.search.SearchRequestor;
68 import org.eclipse.jface.text.IDocument;
69 import org.eclipse.ui.editors.text.TextFileDocumentProvider;
70 import org.eclipse.ui.texteditor.IDocumentProvider;
71 import org.w3c.dom.Document;
72 import org.w3c.dom.Element;
73 import org.w3c.dom.NodeList;
74 import org.xml.sax.InputSource;
75 import org.xml.sax.SAXException;
76 
77 import java.util.HashMap;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.concurrent.atomic.AtomicReference;
81 import java.util.regex.Matcher;
82 import java.util.regex.Pattern;
83 
84 import javax.xml.parsers.DocumentBuilder;
85 import javax.xml.parsers.DocumentBuilderFactory;
86 import javax.xml.xpath.XPathExpressionException;
87 
88 /**
89  * Retrieves and caches manifest information such as the themes to be used for
90  * a given activity.
91  *
92  * @see AndroidManifest
93  */
94 public class ManifestInfo {
95     /**
96      * The maximum number of milliseconds to search for an activity in the codebase when
97      * attempting to associate layouts with activities in
98      * {@link #guessActivity(IFile, String)}
99      */
100     private static final int SEARCH_TIMEOUT_MS = 3000;
101 
102     private final IProject mProject;
103     private String mPackage;
104     private String mManifestTheme;
105     private Map<String, String> mActivityThemes;
106     private IAbstractFile mManifestFile;
107     private long mLastModified;
108     private int mMinSdk;
109     private int mTargetSdk;
110     private String mApplicationIcon;
111     private String mApplicationLabel;
112 
113     /**
114      * Qualified name for the per-project non-persistent property storing the
115      * {@link ManifestInfo} for this project
116      */
117     final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
118             "manifest"); //$NON-NLS-1$
119 
120     /**
121      * Constructs an {@link ManifestInfo} for the given project. Don't use this method;
122      * use the {@link #get} factory method instead.
123      *
124      * @param project project to create an {@link ManifestInfo} for
125      */
ManifestInfo(IProject project)126     private ManifestInfo(IProject project) {
127         mProject = project;
128     }
129 
130     /**
131      * Returns the {@link ManifestInfo} for the given project
132      *
133      * @param project the project the finder is associated with
134      * @return a {@ManifestInfo} for the given project, never null
135      */
136     @NonNull
get(IProject project)137     public static ManifestInfo get(IProject project) {
138         ManifestInfo finder = null;
139         try {
140             finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER);
141         } catch (CoreException e) {
142             // Not a problem; we will just create a new one
143         }
144 
145         if (finder == null) {
146             finder = new ManifestInfo(project);
147             try {
148                 project.setSessionProperty(MANIFEST_FINDER, finder);
149             } catch (CoreException e) {
150                 AdtPlugin.log(e, "Can't store ManifestInfo");
151             }
152         }
153 
154         return finder;
155     }
156 
157     /**
158      * Ensure that the package, theme and activity maps are initialized and up to date
159      * with respect to the manifest file
160      */
sync()161     private void sync() {
162         if (mManifestFile == null) {
163             IFolderWrapper projectFolder = new IFolderWrapper(mProject);
164             mManifestFile = AndroidManifest.getManifest(projectFolder);
165             if (mManifestFile == null) {
166                 return;
167             }
168         }
169 
170         // Check to see if our data is up to date
171         long fileModified = mManifestFile.getModificationStamp();
172         if (fileModified == mLastModified) {
173             // Already have up to date data
174             return;
175         }
176         mLastModified = fileModified;
177 
178         mActivityThemes = new HashMap<String, String>();
179         mManifestTheme = null;
180         mTargetSdk = 1; // Default when not specified
181         mMinSdk = 1; // Default when not specified
182         mPackage = ""; //$NON-NLS-1$
183         mApplicationIcon = null;
184         mApplicationLabel = null;
185 
186         Document document = null;
187         try {
188             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
189             InputSource is = new InputSource(mManifestFile.getContents());
190 
191             factory.setNamespaceAware(true);
192             factory.setValidating(false);
193             DocumentBuilder builder = factory.newDocumentBuilder();
194             document = builder.parse(is);
195 
196             Element root = document.getDocumentElement();
197             mPackage = root.getAttribute(ATTRIBUTE_PACKAGE);
198             NodeList activities = document.getElementsByTagName(NODE_ACTIVITY);
199             for (int i = 0, n = activities.getLength(); i < n; i++) {
200                 Element activity = (Element) activities.item(i);
201                 String theme = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
202                 if (theme != null && theme.length() > 0) {
203                     String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME);
204                     if (name.startsWith(".")  //$NON-NLS-1$
205                             && mPackage != null && mPackage.length() > 0) {
206                         name = mPackage + name;
207                     }
208                     mActivityThemes.put(name, theme);
209                 }
210             }
211 
212             NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION);
213             String defaultTheme = null;
214             if (applications.getLength() > 0) {
215                 assert applications.getLength() == 1;
216                 Element application = (Element) applications.item(0);
217                 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) {
218                     mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON);
219                 }
220                 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) {
221                     mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL);
222                 }
223 
224                 defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
225             }
226 
227             // Look up target SDK
228             if (defaultTheme == null || defaultTheme.length() == 0) {
229                 // From manifest theme documentation:
230                 // "If that attribute is also not set, the default system theme is used."
231 
232                 NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK);
233                 if (usesSdks.getLength() > 0) {
234                     Element usesSdk = (Element) usesSdks.item(0);
235                     mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1);
236                     mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk);
237                 }
238             } else {
239                 mManifestTheme = defaultTheme;
240             }
241         } catch (SAXException e) {
242             AdtPlugin.log(e, "Malformed manifest");
243         } catch (Exception e) {
244             AdtPlugin.log(e, "Could not read Manifest data");
245         }
246     }
247 
getApiVersion(Element usesSdk, String attribute, int defaultApiLevel)248     private static int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) {
249         String valueString = null;
250         if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) {
251             valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute);
252         }
253 
254         if (valueString != null) {
255             int apiLevel = -1;
256             try {
257                 apiLevel = Integer.valueOf(valueString);
258             } catch (NumberFormatException e) {
259                 // Handle codename
260                 if (Sdk.getCurrent() != null) {
261                     IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
262                             "android-" + valueString); //$NON-NLS-1$
263                     if (target != null) {
264                         // codename future API level is current api + 1
265                         apiLevel = target.getVersion().getApiLevel() + 1;
266                     }
267                 }
268             }
269 
270             return apiLevel;
271         }
272 
273         return defaultApiLevel;
274     }
275 
276     /**
277      * Returns the default package registered in the Android manifest
278      *
279      * @return the default package registered in the manifest
280      */
281     @NonNull
getPackage()282     public String getPackage() {
283         sync();
284         return mPackage;
285     }
286 
287     /**
288      * Returns a map from activity full class names to the corresponding theme style to be
289      * used
290      *
291      * @return a map from activity fqcn to theme style
292      */
293     @NonNull
getActivityThemes()294     public Map<String, String> getActivityThemes() {
295         sync();
296         return mActivityThemes;
297     }
298 
299     /**
300      * Returns the default theme for this project, by looking at the manifest default
301      * theme registration, target SDK, rendering target, etc.
302      *
303      * @param renderingTarget the rendering target use to render the theme, or null
304      * @param screenSize the screen size to obtain a default theme for, or null if unknown
305      * @return the theme to use for this project, never null
306      */
307     @NonNull
getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize)308     public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) {
309         sync();
310 
311         if (mManifestTheme != null) {
312             return mManifestTheme;
313         }
314 
315         int renderingTargetSdk = mTargetSdk;
316         if (renderingTarget != null) {
317             renderingTargetSdk = renderingTarget.getVersion().getApiLevel();
318         }
319 
320         int apiLevel = Math.min(mTargetSdk, renderingTargetSdk);
321         // For now this theme works only on XLARGE screens. When it works for all sizes,
322         // add that new apiLevel to this check.
323         if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) {
324             return PREFIX_ANDROID_STYLE + "Theme.Holo"; //$NON-NLS-1$
325         } else {
326             return PREFIX_ANDROID_STYLE + "Theme"; //$NON-NLS-1$
327         }
328     }
329 
330     /**
331      * Returns the application icon, or null
332      *
333      * @return the application icon, or null
334      */
335     @Nullable
getApplicationIcon()336     public String getApplicationIcon() {
337         sync();
338         return mApplicationIcon;
339     }
340 
341     /**
342      * Returns the application label, or null
343      *
344      * @return the application label, or null
345      */
346     @Nullable
getApplicationLabel()347     public String getApplicationLabel() {
348         sync();
349         return mApplicationLabel;
350     }
351 
352     /**
353      * Returns the target SDK version
354      *
355      * @return the target SDK version
356      */
getTargetSdkVersion()357     public int getTargetSdkVersion() {
358         sync();
359         return mTargetSdk;
360     }
361 
362     /**
363      * Returns the minimum SDK version
364      *
365      * @return the minimum SDK version
366      */
getMinSdkVersion()367     public int getMinSdkVersion() {
368         sync();
369         return mMinSdk;
370     }
371 
372     /**
373      * Returns the {@link IPackageFragment} for the package registered in the manifest
374      *
375      * @return the {@link IPackageFragment} for the package registered in the manifest
376      */
377     @Nullable
getPackageFragment()378     public IPackageFragment getPackageFragment() {
379         sync();
380         try {
381             IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
382             if (javaProject != null) {
383                 IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
384                 if (root != null) {
385                     return root.getPackageFragment(mPackage);
386                 }
387             }
388         } catch (CoreException e) {
389             AdtPlugin.log(e, null);
390         }
391 
392         return null;
393     }
394 
395     /**
396      * Returns the activity associated with the given layout file. Makes an educated guess
397      * by peeking at the usages of the R.layout.name field corresponding to the layout and
398      * if it finds a usage.
399      *
400      * @param project the project containing the layout
401      * @param layoutName the layout whose activity we want to look up
402      * @param pkg the package containing activities
403      * @return the activity name
404      */
405     @Nullable
guessActivity(IProject project, String layoutName, String pkg)406     public static String guessActivity(IProject project, String layoutName, String pkg) {
407         final AtomicReference<String> activity = new AtomicReference<String>();
408         SearchRequestor requestor = new SearchRequestor() {
409             @Override
410             public void acceptSearchMatch(SearchMatch match) throws CoreException {
411                 Object element = match.getElement();
412                 if (element instanceof IMethod) {
413                     IMethod method = (IMethod) element;
414                     IType declaringType = method.getDeclaringType();
415                     String fqcn = declaringType.getFullyQualifiedName();
416                     if (activity.get() == null
417                             || (declaringType.getSuperclassName() != null &&
418                                     declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$
419                             || method.getElementName().equals("onCreate")) { //$NON-NLS-1$
420                         activity.set(fqcn);
421                     }
422                 }
423             }
424         };
425         try {
426             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
427             if (javaProject == null) {
428                 return null;
429             }
430             // TODO - look around a bit more and see if we can figure out whether the
431             // call if from within a setContentView call!
432 
433             // Search for which java classes call setContentView(R.layout.layoutname);
434             String typeFqcn = "R.layout"; //$NON-NLS-1$
435             if (pkg != null) {
436                 typeFqcn = pkg + '.' + typeFqcn;
437             }
438 
439             IType type = javaProject.findType(typeFqcn);
440             if (type != null) {
441                 IField field = type.getField(layoutName);
442                 if (field.exists()) {
443                     SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES);
444                     search(requestor, javaProject, pattern);
445                 }
446             }
447         } catch (CoreException e) {
448             AdtPlugin.log(e, null);
449         }
450 
451         return activity.get();
452     }
453 
454     /**
455      * Returns the activity associated with the given layout file.
456      * <p>
457      * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas
458      * guessActivity simply looks for references to "R.layout.foo", this method searches
459      * for all usages of Activity#setContentView(int), and for each match it looks up the
460      * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses
461      * a regexp to pull out "foo" from this, and stores the association that layout "foo"
462      * is associated with the activity class that contained the setContentView call.
463      * <p>
464      * This has two potential advantages:
465      * <ol>
466      * <li>It can be faster. We do the reference search -once-, and we've built a map of
467      * all the layout-to-activity mappings which we can then immediately look up other
468      * layouts for, which is particularly useful at startup when we have to compute the
469      * layout activity associations to populate the theme choosers.
470      * <li>It can be more accurate. Just because an activity references an "R.layout.foo"
471      * field doesn't mean it's setting it as a content view.
472      * </ol>
473      * However, this second advantage is also its chief problem. There are some common
474      * code constructs which means that the associated layout is not explicitly referenced
475      * in a direct setContentView call; on a couple of sample projects I tested I found
476      * patterns like for example "setContentView(v)" where "v" had been computed earlier.
477      * Therefore, for now we're going to stick with the more general approach of just
478      * looking up each field when needed. We're keeping the code around, though statically
479      * compiled out with the "if (false)" construct below in case we revisit this.
480      *
481      * @param layoutFile the layout whose activity we want to look up
482      * @return the activity name
483      */
484     @SuppressWarnings("all")
485     @Nullable
guessActivityBySetContentView(String layoutName)486     public String guessActivityBySetContentView(String layoutName) {
487         if (false) {
488             // These should be fields
489             final Pattern LAYOUT_FIELD_PATTERN =
490                 Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$
491             Map<String, String> mUsages = null;
492 
493             sync();
494             if (mUsages == null) {
495                 final Map<String, String> usages = new HashMap<String, String>();
496                 mUsages = usages;
497                 SearchRequestor requestor = new SearchRequestor() {
498                     @Override
499                     public void acceptSearchMatch(SearchMatch match) throws CoreException {
500                         Object element = match.getElement();
501                         if (element instanceof IMethod) {
502                             IMethod method = (IMethod) element;
503                             IType declaringType = method.getDeclaringType();
504                             String fqcn = declaringType.getFullyQualifiedName();
505                             IDocumentProvider provider = new TextFileDocumentProvider();
506                             IResource resource = match.getResource();
507                             try {
508                                 provider.connect(resource);
509                                 IDocument document = provider.getDocument(resource);
510                                 if (document != null) {
511                                     String matchText = document.get(match.getOffset(),
512                                             match.getLength());
513                                     Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText);
514                                     if (matcher.find()) {
515                                         usages.put(matcher.group(1), fqcn);
516                                     }
517                                 }
518                             } catch (Exception e) {
519                                 AdtPlugin.log(e, "Can't find range information for %1$s",
520                                         resource.getName());
521                             } finally {
522                                 provider.disconnect(resource);
523                             }
524                         }
525                     }
526                 };
527                 try {
528                     IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
529                     if (javaProject == null) {
530                         return null;
531                     }
532 
533                     // Search for which java classes call setContentView(R.layout.layoutname);
534                     String typeFqcn = "R.layout"; //$NON-NLS-1$
535                     if (mPackage != null) {
536                         typeFqcn = mPackage + '.' + typeFqcn;
537                     }
538 
539                     IType activityType = javaProject.findType(SdkConstants.CLASS_ACTIVITY);
540                     if (activityType != null) {
541                         IMethod method = activityType.getMethod(
542                                 "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$
543                         if (method.exists()) {
544                             SearchPattern pattern = SearchPattern.createPattern(method,
545                                     REFERENCES);
546                             search(requestor, javaProject, pattern);
547                         }
548                     }
549                 } catch (CoreException e) {
550                     AdtPlugin.log(e, null);
551                 }
552             }
553 
554             return mUsages.get(layoutName);
555         }
556 
557         return null;
558     }
559 
560     /**
561      * Performs a search using the given pattern, scope and handler. The search will abort
562      * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds.
563      */
search(SearchRequestor requestor, IJavaProject javaProject, SearchPattern pattern)564     private static void search(SearchRequestor requestor, IJavaProject javaProject,
565             SearchPattern pattern) throws CoreException {
566         // Find the package fragment specified in the manifest; the activities should
567         // live there.
568         IJavaSearchScope scope = createPackageScope(javaProject);
569 
570         SearchParticipant[] participants = new SearchParticipant[] {
571             SearchEngine.getDefaultSearchParticipant()
572         };
573         SearchEngine engine = new SearchEngine();
574 
575         final long searchStart = System.currentTimeMillis();
576         NullProgressMonitor monitor = new NullProgressMonitor() {
577             private boolean mCancelled;
578             @Override
579             public void internalWorked(double work) {
580                 long searchEnd = System.currentTimeMillis();
581                 if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) {
582                     mCancelled = true;
583                 }
584             }
585 
586             @Override
587             public boolean isCanceled() {
588                 return mCancelled;
589             }
590         };
591         engine.search(pattern, participants, scope, requestor, monitor);
592     }
593 
594     /** Creates a package search scope for the first package root in the given java project */
createPackageScope(IJavaProject javaProject)595     private static IJavaSearchScope createPackageScope(IJavaProject javaProject) {
596         IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject);
597 
598         IJavaSearchScope scope;
599         if (packageRoot != null) {
600             IJavaElement[] scopeElements = new IJavaElement[] { packageRoot };
601             scope = SearchEngine.createJavaSearchScope(scopeElements);
602         } else {
603             scope = SearchEngine.createWorkspaceScope();
604         }
605         return scope;
606     }
607 
608     /**
609      * Returns the first package root for the given java project
610      *
611      * @param javaProject the project to search in
612      * @return the first package root, or null
613      */
614     @Nullable
getSourcePackageRoot(IJavaProject javaProject)615     public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) {
616         IPackageFragmentRoot packageRoot = null;
617         List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject);
618 
619         IWorkspace workspace = ResourcesPlugin.getWorkspace();
620         for (IPath path : sources) {
621             IResource firstSource = workspace.getRoot().findMember(path);
622             if (firstSource != null) {
623                 packageRoot = javaProject.getPackageFragmentRoot(firstSource);
624                 if (packageRoot != null) {
625                     break;
626                 }
627             }
628         }
629         return packageRoot;
630     }
631 
632     /**
633      * Computes the minimum SDK and target SDK versions for the project
634      *
635      * @param project the project to look up the versions for
636      * @return a pair of (minimum SDK, target SDK) versions, never null
637      */
638     @NonNull
computeSdkVersions(IProject project)639     public static Pair<Integer, Integer> computeSdkVersions(IProject project) {
640         int mMinSdkVersion = 1;
641         int mTargetSdkVersion = 1;
642 
643         IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project));
644         if (manifestFile != null) {
645             try {
646                 Object value = AndroidManifest.getMinSdkVersion(manifestFile);
647                 mMinSdkVersion = 1; // Default case if missing
648                 if (value instanceof Integer) {
649                     mMinSdkVersion = ((Integer) value).intValue();
650                 } else if (value instanceof String) {
651                     // handle codename, only if we can resolve it.
652                     if (Sdk.getCurrent() != null) {
653                         IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
654                                 "android-" + value); //$NON-NLS-1$
655                         if (target != null) {
656                             // codename future API level is current api + 1
657                             mMinSdkVersion = target.getVersion().getApiLevel() + 1;
658                         }
659                     }
660                 }
661 
662                 Integer i = AndroidManifest.getTargetSdkVersion(manifestFile);
663                 if (i == null) {
664                     mTargetSdkVersion = mMinSdkVersion;
665                 } else {
666                     mTargetSdkVersion = i.intValue();
667                 }
668             } catch (XPathExpressionException e) {
669                 // do nothing we'll use 1 below.
670             } catch (StreamException e) {
671                 // do nothing we'll use 1 below.
672             }
673         }
674 
675         return Pair.of(mMinSdkVersion, mTargetSdkVersion);
676     }
677 }
678