diff options
author | Tor Norbye <tnorbye@google.com> | 2012-03-06 14:20:00 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-03-08 07:10:05 -0800 |
commit | 9bfb05e9d9ea4b2b969e50c3096e2fdb95653648 (patch) | |
tree | b74b3aa318789abfa19441131a53134fee8ed960 /eclipse/plugins | |
parent | ce07f83d079dae516b8f3aca9220d6efd782d381 (diff) | |
download | sdk-9bfb05e9d9ea4b2b969e50c3096e2fdb95653648.zip sdk-9bfb05e9d9ea4b2b969e50c3096e2fdb95653648.tar.gz sdk-9bfb05e9d9ea4b2b969e50c3096e2fdb95653648.tar.bz2 |
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 <GridLayout> and <Space> 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
Diffstat (limited to 'eclipse/plugins')
13 files changed, 642 insertions, 87 deletions
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<IMarker> findMarkersOnLine(String markerType, - IResource file, IDocument document, int offset) { + @NonNull + public static List<IMarker> findMarkersOnLine( + @NonNull String markerType, + @NonNull IResource file, + @NonNull IDocument document, + int offset) { List<IMarker> matchingMarkers = new ArrayList<IMarker>(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<Boolean, File> 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 + * <code>waitForFinish</code> 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<String, String> 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<String, String>(); 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<String, String> 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<String> activity = new AtomicReference<String>(); 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<IPath> 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<Integer, Integer> 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 <GridLayout>} into {@code <com.android.support.v7.GridLayout>} 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: + * <ul> + * <li> 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. + * <li> (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.) + * <li> 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. + * <li> 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. + * </ul> + * + * @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<URL> oslibraryList = new ArrayList<URL>(); 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<URL> 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 <code>project.properties</code> * 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$ |