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.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
20 import static com.android.SdkConstants.CLASS_ACTIVITY;
21 import static com.android.SdkConstants.NS_RESOURCES;
22 import static com.android.xml.AndroidManifest.ATTRIBUTE_ICON;
23 import static com.android.xml.AndroidManifest.ATTRIBUTE_LABEL;
24 import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION;
25 import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
26 import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE;
27 import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME;
28 import static com.android.xml.AndroidManifest.ATTRIBUTE_SUPPORTS_RTL;
29 import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION;
30 import static com.android.xml.AndroidManifest.ATTRIBUTE_THEME;
31 import static com.android.xml.AndroidManifest.ATTRIBUTE_UI_OPTIONS;
32 import static com.android.xml.AndroidManifest.ATTRIBUTE_VALUE;
33 import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
34 import static com.android.xml.AndroidManifest.NODE_METADATA;
35 import static com.android.xml.AndroidManifest.NODE_USES_SDK;
36 import static com.android.xml.AndroidManifest.VALUE_PARENT_ACTIVITY;
37 import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES;
38 
39 import com.android.SdkConstants;
40 import com.android.annotations.NonNull;
41 import com.android.annotations.Nullable;
42 import com.android.ide.eclipse.adt.AdtPlugin;
43 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
44 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
45 import com.android.ide.eclipse.adt.io.IFolderWrapper;
46 import com.android.io.IAbstractFile;
47 import com.android.io.StreamException;
48 import com.android.resources.ScreenSize;
49 import com.android.sdklib.IAndroidTarget;
50 import com.android.utils.Pair;
51 import com.android.xml.AndroidManifest;
52 
53 import org.eclipse.core.resources.IFile;
54 import org.eclipse.core.resources.IProject;
55 import org.eclipse.core.resources.IResource;
56 import org.eclipse.core.resources.IWorkspace;
57 import org.eclipse.core.resources.ResourcesPlugin;
58 import org.eclipse.core.runtime.CoreException;
59 import org.eclipse.core.runtime.IPath;
60 import org.eclipse.core.runtime.NullProgressMonitor;
61 import org.eclipse.core.runtime.OperationCanceledException;
62 import org.eclipse.core.runtime.QualifiedName;
63 import org.eclipse.jdt.core.IField;
64 import org.eclipse.jdt.core.IJavaElement;
65 import org.eclipse.jdt.core.IJavaProject;
66 import org.eclipse.jdt.core.IMethod;
67 import org.eclipse.jdt.core.IPackageFragment;
68 import org.eclipse.jdt.core.IPackageFragmentRoot;
69 import org.eclipse.jdt.core.IType;
70 import org.eclipse.jdt.core.ITypeHierarchy;
71 import org.eclipse.jdt.core.search.IJavaSearchScope;
72 import org.eclipse.jdt.core.search.SearchEngine;
73 import org.eclipse.jdt.core.search.SearchMatch;
74 import org.eclipse.jdt.core.search.SearchParticipant;
75 import org.eclipse.jdt.core.search.SearchPattern;
76 import org.eclipse.jdt.core.search.SearchRequestor;
77 import org.eclipse.jdt.internal.core.BinaryType;
78 import org.eclipse.jface.text.IDocument;
79 import org.eclipse.ui.editors.text.TextFileDocumentProvider;
80 import org.eclipse.ui.texteditor.IDocumentProvider;
81 import org.w3c.dom.Document;
82 import org.w3c.dom.Element;
83 import org.w3c.dom.NodeList;
84 import org.xml.sax.InputSource;
85 import org.xml.sax.SAXException;
86 
87 import java.util.ArrayList;
88 import java.util.Collections;
89 import java.util.HashMap;
90 import java.util.LinkedList;
91 import java.util.List;
92 import java.util.Map;
93 import java.util.regex.Matcher;
94 import java.util.regex.Pattern;
95 
96 import javax.xml.parsers.DocumentBuilder;
97 import javax.xml.parsers.DocumentBuilderFactory;
98 import javax.xml.xpath.XPathExpressionException;
99 
100 /**
101  * Retrieves and caches manifest information such as the themes to be used for
102  * a given activity.
103  *
104  * @see AndroidManifest
105  */
106 public class ManifestInfo {
107 
108     public static class ActivityAttributes {
109         @Nullable
110         private final String mIcon;
111         @Nullable
112         private final String mLabel;
113         @NonNull
114         private final String mName;
115         @Nullable
116         private final String mParentActivity;
117         @Nullable
118         private final String mTheme;
119         @Nullable
120         private final String mUiOptions;
121 
ActivityAttributes(Element activity, String packageName)122         public ActivityAttributes(Element activity, String packageName) {
123 
124             // Get activity name.
125             String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME);
126             if (name == null || name.length() == 0) {
127                 throw new RuntimeException("Activity name cannot be empty");
128             }
129             int index = name.indexOf('.');
130             if (index <= 0 && packageName != null && !packageName.isEmpty()) {
131               name =  packageName + (index == -1 ? "." : "") + name;
132             }
133             mName = name;
134 
135             // Get activity icon.
136             String value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON);
137             if (value != null && value.length() > 0) {
138                 mIcon = value;
139             } else {
140                 mIcon = null;
141             }
142 
143             // Get activity label.
144             value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL);
145             if (value != null && value.length() > 0) {
146                 mLabel = value;
147             } else {
148                 mLabel = null;
149             }
150 
151             // Get activity parent. Also search the meta-data for parent info.
152             value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_PARENT_ACTIVITY_NAME);
153             if (value == null || value.length() == 0) {
154                 // TODO: Not sure if meta data can be used for API Level > 16
155                 NodeList metaData = activity.getElementsByTagName(NODE_METADATA);
156                 for (int j = 0, m = metaData.getLength(); j < m; j++) {
157                     Element data = (Element) metaData.item(j);
158                     String metadataName = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME);
159                     if (VALUE_PARENT_ACTIVITY.equals(metadataName)) {
160                         value = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_VALUE);
161                         if (value != null) {
162                             index = value.indexOf('.');
163                             if (index <= 0 && packageName != null && !packageName.isEmpty()) {
164                                 value = packageName + (index == -1 ? "." : "") + value;
165                                 break;
166                             }
167                         }
168                     }
169                 }
170             }
171             if (value != null && value.length() > 0) {
172                 mParentActivity = value;
173             } else {
174                 mParentActivity = null;
175             }
176 
177             // Get activity theme.
178             value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
179             if (value != null && value.length() > 0) {
180                 mTheme = value;
181             } else {
182                 mTheme = null;
183             }
184 
185             // Get UI options.
186             value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_UI_OPTIONS);
187             if (value != null && value.length() > 0) {
188                 mUiOptions = value;
189             } else {
190                 mUiOptions = null;
191             }
192         }
193 
194         @Nullable
getIcon()195         public String getIcon() {
196             return mIcon;
197         }
198 
199         @Nullable
getLabel()200         public String getLabel() {
201             return mLabel;
202         }
203 
getName()204         public String getName() {
205             return mName;
206         }
207 
208         @Nullable
getParentActivity()209         public String getParentActivity() {
210             return mParentActivity;
211         }
212 
213         @Nullable
getTheme()214         public String getTheme() {
215             return mTheme;
216         }
217 
218         @Nullable
getUiOptions()219         public String getUiOptions() {
220             return mUiOptions;
221         }
222     }
223 
224     /**
225      * The maximum number of milliseconds to search for an activity in the codebase when
226      * attempting to associate layouts with activities in
227      * {@link #guessActivity(IFile, String)}
228      */
229     private static final int SEARCH_TIMEOUT_MS = 3000;
230 
231     private final IProject mProject;
232     private String mPackage;
233     private String mManifestTheme;
234     private Map<String, ActivityAttributes> mActivityAttributes;
235     private IAbstractFile mManifestFile;
236     private long mLastModified;
237     private long mLastChecked;
238     private String mMinSdkName;
239     private int mMinSdk;
240     private int mTargetSdk;
241     private String mApplicationIcon;
242     private String mApplicationLabel;
243     private boolean mApplicationSupportsRtl;
244 
245     /**
246      * Qualified name for the per-project non-persistent property storing the
247      * {@link ManifestInfo} for this project
248      */
249     final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
250             "manifest"); //$NON-NLS-1$
251 
252     /**
253      * Constructs an {@link ManifestInfo} for the given project. Don't use this method;
254      * use the {@link #get} factory method instead.
255      *
256      * @param project project to create an {@link ManifestInfo} for
257      */
ManifestInfo(IProject project)258     private ManifestInfo(IProject project) {
259         mProject = project;
260     }
261 
262     /**
263      * Clears the cached manifest information. The next get call on one of the
264      * properties will cause the information to be refreshed.
265      */
clear()266     public void clear() {
267         mLastChecked = 0;
268     }
269 
270     /**
271      * Returns the {@link ManifestInfo} for the given project
272      *
273      * @param project the project the finder is associated with
274      * @return a {@ManifestInfo} for the given project, never null
275      */
276     @NonNull
get(IProject project)277     public static ManifestInfo get(IProject project) {
278         ManifestInfo finder = null;
279         try {
280             finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER);
281         } catch (CoreException e) {
282             // Not a problem; we will just create a new one
283         }
284 
285         if (finder == null) {
286             finder = new ManifestInfo(project);
287             try {
288                 project.setSessionProperty(MANIFEST_FINDER, finder);
289             } catch (CoreException e) {
290                 AdtPlugin.log(e, "Can't store ManifestInfo");
291             }
292         }
293 
294         return finder;
295     }
296 
297     /**
298      * Ensure that the package, theme and activity maps are initialized and up to date
299      * with respect to the manifest file
300      */
sync()301     private void sync() {
302         // Since each of the accessors call sync(), allow a bunch of immediate
303         // accessors to all bypass the file stat() below
304         long now = System.currentTimeMillis();
305         if (now - mLastChecked < 50 && mManifestFile != null) {
306             return;
307         }
308         mLastChecked = now;
309 
310         if (mManifestFile == null) {
311             IFolderWrapper projectFolder = new IFolderWrapper(mProject);
312             mManifestFile = AndroidManifest.getManifest(projectFolder);
313             if (mManifestFile == null) {
314                 return;
315             }
316         }
317 
318         // Check to see if our data is up to date
319         long fileModified = mManifestFile.getModificationStamp();
320         if (fileModified == mLastModified) {
321             // Already have up to date data
322             return;
323         }
324         mLastModified = fileModified;
325 
326         mActivityAttributes = new HashMap<String, ActivityAttributes>();
327         mManifestTheme = null;
328         mTargetSdk = 1; // Default when not specified
329         mMinSdk = 1; // Default when not specified
330         mMinSdkName = "1"; // Default when not specified
331         mPackage = ""; //$NON-NLS-1$
332         mApplicationIcon = null;
333         mApplicationLabel = null;
334         mApplicationSupportsRtl = false;
335 
336         Document document = null;
337         try {
338             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
339             InputSource is = new InputSource(mManifestFile.getContents());
340 
341             factory.setNamespaceAware(true);
342             factory.setValidating(false);
343             DocumentBuilder builder = factory.newDocumentBuilder();
344             document = builder.parse(is);
345 
346             Element root = document.getDocumentElement();
347             mPackage = root.getAttribute(ATTRIBUTE_PACKAGE);
348             NodeList activities = document.getElementsByTagName(NODE_ACTIVITY);
349             for (int i = 0, n = activities.getLength(); i < n; i++) {
350                 Element activity = (Element) activities.item(i);
351                 ActivityAttributes info = new ActivityAttributes(activity, mPackage);
352                 mActivityAttributes.put(info.getName(), info);
353             }
354 
355             NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION);
356             if (applications.getLength() > 0) {
357                 assert applications.getLength() == 1;
358                 Element application = (Element) applications.item(0);
359                 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) {
360                     mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON);
361                 }
362                 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) {
363                     mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL);
364                 }
365                 if (SdkConstants.VALUE_TRUE.equals(application.getAttributeNS(NS_RESOURCES,
366                         ATTRIBUTE_SUPPORTS_RTL))) {
367                     mApplicationSupportsRtl = true;
368                 }
369 
370                 String defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME);
371                 if (defaultTheme != null && !defaultTheme.isEmpty()) {
372                     // From manifest theme documentation:
373                     // "If that attribute is also not set, the default system theme is used."
374                     mManifestTheme = defaultTheme;
375                 }
376             }
377 
378             // Look up target SDK
379             NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK);
380             if (usesSdks.getLength() > 0) {
381                 Element usesSdk = (Element) usesSdks.item(0);
382                 mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1);
383                 mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk);
384             }
385 
386         } catch (SAXException e) {
387             AdtPlugin.log(e, "Malformed manifest");
388         } catch (Exception e) {
389             AdtPlugin.log(e, "Could not read Manifest data");
390         }
391     }
392 
getApiVersion(Element usesSdk, String attribute, int defaultApiLevel)393     private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) {
394         String valueString = null;
395         if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) {
396             valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute);
397             if (attribute.equals(ATTRIBUTE_MIN_SDK_VERSION)) {
398                 mMinSdkName = valueString;
399             }
400         }
401 
402         if (valueString != null) {
403             int apiLevel = -1;
404             try {
405                 apiLevel = Integer.valueOf(valueString);
406             } catch (NumberFormatException e) {
407                 // Handle codename
408                 if (Sdk.getCurrent() != null) {
409                     IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
410                             "android-" + valueString); //$NON-NLS-1$
411                     if (target != null) {
412                         // codename future API level is current api + 1
413                         apiLevel = target.getVersion().getApiLevel() + 1;
414                     }
415                 }
416             }
417 
418             return apiLevel;
419         }
420 
421         return defaultApiLevel;
422     }
423 
424     /**
425      * Returns the default package registered in the Android manifest
426      *
427      * @return the default package registered in the manifest
428      */
429     @NonNull
getPackage()430     public String getPackage() {
431         sync();
432         return mPackage;
433     }
434 
435     /**
436      * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}.
437      *
438      * @return a map from activity fqcn to ActivityAttributes
439      */
440     @NonNull
getActivityAttributesMap()441     public Map<String, ActivityAttributes> getActivityAttributesMap() {
442         sync();
443         return mActivityAttributes;
444     }
445 
446     /**
447      * Returns the attributes of an activity given its full class name.
448      */
449     @Nullable
getActivityAttributes(String activity)450     public ActivityAttributes getActivityAttributes(String activity) {
451         return getActivityAttributesMap().get(activity);
452     }
453 
454     /**
455      * Returns the manifest theme registered on the application, if any
456      *
457      * @return a manifest theme, or null if none was registered
458      */
459     @Nullable
getManifestTheme()460     public String getManifestTheme() {
461         sync();
462         return mManifestTheme;
463     }
464 
465     /**
466      * Returns the default theme for this project, by looking at the manifest default
467      * theme registration, target SDK, rendering target, etc.
468      *
469      * @param renderingTarget the rendering target use to render the theme, or null
470      * @param screenSize the screen size to obtain a default theme for, or null if unknown
471      * @return the theme to use for this project, never null
472      */
473     @NonNull
getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize)474     public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) {
475         sync();
476 
477         if (mManifestTheme != null) {
478             return mManifestTheme;
479         }
480 
481         int renderingTargetSdk = mTargetSdk;
482         if (renderingTarget != null) {
483             renderingTargetSdk = renderingTarget.getVersion().getApiLevel();
484         }
485 
486         int apiLevel = Math.min(mTargetSdk, renderingTargetSdk);
487         // For now this theme works only on XLARGE screens. When it works for all sizes,
488         // add that new apiLevel to this check.
489         if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) {
490             return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$
491         } else {
492             return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$
493         }
494     }
495 
496     /**
497      * Returns the application icon, or null
498      *
499      * @return the application icon, or null
500      */
501     @Nullable
getApplicationIcon()502     public String getApplicationIcon() {
503         sync();
504         return mApplicationIcon;
505     }
506 
507     /**
508      * Returns the application label, or null
509      *
510      * @return the application label, or null
511      */
512     @Nullable
getApplicationLabel()513     public String getApplicationLabel() {
514         sync();
515         return mApplicationLabel;
516     }
517 
518     /**
519      * Returns true if the application has RTL support.
520      *
521      * @return true if the application has RTL support.
522      */
isRtlSupported()523     public boolean isRtlSupported() {
524         sync();
525         return mApplicationSupportsRtl;
526     }
527 
528     /**
529      * Returns the target SDK version
530      *
531      * @return the target SDK version
532      */
getTargetSdkVersion()533     public int getTargetSdkVersion() {
534         sync();
535         return mTargetSdk;
536     }
537 
538     /**
539      * Returns the minimum SDK version
540      *
541      * @return the minimum SDK version
542      */
getMinSdkVersion()543     public int getMinSdkVersion() {
544         sync();
545         return mMinSdk;
546     }
547 
548     /**
549      * Returns the minimum SDK version name (which may not be a numeric string, e.g.
550      * it could be a codename). It will never be null or empty; if no min sdk version
551      * was specified in the manifest, the return value will be "1". Use
552      * {@link #getMinSdkCodeName()} instead if you want to look up whether there is a code name.
553      *
554      * @return the minimum SDK version
555      */
556     @NonNull
getMinSdkName()557     public String getMinSdkName() {
558         sync();
559         if (mMinSdkName == null || mMinSdkName.isEmpty()) {
560             mMinSdkName = "1"; //$NON-NLS-1$
561         }
562 
563         return mMinSdkName;
564     }
565 
566     /**
567      * Returns the code name used for the minimum SDK version, if any.
568      *
569      * @return the minSdkVersion codename or null
570      */
571     @Nullable
getMinSdkCodeName()572     public String getMinSdkCodeName() {
573         String minSdkName = getMinSdkName();
574         if (!Character.isDigit(minSdkName.charAt(0))) {
575             return minSdkName;
576         }
577 
578         return null;
579     }
580 
581     /**
582      * Returns the {@link IPackageFragment} for the package registered in the manifest
583      *
584      * @return the {@link IPackageFragment} for the package registered in the manifest
585      */
586     @Nullable
getPackageFragment()587     public IPackageFragment getPackageFragment() {
588         sync();
589         try {
590             IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
591             if (javaProject != null) {
592                 IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
593                 if (root != null) {
594                     return root.getPackageFragment(mPackage);
595                 }
596             }
597         } catch (CoreException e) {
598             AdtPlugin.log(e, null);
599         }
600 
601         return null;
602     }
603 
604     /**
605      * Returns the activity associated with the given layout file. Makes an educated guess
606      * by peeking at the usages of the R.layout.name field corresponding to the layout and
607      * if it finds a usage.
608      *
609      * @param project the project containing the layout
610      * @param layoutName the layout whose activity we want to look up
611      * @param pkg the package containing activities
612      * @return the activity name
613      */
614     @Nullable
guessActivity(IProject project, String layoutName, String pkg)615     public static String guessActivity(IProject project, String layoutName, String pkg) {
616         List<String> activities = guessActivities(project, layoutName, pkg);
617         if (activities.size() > 0) {
618             return activities.get(0);
619         } else {
620             return null;
621         }
622     }
623 
624     /**
625      * Returns the activities associated with the given layout file. Makes an educated guess
626      * by peeking at the usages of the R.layout.name field corresponding to the layout and
627      * if it finds a usage.
628      *
629      * @param project the project containing the layout
630      * @param layoutName the layout whose activity we want to look up
631      * @param pkg the package containing activities
632      * @return the activity name
633      */
634     @NonNull
guessActivities(IProject project, String layoutName, String pkg)635     public static List<String> guessActivities(IProject project, String layoutName, String pkg) {
636         final LinkedList<String> activities = new LinkedList<String>();
637         SearchRequestor requestor = new SearchRequestor() {
638             @Override
639             public void acceptSearchMatch(SearchMatch match) throws CoreException {
640                 Object element = match.getElement();
641                 if (element instanceof IMethod) {
642                     IMethod method = (IMethod) element;
643                     IType declaringType = method.getDeclaringType();
644                     String fqcn = declaringType.getFullyQualifiedName();
645 
646                     if ((declaringType.getSuperclassName() != null &&
647                             declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$
648                         || method.getElementName().equals("onCreate")) { //$NON-NLS-1$
649                         activities.addFirst(fqcn);
650                     } else {
651                         activities.addLast(fqcn);
652                     }
653                 }
654             }
655         };
656         try {
657             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
658             if (javaProject == null) {
659                 return Collections.emptyList();
660             }
661             // TODO - look around a bit more and see if we can figure out whether the
662             // call if from within a setContentView call!
663 
664             // Search for which java classes call setContentView(R.layout.layoutname);
665             String typeFqcn = "R.layout"; //$NON-NLS-1$
666             if (pkg != null) {
667                 typeFqcn = pkg + '.' + typeFqcn;
668             }
669 
670             IType type = javaProject.findType(typeFqcn);
671             if (type != null) {
672                 IField field = type.getField(layoutName);
673                 if (field.exists()) {
674                     SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES);
675                     try {
676                         search(requestor, javaProject, pattern);
677                     } catch (OperationCanceledException canceled) {
678                         // pass
679                     }
680                 }
681             }
682         } catch (CoreException e) {
683             AdtPlugin.log(e, null);
684         }
685 
686         return activities;
687     }
688 
689     /**
690      * Returns all activities found in the given project (including those in libraries,
691      * except for android.jar itself)
692      *
693      * @param project the project
694      * @return a list of activity classes as fully qualified class names
695      */
696     @SuppressWarnings("restriction") // BinaryType
697     @NonNull
getProjectActivities(IProject project)698     public static List<String> getProjectActivities(IProject project) {
699         final List<String> activities = new ArrayList<String>();
700         try {
701             final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
702             if (javaProject != null) {
703                 IType[] activityTypes = new IType[0];
704                 IType activityType = javaProject.findType(CLASS_ACTIVITY);
705                 if (activityType != null) {
706                     ITypeHierarchy hierarchy =
707                         activityType.newTypeHierarchy(javaProject, new NullProgressMonitor());
708                     activityTypes = hierarchy.getAllSubtypes(activityType);
709                     for (IType type : activityTypes) {
710                         if (type instanceof BinaryType && (type.getClassFile() == null
711                                     || type.getClassFile().getResource() == null)) {
712                             continue;
713                         }
714                         activities.add(type.getFullyQualifiedName());
715                     }
716                 }
717             }
718         } catch (CoreException e) {
719             AdtPlugin.log(e, null);
720         }
721 
722         return activities;
723     }
724 
725 
726     /**
727      * Returns the activity associated with the given layout file.
728      * <p>
729      * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas
730      * guessActivity simply looks for references to "R.layout.foo", this method searches
731      * for all usages of Activity#setContentView(int), and for each match it looks up the
732      * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses
733      * a regexp to pull out "foo" from this, and stores the association that layout "foo"
734      * is associated with the activity class that contained the setContentView call.
735      * <p>
736      * This has two potential advantages:
737      * <ol>
738      * <li>It can be faster. We do the reference search -once-, and we've built a map of
739      * all the layout-to-activity mappings which we can then immediately look up other
740      * layouts for, which is particularly useful at startup when we have to compute the
741      * layout activity associations to populate the theme choosers.
742      * <li>It can be more accurate. Just because an activity references an "R.layout.foo"
743      * field doesn't mean it's setting it as a content view.
744      * </ol>
745      * However, this second advantage is also its chief problem. There are some common
746      * code constructs which means that the associated layout is not explicitly referenced
747      * in a direct setContentView call; on a couple of sample projects I tested I found
748      * patterns like for example "setContentView(v)" where "v" had been computed earlier.
749      * Therefore, for now we're going to stick with the more general approach of just
750      * looking up each field when needed. We're keeping the code around, though statically
751      * compiled out with the "if (false)" construct below in case we revisit this.
752      *
753      * @param layoutFile the layout whose activity we want to look up
754      * @return the activity name
755      */
756     @SuppressWarnings("all")
757     @Nullable
guessActivityBySetContentView(String layoutName)758     public String guessActivityBySetContentView(String layoutName) {
759         if (false) {
760             // These should be fields
761             final Pattern LAYOUT_FIELD_PATTERN =
762                 Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$
763             Map<String, String> mUsages = null;
764 
765             sync();
766             if (mUsages == null) {
767                 final Map<String, String> usages = new HashMap<String, String>();
768                 mUsages = usages;
769                 SearchRequestor requestor = new SearchRequestor() {
770                     @Override
771                     public void acceptSearchMatch(SearchMatch match) throws CoreException {
772                         Object element = match.getElement();
773                         if (element instanceof IMethod) {
774                             IMethod method = (IMethod) element;
775                             IType declaringType = method.getDeclaringType();
776                             String fqcn = declaringType.getFullyQualifiedName();
777                             IDocumentProvider provider = new TextFileDocumentProvider();
778                             IResource resource = match.getResource();
779                             try {
780                                 provider.connect(resource);
781                                 IDocument document = provider.getDocument(resource);
782                                 if (document != null) {
783                                     String matchText = document.get(match.getOffset(),
784                                             match.getLength());
785                                     Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText);
786                                     if (matcher.find()) {
787                                         usages.put(matcher.group(1), fqcn);
788                                     }
789                                 }
790                             } catch (Exception e) {
791                                 AdtPlugin.log(e, "Can't find range information for %1$s",
792                                         resource.getName());
793                             } finally {
794                                 provider.disconnect(resource);
795                             }
796                         }
797                     }
798                 };
799                 try {
800                     IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
801                     if (javaProject == null) {
802                         return null;
803                     }
804 
805                     // Search for which java classes call setContentView(R.layout.layoutname);
806                     String typeFqcn = "R.layout"; //$NON-NLS-1$
807                     if (mPackage != null) {
808                         typeFqcn = mPackage + '.' + typeFqcn;
809                     }
810 
811                     IType activityType = javaProject.findType(CLASS_ACTIVITY);
812                     if (activityType != null) {
813                         IMethod method = activityType.getMethod(
814                                 "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$
815                         if (method.exists()) {
816                             SearchPattern pattern = SearchPattern.createPattern(method,
817                                     REFERENCES);
818                             search(requestor, javaProject, pattern);
819                         }
820                     }
821                 } catch (CoreException e) {
822                     AdtPlugin.log(e, null);
823                 }
824             }
825 
826             return mUsages.get(layoutName);
827         }
828 
829         return null;
830     }
831 
832     /**
833      * Performs a search using the given pattern, scope and handler. The search will abort
834      * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds.
835      */
search(SearchRequestor requestor, IJavaProject javaProject, SearchPattern pattern)836     private static void search(SearchRequestor requestor, IJavaProject javaProject,
837             SearchPattern pattern) throws CoreException {
838         // Find the package fragment specified in the manifest; the activities should
839         // live there.
840         IJavaSearchScope scope = createPackageScope(javaProject);
841 
842         SearchParticipant[] participants = new SearchParticipant[] {
843             SearchEngine.getDefaultSearchParticipant()
844         };
845         SearchEngine engine = new SearchEngine();
846 
847         final long searchStart = System.currentTimeMillis();
848         NullProgressMonitor monitor = new NullProgressMonitor() {
849             private boolean mCancelled;
850             @Override
851             public void internalWorked(double work) {
852                 long searchEnd = System.currentTimeMillis();
853                 if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) {
854                     mCancelled = true;
855                 }
856             }
857 
858             @Override
859             public boolean isCanceled() {
860                 return mCancelled;
861             }
862         };
863         engine.search(pattern, participants, scope, requestor, monitor);
864     }
865 
866     /** Creates a package search scope for the first package root in the given java project */
createPackageScope(IJavaProject javaProject)867     private static IJavaSearchScope createPackageScope(IJavaProject javaProject) {
868         IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject);
869 
870         IJavaSearchScope scope;
871         if (packageRoot != null) {
872             IJavaElement[] scopeElements = new IJavaElement[] { packageRoot };
873             scope = SearchEngine.createJavaSearchScope(scopeElements);
874         } else {
875             scope = SearchEngine.createWorkspaceScope();
876         }
877         return scope;
878     }
879 
880     /**
881      * Returns the first package root for the given java project
882      *
883      * @param javaProject the project to search in
884      * @return the first package root, or null
885      */
886     @Nullable
getSourcePackageRoot(IJavaProject javaProject)887     public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) {
888         IPackageFragmentRoot packageRoot = null;
889         List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject);
890 
891         IWorkspace workspace = ResourcesPlugin.getWorkspace();
892         for (IPath path : sources) {
893             IResource firstSource = workspace.getRoot().findMember(path);
894             if (firstSource != null) {
895                 packageRoot = javaProject.getPackageFragmentRoot(firstSource);
896                 if (packageRoot != null) {
897                     break;
898                 }
899             }
900         }
901         return packageRoot;
902     }
903 
904     /**
905      * Computes the minimum SDK and target SDK versions for the project
906      *
907      * @param project the project to look up the versions for
908      * @return a pair of (minimum SDK, target SDK) versions, never null
909      */
910     @NonNull
computeSdkVersions(IProject project)911     public static Pair<Integer, Integer> computeSdkVersions(IProject project) {
912         int mMinSdkVersion = 1;
913         int mTargetSdkVersion = 1;
914 
915         IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project));
916         if (manifestFile != null) {
917             try {
918                 Object value = AndroidManifest.getMinSdkVersion(manifestFile);
919                 mMinSdkVersion = 1; // Default case if missing
920                 if (value instanceof Integer) {
921                     mMinSdkVersion = ((Integer) value).intValue();
922                 } else if (value instanceof String) {
923                     // handle codename, only if we can resolve it.
924                     if (Sdk.getCurrent() != null) {
925                         IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
926                                 "android-" + value); //$NON-NLS-1$
927                         if (target != null) {
928                             // codename future API level is current api + 1
929                             mMinSdkVersion = target.getVersion().getApiLevel() + 1;
930                         }
931                     }
932                 }
933 
934                 value = AndroidManifest.getTargetSdkVersion(manifestFile);
935                 if (value == null) {
936                     mTargetSdkVersion = mMinSdkVersion;
937                 } else if (value instanceof String) {
938                     // handle codename, only if we can resolve it.
939                     if (Sdk.getCurrent() != null) {
940                         IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(
941                                 "android-" + value); //$NON-NLS-1$
942                         if (target != null) {
943                             // codename future API level is current api + 1
944                         	mTargetSdkVersion = target.getVersion().getApiLevel() + 1;
945                         }
946                     }
947                 }
948             } catch (XPathExpressionException e) {
949                 // do nothing we'll use 1 below.
950             } catch (StreamException e) {
951                 // do nothing we'll use 1 below.
952             }
953         }
954 
955         return Pair.of(mMinSdkVersion, mTargetSdkVersion);
956     }
957 }
958