From 9bfb05e9d9ea4b2b969e50c3096e2fdb95653648 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Tue, 6 Mar 2012 14:20:00 -0800 Subject: Handle the android support gridlayout library automatically This changeset adds support for the android support library's GridLayout library project. When you create a new layout with the GridLayout, or when you drop a GridLayout, the IDE checks whether you need the compatibility version of GridLayout (e.g. min sdk < 14), and if so, offers to install it. This will then first run the SDK manager to install the android support package into extras, and then it creates a local library project in the Eclipse workspace, and updates the library dependency to reference it. Finally, it rewrites tags such that the layout will use the compatibility package for the and tags. This is done in the node handler, so client rule code will automatically get the right compatibility tag; they don't need to handle it there. Change-Id: I6da926eee7ffa956832ddd311d4180e8ff38ae07 --- eclipse/dictionary.txt | 1 + .../android/ide/common/layout/LayoutConstants.java | 2 + .../src/com/android/ide/eclipse/adt/AdtUtils.java | 46 +++- .../actions/AddCompatibilityJarAction.java | 245 ++++++++++++++++++++- .../adt/internal/actions/FixProjectAction.java | 16 +- .../build/builders/PreCompilerBuilder.java | 2 +- .../editors/layout/gre/ClientRulesEngine.java | 5 +- .../adt/internal/editors/layout/gre/NodeProxy.java | 12 +- .../internal/editors/manifest/ManifestInfo.java | 99 ++++++--- .../adt/internal/lint/AddSuppressAttribute.java | 17 +- .../project/CompatibilityLibraryHelper.java | 176 +++++++++++++++ .../resources/manager/ProjectClassLoader.java | 91 +++++--- .../android/ide/eclipse/adt/internal/sdk/Sdk.java | 2 +- .../wizards/newxmlfile/NewXmlFileWizard.java | 16 ++ 14 files changed, 643 insertions(+), 87 deletions(-) create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java (limited to 'eclipse') diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index aa985c7..dca9b23 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -199,6 +199,7 @@ preloaded preloads primordial printf +pristine programmatic programmatically proguard diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java index 1b9f815..3129f4d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java @@ -196,6 +196,7 @@ public class LayoutConstants { /** The fully qualified class name of a RelativeLayout view */ public static final String FQCN_GRID_LAYOUT = "android.widget.GridLayout"; //$NON-NLS-1$ + public static final String FQCN_GRID_LAYOUT_V7 = "android.support.v7.widget.GridLayout"; //$NON-NLS-1$ /** The fully qualified class name of a FrameLayout view */ public static final String FQCN_FRAME_LAYOUT = "android.widget.FrameLayout"; //$NON-NLS-1$ @@ -247,6 +248,7 @@ public class LayoutConstants { /** The fully qualified class name of a Space */ public static final String FQCN_SPACE = "android.widget.Space"; //$NON-NLS-1$ + public static final String FQCN_SPACE_V7 = "android.support.v7.widget.Space"; //$NON-NLS-1$ /** The fully qualified class name of a TextView view */ public static final String FQCN_TEXT_VIEW = "android.widget.TextView"; //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java index 62b6804..7e0392a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java @@ -373,8 +373,12 @@ public class AdtUtils { * @param offset the offset to be checked * @return a list (possibly empty but never null) of matching markers */ - public static List findMarkersOnLine(String markerType, - IResource file, IDocument document, int offset) { + @NonNull + public static List findMarkersOnLine( + @NonNull String markerType, + @NonNull IResource file, + @NonNull IDocument document, + int offset) { List matchingMarkers = new ArrayList(2); try { IMarker[] markers = file.findMarkers(markerType, true, IResource.DEPTH_ZERO); @@ -413,6 +417,7 @@ public class AdtUtils { * * @return the available and open Android projects, never null */ + @NonNull public static IJavaProject[] getOpenAndroidProjects() { return BaseProjectHelper.getAndroidProjects(new IProjectFilter() { @Override @@ -423,6 +428,43 @@ public class AdtUtils { } /** + * Returns a unique project name, based on the given {@code base} file name + * possibly with a {@code conjunction} and a new number behind it to ensure + * that the project name is unique. For example, + * {@code getUniqueProjectName("project", "_")} will return + * {@code "project"} if that name does not already exist, and if it does, it + * will return {@code "project_2"}. + * + * @param base the base name to use, such as "foo" + * @param conjunction a string to insert between the base name and the + * number. + * @return a unique project name based on the given base and conjunction + */ + public static String getUniqueProjectName(String base, String conjunction) { + // We're using all workspace projects here rather than just open Android project + // via getOpenAndroidProjects because the name cannot conflict with non-Android + // or closed projects either + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IProject[] projects = workspaceRoot.getProjects(); + + for (int i = 1; i < 1000; i++) { + String name = i == 1 ? base : base + conjunction + Integer.toString(i); + boolean found = false; + for (IProject project : projects) { + if (project.getName().equals(name)) { + found = true; + break; + } + } + if (!found) { + return name; + } + } + + return base; + } + + /** * Returns the name of the parent folder for the given editor input * * @param editorInput the editor input to check diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddCompatibilityJarAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddCompatibilityJarAction.java index b758b67..2428e60 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddCompatibilityJarAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddCompatibilityJarAction.java @@ -17,18 +17,28 @@ package com.android.ide.eclipse.adt.internal.actions; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; import com.android.sdklib.io.FileOp; import com.android.sdkuilib.internal.repository.sdkman2.AdtUpdateDialog; import com.android.util.Pair; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.IFileSystem; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; @@ -117,12 +127,22 @@ public class AddCompatibilityJarAction implements IObjectActionDelegate { AdtPlugin.log(IStatus.ERROR, "JavaProject is null for %1$s", project); //$NON-NLS-1$ } + File jarPath = installSupport(); + if (jarPath != null) { + return addJar(javaProject, jarPath, waitForFinish); + } else { + return false; + } + } + + private static File installSupport() { + final Sdk sdk = Sdk.getCurrent(); if (sdk == null) { AdtPlugin.printErrorToConsole( AddCompatibilityJarAction.class.getSimpleName(), // tag "Error: Android SDK is not loaded yet."); //$NON-NLS-1$ - return false; + return null; } // TODO: For the generic action, check the library isn't in the project already. @@ -138,9 +158,12 @@ public class AddCompatibilityJarAction implements IObjectActionDelegate { Pair result = window.installExtraPackage( "android", "support"); //$NON-NLS-1$ //$NON-NLS-2$ + // TODO: Make sure the version is at the required level; we know we need at least one + // containing the v7 support + if (!result.getFirst().booleanValue()) { AdtPlugin.printErrorToConsole("Failed to install Android Compatibility library"); - return false; + return null; } // TODO these "v4" values needs to be dynamic, e.g. we could try to match @@ -153,11 +176,19 @@ public class AddCompatibilityJarAction implements IObjectActionDelegate { if (!jarPath.isFile()) { AdtPlugin.printErrorToConsole("Android Compatibility JAR not found:", jarPath.getAbsolutePath()); - return false; + return null; } - // Then run an Eclipse asynchronous job to update the project + return jarPath; + } + + private static boolean addJar( + final IJavaProject javaProject, + final File jarPath, + boolean waitForFinish) { + // Run an Eclipse asynchronous job to update the project + final IProject project = javaProject.getProject(); Job job = new Job("Add Compatibility Library to Project") { @Override protected IStatus run(IProgressMonitor monitor) { @@ -208,6 +239,212 @@ public class AddCompatibilityJarAction implements IObjectActionDelegate { return true; } + /** + * Similar to {@link #install}, but rather than copy a jar into the given + * project, it creates a new library project in the workspace for the + * compatibility library, and adds a library dependency on the newly + * installed library from the given project. + * + * @param project the project to add a dependency on the library to + * @param waitForFinish If true, block until the task has finished + * @return true if the installation was successful (or if + * waitForFinish is false, if the installation is + * likely to be successful - e.g. the user has at least agreed to + * all installation prompts.) + */ + public static boolean installLibrary(final IProject project, boolean waitForFinish) { + final IJavaProject javaProject = JavaCore.create(project); + if (javaProject != null) { + + File sdk = new File(Sdk.getCurrent().getSdkLocation()); + File supportPath = new File(sdk, + SdkConstants.FD_EXTRAS + File.separator + + "android" + File.separator //$NON-NLS-1$ + + "support"); //$NON-NLS-1$ + if (!supportPath.isDirectory()) { + File path = installSupport(); + if (path == null) { + return false; + } + assert path.equals(supportPath); + } + File libraryPath = new File(supportPath, + "v7" + File.separator //$NON-NLS-1$ + + "gridlayout"); //$NON-NLS-1$ + if (!libraryPath.isDirectory()) { + // Upgrade support package: it's out of date. The SDK manager will + // perform an upgrade to the latest version if the package is already installed. + File path = installSupport(); + if (path == null) { + return false; + } + assert path.equals(libraryPath) : path; + } + + // Create workspace copy of the project and add library dependency + IProject libraryProject = createLibraryProject(libraryPath, project, waitForFinish); + if (libraryProject != null) { + return addLibraryDependency(libraryProject, project, waitForFinish); + } + } + + return false; + } + + /** + * Creates a library project in the Eclipse workspace out of the grid layout project + * in the SDK tree. + * + * @param libraryPath the path to the directory tree containing the project contents + * @param project the project to copy the SDK target out of + * @param waitForFinish whether the operation should finish before this method returns + * @return a library project, or null if it fails for some reason + */ + private static IProject createLibraryProject( + final File libraryPath, + final IProject project, + boolean waitForFinish) { + + // Install a new library into the workspace. This is a copy rather than + // a reference to the compatibility library version such that modifications + // do not modify the pristine copy in the SDK install area. + + final IProject newProject; + try { + IProgressMonitor monitor = new NullProgressMonitor(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + + String name = AdtUtils.getUniqueProjectName( + "gridlayout_v7", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + newProject = root.getProject(name); + newProject.create(monitor); + + // Copy in the files recursively + IFileSystem fileSystem = EFS.getLocalFileSystem(); + IFileStore sourceDir = fileSystem.getStore(libraryPath.toURI()); + IFileStore destDir = fileSystem.getStore(newProject.getLocationURI()); + sourceDir.copy(destDir, EFS.OVERWRITE, null); + + // Make sure the src folder exists + destDir.getChild("src").mkdir(0, null /*monitor*/); + + // Set the android platform to the same level as the calling project + ProjectState state = Sdk.getProjectState(project); + String target = state.getProperties().getProperty(ProjectProperties.PROPERTY_TARGET); + if (target != null && target.length() > 0) { + ProjectProperties properties = ProjectProperties.load(libraryPath.getPath(), + PropertyType.PROJECT); + ProjectPropertiesWorkingCopy copy = properties.makeWorkingCopy(); + copy.setProperty(ProjectProperties.PROPERTY_TARGET, target); + try { + copy.save(); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + newProject.open(monitor); + + return newProject; + } catch (CoreException e) { + AdtPlugin.log(e, null); + return null; + } + } + + /** + * Adds a library dependency on the given library into the given project. + * + * @param libraryProject the library project to depend on + * @param dependentProject the project to write the dependency into + * @param waitForFinish whether this method should wait for the job to + * finish + * @return true if the operation succeeded + */ + public static boolean addLibraryDependency( + final IProject libraryProject, + final IProject dependentProject, + boolean waitForFinish) { + + // Now add library dependency + + // Run an Eclipse asynchronous job to update the project + Job job = new Job("Add Compatibility Library Dependency to Project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Add library dependency to project build path", 3); + monitor.worked(1); + + // TODO: Add library project to the project.properties file! + ProjectState state = Sdk.getProjectState(dependentProject); + ProjectPropertiesWorkingCopy mPropertiesWorkingCopy = + state.getProperties().makeWorkingCopy(); + + // Get the highest version number of the libraries; there cannot be any + // gaps so we will assign the next library the next number + int nextVersion = 1; + for (String property : mPropertiesWorkingCopy.keySet()) { + if (property.startsWith(ProjectProperties.PROPERTY_LIB_REF)) { + String s = property.substring( + ProjectProperties.PROPERTY_LIB_REF.length()); + int version = Integer.parseInt(s); + if (version >= nextVersion) { + nextVersion = version + 1; + } + } + } + + IPath relativePath = libraryProject.getLocation().makeRelativeTo( + dependentProject.getLocation()); + + mPropertiesWorkingCopy.setProperty( + ProjectProperties.PROPERTY_LIB_REF + nextVersion, + relativePath.toString()); + try { + mPropertiesWorkingCopy.save(); + IResource projectProp = dependentProject.findMember( + SdkConstants.FN_PROJECT_PROPERTIES); + projectProp.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + } catch (Exception e) { + String msg = String.format( + "Failed to save %1$s for project %2$s", + SdkConstants.FN_PROJECT_PROPERTIES, dependentProject.getName()); + AdtPlugin.log(e, msg); + } + + // Project fix-ups + Job fix = FixProjectAction.createFixProjectJob(libraryProject); + fix.schedule(); + fix.join(); + + monitor.worked(1); + + return Status.OK_STATUS; + } catch (Exception e) { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Failed", e); //$NON-NLS-1$ + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }; + job.schedule(); + + if (waitForFinish) { + try { + job.join(); + return job.getState() == IStatus.OK; + } catch (InterruptedException e) { + AdtPlugin.log(e, null); + } + } + + return true; + } + private static IResource copyJarIntoProject( IProject project, File jarPath, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java index c073022..254219f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.actions; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.internal.project.AndroidNature; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; @@ -82,7 +83,18 @@ public class FixProjectAction implements IObjectActionDelegate { } private void fixProject(final IProject project) { - new Job("Fix Project Properties") { + createFixProjectJob(project).schedule(); + } + + /** + * Creates a job to fix the project + * + * @param project the project to fix + * @return a job to perform the fix (not yet scheduled) + */ + @NonNull + public static Job createFixProjectJob(@NonNull final IProject project) { + return new Job("Fix Project Properties") { @Override protected IStatus run(IProgressMonitor monitor) { @@ -129,7 +141,7 @@ public class FixProjectAction implements IObjectActionDelegate { } } } - }.schedule(); + }; } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java index 8234f25..63bc255 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java @@ -588,7 +588,7 @@ public class PreCompilerBuilder extends BaseBuilder { Messages.Removing_Generated_Classes); // remove all the derived resources from the 'gen' source folder. - if (mGenFolder != null) { + if (mGenFolder != null && mGenFolder.exists()) { // gen folder should not be derived, but previous version could set it to derived // so we make sure this isn't the case (or it'll get deleted by the clean) mGenFolder.setDerived(false, monitor); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java index 9b186a1..7b860fe 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -193,7 +193,9 @@ class ClientRulesEngine implements IClientRulesEngine { Sdk currentSdk = Sdk.getCurrent(); if (currentSdk != null) { IAndroidTarget target = currentSdk.getTarget(mRulesEngine.getEditor().getProject()); - return target.getVersion().getApiLevel(); + if (target != null) { + return target.getVersion().getApiLevel(); + } } return -1; @@ -353,6 +355,7 @@ class ClientRulesEngine implements IClientRulesEngine { // First check to make sure fragments are available, and if not, // warn the user. IAndroidTarget target = Sdk.getCurrent().getTarget(project); + // No, this should be using the min SDK instead! if (target.getVersion().getApiLevel() < 11 && oldFragmentType == null) { // Compatibility library must be present MessageDialog dialog = diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index a4306fa..ea464c1 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -37,6 +37,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElement import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.project.CompatibilityLibraryHelper; import org.eclipse.swt.graphics.Rectangle; import org.w3c.dom.NamedNodeMap; @@ -256,6 +257,13 @@ public class NodeProxy implements INode { private INode insertOrAppend(String viewFqcn, int index) { checkEditOK(); + AndroidXmlEditor editor = mNode.getEditor(); + if (editor != null) { + // Possibly replace the tag with a compatibility version if the + // minimum SDK requires it + viewFqcn = CompatibilityLibraryHelper.getTagFor(editor.getProject(), viewFqcn); + } + // Find the descriptor for this FQCN ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn); if (vd == null) { @@ -277,14 +285,12 @@ public class NodeProxy implements INode { } } + // Set default attributes -- but only for new widgets (not when moving or copying) RulesEngine engine = null; - AndroidXmlEditor editor = mNode.getEditor(); LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); if (delegate != null) { engine = delegate.getRulesEngine(); } - - // Set default attributes -- but only for new widgets (not when moving or copying) if (engine == null || engine.getInsertType().isCreate()) { // TODO: This should probably use IViewRule#getDefaultAttributes() at some point DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); 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 index 4ec3801..f749e2b 100644 --- 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 @@ -29,6 +29,8 @@ import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY; import static com.android.sdklib.xml.AndroidManifest.NODE_USES_SDK; import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES; +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; @@ -103,6 +105,7 @@ public class ManifestInfo { private Map mActivityThemes; private IAbstractFile mManifestFile; private long mLastModified; + private int mMinSdk; private int mTargetSdk; private String mApplicationIcon; private String mApplicationLabel; @@ -130,6 +133,7 @@ public class ManifestInfo { * @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 { @@ -174,6 +178,7 @@ public class ManifestInfo { mActivityThemes = new HashMap(); mManifestTheme = null; mTargetSdk = 1; // Default when not specified + mMinSdk = 1; // Default when not specified mPackage = ""; //$NON-NLS-1$ mApplicationIcon = null; mApplicationLabel = null; @@ -227,32 +232,8 @@ public class ManifestInfo { NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK); if (usesSdks.getLength() > 0) { Element usesSdk = (Element) usesSdks.item(0); - String targetSdk = null; - if (usesSdk.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_TARGET_SDK_VERSION)) { - targetSdk = usesSdk.getAttributeNS(NS_RESOURCES, - ATTRIBUTE_TARGET_SDK_VERSION); - } else if (usesSdk.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_MIN_SDK_VERSION)) { - targetSdk = usesSdk.getAttributeNS(NS_RESOURCES, - ATTRIBUTE_MIN_SDK_VERSION); - } - if (targetSdk != null) { - int apiLevel = -1; - try { - apiLevel = Integer.valueOf(targetSdk); - } catch (NumberFormatException e) { - // Handle codename - if (Sdk.getCurrent() != null) { - IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( - "android-" + targetSdk); //$NON-NLS-1$ - if (target != null) { - // codename future API level is current api + 1 - apiLevel = target.getVersion().getApiLevel() + 1; - } - } - } - - mTargetSdk = apiLevel; - } + mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1); + mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk); } } else { mManifestTheme = defaultTheme; @@ -264,11 +245,40 @@ public class ManifestInfo { } } + private static int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { + String valueString = null; + if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) { + valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute); + } + + 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; @@ -280,6 +290,7 @@ public class ManifestInfo { * * @return a map from activity fqcn to theme style */ + @NonNull public Map getActivityThemes() { sync(); return mActivityThemes; @@ -293,6 +304,7 @@ public class ManifestInfo { * @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(); @@ -320,6 +332,7 @@ public class ManifestInfo { * * @return the application icon, or null */ + @Nullable public String getApplicationIcon() { sync(); return mApplicationIcon; @@ -330,16 +343,38 @@ public class ManifestInfo { * * @return the application label, or null */ + @Nullable public String getApplicationLabel() { sync(); return mApplicationLabel; } /** + * 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 {@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 { @@ -367,6 +402,7 @@ public class ManifestInfo { * @param pkg the package containing activities * @return the activity name */ + @Nullable public static String guessActivity(IProject project, String layoutName, String pkg) { final AtomicReference activity = new AtomicReference(); SearchRequestor requestor = new SearchRequestor() { @@ -446,6 +482,7 @@ public class ManifestInfo { * @return the activity name */ @SuppressWarnings("all") + @Nullable public String guessActivityBySetContentView(String layoutName) { if (false) { // These should be fields @@ -568,7 +605,13 @@ public class ManifestInfo { return scope; } - /** Returns the first package root for the given java project */ + /** + * 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 sources = BaseProjectHelper.getSourceClasspaths(javaProject); @@ -589,8 +632,10 @@ public class ManifestInfo { /** * 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 computeSdkVersions(IProject project) { int mMinSdkVersion = 1; int mTargetSdkVersion = 1; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java index fde228b..1137901 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java @@ -28,7 +28,6 @@ import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; -import com.android.tools.lint.checks.DuplicateIdDetector; import org.eclipse.core.resources.IMarker; import org.eclipse.core.runtime.CoreException; @@ -195,21 +194,9 @@ class AddSuppressAttribute implements ICompletionProposal { return null; } - // Some issues cannot find a specific node scope associated with the error - // (for example because it involves cross-file analysis and at the end of - // the project scan when the warnings are computed the DOM model is no longer - // available). Until that's resolved, we need to filter these out such that - // we don't add misleading annotations on individual elements; the fallback - // path is the DOM document itself instead. - if (id.equals(DuplicateIdDetector.CROSS_LAYOUT.getId())) { - node = document.getDocumentElement(); - } - + node = document.getDocumentElement(); if (node == null) { - node = document.getDocumentElement(); - if (node == null) { - return null; - } + return null; } String desc = String.format("Add ignore '%1$s\' to element", id); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java new file mode 100644 index 0000000..8f6de3a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 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.project; + +import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_LAYOUT_V7; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE_V7; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.actions.AddCompatibilityJarAction; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; + +/** + * Helper class for the Android Support Library. The support library provides + * (for example) a backport of GridLayout, which must be used as a library + * project rather than a jar library since it has resources. This class provides + * support for finding the library project, or downloading and installing it on + * demand if it does not, as well as translating tags such as + * {@code } into {@code } if it + * does not. + */ +public class CompatibilityLibraryHelper { + /** + * Returns the correct tag to use for the given view tag. This is normally + * the same as the tag itself. However, for some views which are not available + * on all platforms, this will: + *
    + *
  • Check if the view is available in the compatibility library, + * and if so, if the support library is not installed, will offer to + * install it via the SDK manager. + *
  • (The tool may also offer to adjust the minimum SDK of the project + * up to a level such that the given tag is supported directly, and then + * this method will return the original tag.) + *
  • Check whether the compatibility library is included in the project, and + * if not, offer to copy it into the workspace and add a library dependency. + *
  • Return the alternative tag. For example, for "GridLayout", it will + * (if the minimum SDK is less than 14) return "com.android.support.v7.GridLayout" + * instead. + *
+ * + * @param project the project to add the dependency into + * @param tag the tag to look up, such as "GridLayout" + * @return the tag to use in the layout, normally the same as the input tag but possibly + * an equivalent compatibility library tag instead. + */ + @NonNull + public static String getTagFor(@NonNull IProject project, @NonNull String tag) { + boolean isGridLayout = tag.equals(FQCN_GRID_LAYOUT); + boolean isSpace = tag.equals(FQCN_SPACE); + if (isGridLayout || isSpace) { + int minSdk = ManifestInfo.get(project).getMinSdkVersion(); + if (minSdk < 14) { + // See if the support library is installed in the SDK area + // See if there is a local project in the workspace providing the + // project + IProject supportProject = getSupportProjectV7(); + if (supportProject != null) { + // Make sure I have a dependency on it + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + for (LibraryState library : state.getLibraries()) { + if (supportProject.equals(library.getProjectState().getProject())) { + // Found it: you have the compatibility library and have linked + // to it: use the alternative tag + return isGridLayout ? FQCN_GRID_LAYOUT_V7 : FQCN_SPACE_V7; + } + } + } + } + + // Ask user to install it + String message = String.format( + "%1$s requires API level 14 or higher, or a compatibility " + + "library for older versions.\n\n" + + " Do you want to install the compatibility library?", tag); + MessageDialog dialog = + new MessageDialog( + Display.getCurrent().getActiveShell(), + "Warning", + null, + message, + MessageDialog.QUESTION, + new String[] { + "Install", "Cancel" + }, + 1 /* default button: Cancel */); + int answer = dialog.open(); + if (answer == 0) { + if (supportProject != null) { + // Just add library dependency + if (!AddCompatibilityJarAction.addLibraryDependency( + supportProject, + project, + true /* waitForFinish */)) { + return tag; + } + } else { + // Install library AND add dependency + if (!AddCompatibilityJarAction.installLibrary( + project, + true /* waitForFinish */)) { + return tag; + } + } + + return isGridLayout ? FQCN_GRID_LAYOUT_V7 : FQCN_SPACE_V7; + } + } + } + + return tag; + } + + /** Cache for {@link #getSupportProjectV7()} */ + private static IProject sCachedProject; + + /** + * Finds and returns the support project in the workspace, if any. + * + * @return the android support library project, or null if not found + */ + @Nullable + public static IProject getSupportProjectV7() { + if (sCachedProject != null) { + if (sCachedProject.isAccessible()) { + return sCachedProject; + } else { + sCachedProject = null; + } + } + + sCachedProject = findSupportProjectV7(); + return sCachedProject; + } + + @Nullable + private static IProject findSupportProjectV7() { + for (IJavaProject javaProject : AdtUtils.getOpenAndroidProjects()) { + IProject project = javaProject.getProject(); + ProjectState state = Sdk.getProjectState(project); + if (state.isLibrary()) { + ManifestInfo manifestInfo = ManifestInfo.get(project); + if (manifestInfo.getPackage().equals("android.support.v7.gridlayout")) { //$NON-NLS-1$ + return project; + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java index eb0ddf1..e118ff7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.resources.manager; import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.build.BuildHelper; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; @@ -27,9 +28,11 @@ import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathContainer; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; import java.io.File; import java.io.FileInputStream; @@ -253,8 +256,6 @@ public final class ProjectClassLoader extends ClassLoader { // get a java project from it IJavaProject javaProject = JavaCore.create(mJavaProject.getProject()); - IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); - ArrayList oslibraryList = new ArrayList(); IClasspathEntry[] classpaths = javaProject.readRawClasspath(); if (classpaths != null) { @@ -266,38 +267,30 @@ public final class ProjectClassLoader extends ClassLoader { e = JavaCore.getResolvedClasspathEntry(e); } - // get the IPath - IPath path = e.getPath(); - - // check the name ends with .jar - if (AdtConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { - boolean local = false; - IResource resource = wsRoot.findMember(path); - if (resource != null && resource.exists() && - resource.getType() == IResource.FILE) { - local = true; - try { - oslibraryList.add(new File(resource.getLocation().toOSString()) - .toURI().toURL()); - } catch (MalformedURLException mue) { - // pass - } - } - - if (local == false) { - // if the jar path doesn't match a workspace resource, - // then we get an OSString and check if this links to a valid file. - String osFullPath = path.toOSString(); - - File f = new File(osFullPath); - if (f.exists()) { - try { - oslibraryList.add(f.toURI().toURL()); - } catch (MalformedURLException mue) { - // pass + handleClassPathEntry(e, oslibraryList); + } else if (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + // get the container. + try { + IClasspathContainer container = JavaCore.getClasspathContainer( + e.getPath(), javaProject); + // ignore the system and default_system types as they represent + // libraries that are part of the runtime. + if (container != null && + container.getKind() == IClasspathContainer.K_APPLICATION) { + IClasspathEntry[] entries = container.getClasspathEntries(); + for (IClasspathEntry entry : entries) { + // TODO: Xav -- is this necessary? + if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + entry = JavaCore.getResolvedClasspathEntry(entry); } + + handleClassPathEntry(entry, oslibraryList); } } + } catch (JavaModelException jme) { + // can't resolve the container? ignore it. + AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", + e.getPath()); } } } @@ -305,4 +298,40 @@ public final class ProjectClassLoader extends ClassLoader { return oslibraryList.toArray(new URL[oslibraryList.size()]); } + + private void handleClassPathEntry(IClasspathEntry e, ArrayList oslibraryList) { + // get the IPath + IPath path = e.getPath(); + + // check the name ends with .jar + if (AdtConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + boolean local = false; + IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path); + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + local = true; + try { + oslibraryList.add(new File(resource.getLocation().toOSString()) + .toURI().toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + + if (local == false) { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.exists()) { + try { + oslibraryList.add(f.toURI().toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + } + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java index 63f381f..6388644 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java @@ -332,7 +332,7 @@ public final class Sdk { /** * Initializes a new project with a target. This creates the project.properties * file. - * @param project the project to intialize + * @param project the project to initialize * @param target the project's target. * @throws IOException if creating the file failed in any way. * @throws StreamException diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java index 4e17125..815848e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java @@ -18,6 +18,9 @@ package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; +import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; + import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; @@ -26,7 +29,9 @@ import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.CompatibilityLibraryHelper; import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo; import com.android.resources.ResourceFolderType; import com.android.util.Pair; @@ -201,6 +206,17 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { StringBuilder sb = new StringBuilder(XML_HEADER_LINE); + if (folderType == ResourceFolderType.LAYOUT && root.equals(GRID_LAYOUT)) { + IProject project = file.getParent().getProject(); + int minSdk = ManifestInfo.get(project).getMinSdkVersion(); + if (minSdk < 14) { + root = CompatibilityLibraryHelper.getTagFor(project, FQCN_GRID_LAYOUT); + if (root.equals(FQCN_GRID_LAYOUT)) { + root = GRID_LAYOUT; + } + } + } + sb.append('<').append(root); if (xmlns != null) { sb.append('\n').append(" xmlns:android=\"").append(xmlns).append('"'); //$NON-NLS-1$ -- cgit v1.1