diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java | 957 |
1 files changed, 957 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java new file mode 100644 index 000000000..6d2d1c1f2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java @@ -0,0 +1,957 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.manifest; + +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.SdkConstants.NS_RESOURCES; +import static com.android.xml.AndroidManifest.ATTRIBUTE_ICON; +import static com.android.xml.AndroidManifest.ATTRIBUTE_LABEL; +import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; +import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_SUPPORTS_RTL; +import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; +import static com.android.xml.AndroidManifest.ATTRIBUTE_THEME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_UI_OPTIONS; +import static com.android.xml.AndroidManifest.ATTRIBUTE_VALUE; +import static com.android.xml.AndroidManifest.NODE_ACTIVITY; +import static com.android.xml.AndroidManifest.NODE_METADATA; +import static com.android.xml.AndroidManifest.NODE_USES_SDK; +import static com.android.xml.AndroidManifest.VALUE_PARENT_ACTIVITY; +import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.resources.ScreenSize; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.core.BinaryType; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathExpressionException; + +/** + * Retrieves and caches manifest information such as the themes to be used for + * a given activity. + * + * @see AndroidManifest + */ +public class ManifestInfo { + + public static class ActivityAttributes { + @Nullable + private final String mIcon; + @Nullable + private final String mLabel; + @NonNull + private final String mName; + @Nullable + private final String mParentActivity; + @Nullable + private final String mTheme; + @Nullable + private final String mUiOptions; + + public ActivityAttributes(Element activity, String packageName) { + + // Get activity name. + String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); + if (name == null || name.length() == 0) { + throw new RuntimeException("Activity name cannot be empty"); + } + int index = name.indexOf('.'); + if (index <= 0 && packageName != null && !packageName.isEmpty()) { + name = packageName + (index == -1 ? "." : "") + name; + } + mName = name; + + // Get activity icon. + String value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); + if (value != null && value.length() > 0) { + mIcon = value; + } else { + mIcon = null; + } + + // Get activity label. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); + if (value != null && value.length() > 0) { + mLabel = value; + } else { + mLabel = null; + } + + // Get activity parent. Also search the meta-data for parent info. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_PARENT_ACTIVITY_NAME); + if (value == null || value.length() == 0) { + // TODO: Not sure if meta data can be used for API Level > 16 + NodeList metaData = activity.getElementsByTagName(NODE_METADATA); + for (int j = 0, m = metaData.getLength(); j < m; j++) { + Element data = (Element) metaData.item(j); + String metadataName = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); + if (VALUE_PARENT_ACTIVITY.equals(metadataName)) { + value = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_VALUE); + if (value != null) { + index = value.indexOf('.'); + if (index <= 0 && packageName != null && !packageName.isEmpty()) { + value = packageName + (index == -1 ? "." : "") + value; + break; + } + } + } + } + } + if (value != null && value.length() > 0) { + mParentActivity = value; + } else { + mParentActivity = null; + } + + // Get activity theme. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); + if (value != null && value.length() > 0) { + mTheme = value; + } else { + mTheme = null; + } + + // Get UI options. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_UI_OPTIONS); + if (value != null && value.length() > 0) { + mUiOptions = value; + } else { + mUiOptions = null; + } + } + + @Nullable + public String getIcon() { + return mIcon; + } + + @Nullable + public String getLabel() { + return mLabel; + } + + public String getName() { + return mName; + } + + @Nullable + public String getParentActivity() { + return mParentActivity; + } + + @Nullable + public String getTheme() { + return mTheme; + } + + @Nullable + public String getUiOptions() { + return mUiOptions; + } + } + + /** + * The maximum number of milliseconds to search for an activity in the codebase when + * attempting to associate layouts with activities in + * {@link #guessActivity(IFile, String)} + */ + private static final int SEARCH_TIMEOUT_MS = 3000; + + private final IProject mProject; + private String mPackage; + private String mManifestTheme; + private Map<String, ActivityAttributes> mActivityAttributes; + private IAbstractFile mManifestFile; + private long mLastModified; + private long mLastChecked; + private String mMinSdkName; + private int mMinSdk; + private int mTargetSdk; + private String mApplicationIcon; + private String mApplicationLabel; + private boolean mApplicationSupportsRtl; + + /** + * Qualified name for the per-project non-persistent property storing the + * {@link ManifestInfo} for this project + */ + final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, + "manifest"); //$NON-NLS-1$ + + /** + * Constructs an {@link ManifestInfo} for the given project. Don't use this method; + * use the {@link #get} factory method instead. + * + * @param project project to create an {@link ManifestInfo} for + */ + private ManifestInfo(IProject project) { + mProject = project; + } + + /** + * Clears the cached manifest information. The next get call on one of the + * properties will cause the information to be refreshed. + */ + public void clear() { + mLastChecked = 0; + } + + /** + * Returns the {@link ManifestInfo} for the given project + * + * @param project the project the finder is associated with + * @return a {@ManifestInfo} for the given project, never null + */ + @NonNull + public static ManifestInfo get(IProject project) { + ManifestInfo finder = null; + try { + finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER); + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + + if (finder == null) { + finder = new ManifestInfo(project); + try { + project.setSessionProperty(MANIFEST_FINDER, finder); + } catch (CoreException e) { + AdtPlugin.log(e, "Can't store ManifestInfo"); + } + } + + return finder; + } + + /** + * Ensure that the package, theme and activity maps are initialized and up to date + * with respect to the manifest file + */ + private void sync() { + // Since each of the accessors call sync(), allow a bunch of immediate + // accessors to all bypass the file stat() below + long now = System.currentTimeMillis(); + if (now - mLastChecked < 50 && mManifestFile != null) { + return; + } + mLastChecked = now; + + if (mManifestFile == null) { + IFolderWrapper projectFolder = new IFolderWrapper(mProject); + mManifestFile = AndroidManifest.getManifest(projectFolder); + if (mManifestFile == null) { + return; + } + } + + // Check to see if our data is up to date + long fileModified = mManifestFile.getModificationStamp(); + if (fileModified == mLastModified) { + // Already have up to date data + return; + } + mLastModified = fileModified; + + mActivityAttributes = new HashMap<String, ActivityAttributes>(); + mManifestTheme = null; + mTargetSdk = 1; // Default when not specified + mMinSdk = 1; // Default when not specified + mMinSdkName = "1"; // Default when not specified + mPackage = ""; //$NON-NLS-1$ + mApplicationIcon = null; + mApplicationLabel = null; + mApplicationSupportsRtl = false; + + Document document = null; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + InputSource is = new InputSource(mManifestFile.getContents()); + + factory.setNamespaceAware(true); + factory.setValidating(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + document = builder.parse(is); + + Element root = document.getDocumentElement(); + mPackage = root.getAttribute(ATTRIBUTE_PACKAGE); + NodeList activities = document.getElementsByTagName(NODE_ACTIVITY); + for (int i = 0, n = activities.getLength(); i < n; i++) { + Element activity = (Element) activities.item(i); + ActivityAttributes info = new ActivityAttributes(activity, mPackage); + mActivityAttributes.put(info.getName(), info); + } + + NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION); + if (applications.getLength() > 0) { + assert applications.getLength() == 1; + Element application = (Element) applications.item(0); + if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) { + mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); + } + if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) { + mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); + } + if (SdkConstants.VALUE_TRUE.equals(application.getAttributeNS(NS_RESOURCES, + ATTRIBUTE_SUPPORTS_RTL))) { + mApplicationSupportsRtl = true; + } + + String defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); + if (defaultTheme != null && !defaultTheme.isEmpty()) { + // From manifest theme documentation: + // "If that attribute is also not set, the default system theme is used." + mManifestTheme = defaultTheme; + } + } + + // Look up target SDK + NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK); + if (usesSdks.getLength() > 0) { + Element usesSdk = (Element) usesSdks.item(0); + mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1); + mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk); + } + + } catch (SAXException e) { + AdtPlugin.log(e, "Malformed manifest"); + } catch (Exception e) { + AdtPlugin.log(e, "Could not read Manifest data"); + } + } + + private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { + String valueString = null; + if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) { + valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute); + if (attribute.equals(ATTRIBUTE_MIN_SDK_VERSION)) { + mMinSdkName = valueString; + } + } + + if (valueString != null) { + int apiLevel = -1; + try { + apiLevel = Integer.valueOf(valueString); + } catch (NumberFormatException e) { + // Handle codename + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + valueString); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + apiLevel = target.getVersion().getApiLevel() + 1; + } + } + } + + return apiLevel; + } + + return defaultApiLevel; + } + + /** + * Returns the default package registered in the Android manifest + * + * @return the default package registered in the manifest + */ + @NonNull + public String getPackage() { + sync(); + return mPackage; + } + + /** + * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}. + * + * @return a map from activity fqcn to ActivityAttributes + */ + @NonNull + public Map<String, ActivityAttributes> getActivityAttributesMap() { + sync(); + return mActivityAttributes; + } + + /** + * Returns the attributes of an activity given its full class name. + */ + @Nullable + public ActivityAttributes getActivityAttributes(String activity) { + return getActivityAttributesMap().get(activity); + } + + /** + * Returns the manifest theme registered on the application, if any + * + * @return a manifest theme, or null if none was registered + */ + @Nullable + public String getManifestTheme() { + sync(); + return mManifestTheme; + } + + /** + * Returns the default theme for this project, by looking at the manifest default + * theme registration, target SDK, rendering target, etc. + * + * @param renderingTarget the rendering target use to render the theme, or null + * @param screenSize the screen size to obtain a default theme for, or null if unknown + * @return the theme to use for this project, never null + */ + @NonNull + public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) { + sync(); + + if (mManifestTheme != null) { + return mManifestTheme; + } + + int renderingTargetSdk = mTargetSdk; + if (renderingTarget != null) { + renderingTargetSdk = renderingTarget.getVersion().getApiLevel(); + } + + int apiLevel = Math.min(mTargetSdk, renderingTargetSdk); + // For now this theme works only on XLARGE screens. When it works for all sizes, + // add that new apiLevel to this check. + if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) { + return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$ + } else { + return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$ + } + } + + /** + * Returns the application icon, or null + * + * @return the application icon, or null + */ + @Nullable + public String getApplicationIcon() { + sync(); + return mApplicationIcon; + } + + /** + * Returns the application label, or null + * + * @return the application label, or null + */ + @Nullable + public String getApplicationLabel() { + sync(); + return mApplicationLabel; + } + + /** + * Returns true if the application has RTL support. + * + * @return true if the application has RTL support. + */ + public boolean isRtlSupported() { + sync(); + return mApplicationSupportsRtl; + } + + /** + * Returns the target SDK version + * + * @return the target SDK version + */ + public int getTargetSdkVersion() { + sync(); + return mTargetSdk; + } + + /** + * Returns the minimum SDK version + * + * @return the minimum SDK version + */ + public int getMinSdkVersion() { + sync(); + return mMinSdk; + } + + /** + * Returns the minimum SDK version name (which may not be a numeric string, e.g. + * it could be a codename). It will never be null or empty; if no min sdk version + * was specified in the manifest, the return value will be "1". Use + * {@link #getMinSdkCodeName()} instead if you want to look up whether there is a code name. + * + * @return the minimum SDK version + */ + @NonNull + public String getMinSdkName() { + sync(); + if (mMinSdkName == null || mMinSdkName.isEmpty()) { + mMinSdkName = "1"; //$NON-NLS-1$ + } + + return mMinSdkName; + } + + /** + * Returns the code name used for the minimum SDK version, if any. + * + * @return the minSdkVersion codename or null + */ + @Nullable + public String getMinSdkCodeName() { + String minSdkName = getMinSdkName(); + if (!Character.isDigit(minSdkName.charAt(0))) { + return minSdkName; + } + + return null; + } + + /** + * Returns the {@link IPackageFragment} for the package registered in the manifest + * + * @return the {@link IPackageFragment} for the package registered in the manifest + */ + @Nullable + public IPackageFragment getPackageFragment() { + sync(); + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); + if (javaProject != null) { + IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); + if (root != null) { + return root.getPackageFragment(mPackage); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** + * Returns the activity associated with the given layout file. Makes an educated guess + * by peeking at the usages of the R.layout.name field corresponding to the layout and + * if it finds a usage. + * + * @param project the project containing the layout + * @param layoutName the layout whose activity we want to look up + * @param pkg the package containing activities + * @return the activity name + */ + @Nullable + public static String guessActivity(IProject project, String layoutName, String pkg) { + List<String> activities = guessActivities(project, layoutName, pkg); + if (activities.size() > 0) { + return activities.get(0); + } else { + return null; + } + } + + /** + * Returns the activities associated with the given layout file. Makes an educated guess + * by peeking at the usages of the R.layout.name field corresponding to the layout and + * if it finds a usage. + * + * @param project the project containing the layout + * @param layoutName the layout whose activity we want to look up + * @param pkg the package containing activities + * @return the activity name + */ + @NonNull + public static List<String> guessActivities(IProject project, String layoutName, String pkg) { + final LinkedList<String> activities = new LinkedList<String>(); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod method = (IMethod) element; + IType declaringType = method.getDeclaringType(); + String fqcn = declaringType.getFullyQualifiedName(); + + if ((declaringType.getSuperclassName() != null && + declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$ + || method.getElementName().equals("onCreate")) { //$NON-NLS-1$ + activities.addFirst(fqcn); + } else { + activities.addLast(fqcn); + } + } + } + }; + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject == null) { + return Collections.emptyList(); + } + // TODO - look around a bit more and see if we can figure out whether the + // call if from within a setContentView call! + + // Search for which java classes call setContentView(R.layout.layoutname); + String typeFqcn = "R.layout"; //$NON-NLS-1$ + if (pkg != null) { + typeFqcn = pkg + '.' + typeFqcn; + } + + IType type = javaProject.findType(typeFqcn); + if (type != null) { + IField field = type.getField(layoutName); + if (field.exists()) { + SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES); + try { + search(requestor, javaProject, pattern); + } catch (OperationCanceledException canceled) { + // pass + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return activities; + } + + /** + * Returns all activities found in the given project (including those in libraries, + * except for android.jar itself) + * + * @param project the project + * @return a list of activity classes as fully qualified class names + */ + @SuppressWarnings("restriction") // BinaryType + @NonNull + public static List<String> getProjectActivities(IProject project) { + final List<String> activities = new ArrayList<String>(); + try { + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + IType[] activityTypes = new IType[0]; + IType activityType = javaProject.findType(CLASS_ACTIVITY); + if (activityType != null) { + ITypeHierarchy hierarchy = + activityType.newTypeHierarchy(javaProject, new NullProgressMonitor()); + activityTypes = hierarchy.getAllSubtypes(activityType); + for (IType type : activityTypes) { + if (type instanceof BinaryType && (type.getClassFile() == null + || type.getClassFile().getResource() == null)) { + continue; + } + activities.add(type.getFullyQualifiedName()); + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return activities; + } + + + /** + * Returns the activity associated with the given layout file. + * <p> + * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas + * guessActivity simply looks for references to "R.layout.foo", this method searches + * for all usages of Activity#setContentView(int), and for each match it looks up the + * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses + * a regexp to pull out "foo" from this, and stores the association that layout "foo" + * is associated with the activity class that contained the setContentView call. + * <p> + * This has two potential advantages: + * <ol> + * <li>It can be faster. We do the reference search -once-, and we've built a map of + * all the layout-to-activity mappings which we can then immediately look up other + * layouts for, which is particularly useful at startup when we have to compute the + * layout activity associations to populate the theme choosers. + * <li>It can be more accurate. Just because an activity references an "R.layout.foo" + * field doesn't mean it's setting it as a content view. + * </ol> + * However, this second advantage is also its chief problem. There are some common + * code constructs which means that the associated layout is not explicitly referenced + * in a direct setContentView call; on a couple of sample projects I tested I found + * patterns like for example "setContentView(v)" where "v" had been computed earlier. + * Therefore, for now we're going to stick with the more general approach of just + * looking up each field when needed. We're keeping the code around, though statically + * compiled out with the "if (false)" construct below in case we revisit this. + * + * @param layoutFile the layout whose activity we want to look up + * @return the activity name + */ + @SuppressWarnings("all") + @Nullable + public String guessActivityBySetContentView(String layoutName) { + if (false) { + // These should be fields + final Pattern LAYOUT_FIELD_PATTERN = + Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$ + Map<String, String> mUsages = null; + + sync(); + if (mUsages == null) { + final Map<String, String> usages = new HashMap<String, String>(); + mUsages = usages; + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod method = (IMethod) element; + IType declaringType = method.getDeclaringType(); + String fqcn = declaringType.getFullyQualifiedName(); + IDocumentProvider provider = new TextFileDocumentProvider(); + IResource resource = match.getResource(); + try { + provider.connect(resource); + IDocument document = provider.getDocument(resource); + if (document != null) { + String matchText = document.get(match.getOffset(), + match.getLength()); + Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText); + if (matcher.find()) { + usages.put(matcher.group(1), fqcn); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", + resource.getName()); + } finally { + provider.disconnect(resource); + } + } + } + }; + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); + if (javaProject == null) { + return null; + } + + // Search for which java classes call setContentView(R.layout.layoutname); + String typeFqcn = "R.layout"; //$NON-NLS-1$ + if (mPackage != null) { + typeFqcn = mPackage + '.' + typeFqcn; + } + + IType activityType = javaProject.findType(CLASS_ACTIVITY); + if (activityType != null) { + IMethod method = activityType.getMethod( + "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$ + if (method.exists()) { + SearchPattern pattern = SearchPattern.createPattern(method, + REFERENCES); + search(requestor, javaProject, pattern); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + return mUsages.get(layoutName); + } + + return null; + } + + /** + * Performs a search using the given pattern, scope and handler. The search will abort + * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds. + */ + private static void search(SearchRequestor requestor, IJavaProject javaProject, + SearchPattern pattern) throws CoreException { + // Find the package fragment specified in the manifest; the activities should + // live there. + IJavaSearchScope scope = createPackageScope(javaProject); + + SearchParticipant[] participants = new SearchParticipant[] { + SearchEngine.getDefaultSearchParticipant() + }; + SearchEngine engine = new SearchEngine(); + + final long searchStart = System.currentTimeMillis(); + NullProgressMonitor monitor = new NullProgressMonitor() { + private boolean mCancelled; + @Override + public void internalWorked(double work) { + long searchEnd = System.currentTimeMillis(); + if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) { + mCancelled = true; + } + } + + @Override + public boolean isCanceled() { + return mCancelled; + } + }; + engine.search(pattern, participants, scope, requestor, monitor); + } + + /** Creates a package search scope for the first package root in the given java project */ + private static IJavaSearchScope createPackageScope(IJavaProject javaProject) { + IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject); + + IJavaSearchScope scope; + if (packageRoot != null) { + IJavaElement[] scopeElements = new IJavaElement[] { packageRoot }; + scope = SearchEngine.createJavaSearchScope(scopeElements); + } else { + scope = SearchEngine.createWorkspaceScope(); + } + return scope; + } + + /** + * Returns the first package root for the given java project + * + * @param javaProject the project to search in + * @return the first package root, or null + */ + @Nullable + public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) { + IPackageFragmentRoot packageRoot = null; + List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject); + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + for (IPath path : sources) { + IResource firstSource = workspace.getRoot().findMember(path); + if (firstSource != null) { + packageRoot = javaProject.getPackageFragmentRoot(firstSource); + if (packageRoot != null) { + break; + } + } + } + return packageRoot; + } + + /** + * Computes the minimum SDK and target SDK versions for the project + * + * @param project the project to look up the versions for + * @return a pair of (minimum SDK, target SDK) versions, never null + */ + @NonNull + public static Pair<Integer, Integer> computeSdkVersions(IProject project) { + int mMinSdkVersion = 1; + int mTargetSdkVersion = 1; + + IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project)); + if (manifestFile != null) { + try { + Object value = AndroidManifest.getMinSdkVersion(manifestFile); + mMinSdkVersion = 1; // Default case if missing + if (value instanceof Integer) { + mMinSdkVersion = ((Integer) value).intValue(); + } else if (value instanceof String) { + // handle codename, only if we can resolve it. + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + value); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + mMinSdkVersion = target.getVersion().getApiLevel() + 1; + } + } + } + + value = AndroidManifest.getTargetSdkVersion(manifestFile); + if (value == null) { + mTargetSdkVersion = mMinSdkVersion; + } else if (value instanceof String) { + // handle codename, only if we can resolve it. + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + value); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + mTargetSdkVersion = target.getVersion().getApiLevel() + 1; + } + } + } + } catch (XPathExpressionException e) { + // do nothing we'll use 1 below. + } catch (StreamException e) { + // do nothing we'll use 1 below. + } + } + + return Pair.of(mMinSdkVersion, mTargetSdkVersion); + } +} |