• 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.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