| /* |
| * 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); |
| } |
| } |