Home | History | Annotate | Download | only in manifest
      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      */
    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
    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      */
    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 
    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
    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
    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
    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
    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
    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      */
    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      */
    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
    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
    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
    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      */
    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 */
    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
    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
    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