aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-09-02 10:40:57 -0700
committerTor Norbye <tnorbye@google.com>2011-09-02 12:25:51 -0700
commit6a41615061dab508e87cbb18f005c5f7edb79dbd (patch)
tree86f1ae41cb5868491589fe2bae77516872527973
parent0c96c744e157c450f7d66bf62f9d569b9514e747 (diff)
downloadsdk-6a41615061dab508e87cbb18f005c5f7edb79dbd.zip
sdk-6a41615061dab508e87cbb18f005c5f7edb79dbd.tar.gz
sdk-6a41615061dab508e87cbb18f005c5f7edb79dbd.tar.bz2
Clean up layout and menu file scanning code
This changeset fixes some issues around the new lazy scanning of layout and menu files. First, it partly fixes "19657: AAPT errors aren't shown when adding an error to a valid XML file". With the new optimization of not running aapt on layout files where no ids have changed, we would no longer pick up changes where an invalid or nonexistent resource is added. We now perform some basic validation of resources as well as XML parsing errors. Second, it fixes a bug in the id before and after comparison used to determine if aapt needs to run: The code would call map.keySet() before and after the ids were added, but this resolved to the same keyset so the equals comparison was always true regardless of the content. Third, it fixes an infinite loop issue with library projects, and avoids doing unnecessary classpath modifications when there are no changed projects. Finally, it changes the "needsId" flag. The state of whether aapt needs to be run was stored per repository, and there is a bug where it does not get cleared properly which can yield a compilation loop. This changeset introduces a new "ScanningContext" object which is passed down to the various resource file updater methods. This context object now holds the needsId state object (which is renamed to "needsFullAapt"), and it is also the object where errors can be registered. Change-Id: I5632612c2d93e2f10f0803e9223921adb67602be
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java9
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java166
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java255
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java27
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java7
-rw-r--r--ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java57
-rw-r--r--ide_common/src/com/android/ide/common/resources/IdResourceParser.java139
-rw-r--r--ide_common/src/com/android/ide/common/resources/MultiResourceFile.java16
-rw-r--r--ide_common/src/com/android/ide/common/resources/ResourceFile.java6
-rw-r--r--ide_common/src/com/android/ide/common/resources/ResourceFolder.java25
-rw-r--r--ide_common/src/com/android/ide/common/resources/ResourceRepository.java88
-rw-r--r--ide_common/src/com/android/ide/common/resources/ScanningContext.java92
-rw-r--r--ide_common/src/com/android/ide/common/resources/SingleResourceFile.java10
19 files changed, 730 insertions, 242 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java
index 933dc40..47215cc 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java
@@ -23,10 +23,10 @@ import com.android.ide.eclipse.adt.internal.build.AaptExecException;
import com.android.ide.eclipse.adt.internal.build.AaptParser;
import com.android.ide.eclipse.adt.internal.build.AaptResultException;
import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker;
import com.android.ide.eclipse.adt.internal.build.DexException;
import com.android.ide.eclipse.adt.internal.build.Messages;
import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
-import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
@@ -377,7 +377,7 @@ public class PostCompilerBuilder extends BaseBuilder {
}
// store the build status in the persistent storage
- saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex);
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
saveProjectBooleanProperty(PROPERTY_UPDATE_CRUNCH_CACHE, mUpdateCrunchCache);
saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
@@ -415,6 +415,7 @@ public class PostCompilerBuilder extends BaseBuilder {
writeLibraryPackage(jarIFile, project, javaOutputFolder,
referencedJavaProjects);
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex = false);
}
return allRefProjects;
@@ -859,7 +860,7 @@ public class PostCompilerBuilder extends BaseBuilder {
}
}
- saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex);
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
} catch (Exception e) {
AdtPlugin.log(e, "Failed to write jar file %s", jarIFile.getLocation().toOSString());
} finally {
@@ -892,7 +893,7 @@ public class PostCompilerBuilder extends BaseBuilder {
// load the build status. We pass true as the default value to
// force a recompile in case the property was not found
- mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true);
+ mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, true);
mUpdateCrunchCache = loadProjectBooleanProperty(PROPERTY_UPDATE_CRUNCH_CACHE, true);
mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
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 4e7c8f7..195a279 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
@@ -30,6 +30,8 @@ import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener;
+import com.android.ide.eclipse.adt.internal.resources.manager.IdeScanningContext;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
@@ -255,16 +257,40 @@ public class PreCompilerBuilder extends BaseBuilder {
} else {
dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList, mProcessors);
delta.accept(dv);
- // Notify the ResourceManager:
- ResourceManager resManager = ResourceManager.getInstance();
- resManager.processDelta(delta);
- // Check whether this project or its dependencies (libraries) have
- // resources that need compilation
- mMustCompileResources |= resManager.projectNeedsIdGeneration(project);
// Check to see if Manifest.xml, Manifest.java, or R.java have changed:
mMustCompileResources |= dv.getCompileResources();
+ // Notify the ResourceManager:
+ ResourceManager resManager = ResourceManager.getInstance();
+ ProjectResources projectResources = resManager.getProjectResources(project);
+
+ if (ResourceManager.isAutoBuilding()) {
+ IdeScanningContext context = new IdeScanningContext(projectResources, project);
+
+ resManager.processDelta(delta, context);
+
+ // Check whether this project or its dependencies (libraries) have
+ // resources that need compilation
+ if (context.needsFullAapt()) {
+ mMustCompileResources = true;
+
+ assert context.getAaptRequestedProjects() != null &&
+ context.getAaptRequestedProjects().size() == 1 &&
+ context.getAaptRequestedProjects().iterator().next() == project;
+
+ // Must also call markAaptRequested on the project to not just
+ // store "aapt required" on this project, but also on any projects
+ // depending on this project if it's a library project
+ ResourceManager.markAaptRequested(project);
+ }
+
+ // Update error markers in the source editor
+ if (!mMustCompileResources) {
+ context.updateMarkers(false /* async */);
+ }
+ } // else: already processed the deltas in ResourceManager's IRawDeltaListener
+
for (SourceProcessor processor : mProcessors) {
processor.doneVisiting(project);
}
@@ -275,8 +301,12 @@ public class PreCompilerBuilder extends BaseBuilder {
}
}
+ // Has anyone marked this project as needing aapt? Typically done when
+ // one of the library projects this project depends on has changed
+ mMustCompileResources |= ResourceManager.isAaptRequested(project);
+
// store the build status in the persistent storage
- saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
// if there was some XML errors, we just return w/o doing
// anything since we've put some markers in the files anyway.
@@ -456,7 +486,7 @@ public class PreCompilerBuilder extends BaseBuilder {
processor.prepareFullBuild(project);
}
- saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
}
// run the source processors
@@ -474,7 +504,7 @@ public class PreCompilerBuilder extends BaseBuilder {
if ((processorStatus & SourceProcessor.COMPILE_STATUS_RES) != 0) {
mMustCompileResources = true;
// save the current state before attempting the compilation
- saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
}
// handle the resources, after the processors are run since some (renderscript)
@@ -660,6 +690,7 @@ public class PreCompilerBuilder extends BaseBuilder {
String osResPath, String osManifestPath, IFolder packageFolder,
ArrayList<IFolder> libResFolders, String libraryPackages, boolean isLibrary)
throws AbortBuildException {
+
// We actually need to delete the manifest.java as it may become empty and
// in this case aapt doesn't generate an empty one, but instead doesn't
// touch it.
@@ -793,6 +824,7 @@ public class PreCompilerBuilder extends BaseBuilder {
// run it again, unless there's a new resource change.
saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES,
mMustCompileResources = false);
+ ResourceManager.clearAaptRequest(project);
}
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
index 3f5a318..9b81090 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
@@ -696,7 +696,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
// inside the quotes.
// Special case for attributes: insert ="" stuff and locate caret inside ""
proposals.add(new CompletionProposal(
- keyword + suffix , // String replacementString
+ keyword + suffix, // String replacementString
offset - wordPrefix.length(), // int replacementOffset
wordPrefix.length() + replaceLength,// int replacementLength
cursorPosition, // cursorPosition
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
index fc3d7d7..b4b4433 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
@@ -303,7 +303,7 @@ public class RenderService {
// Find the layout file.
ResourceValue contextLayout = mResourceResolver.findResValue(
- LAYOUT_PREFIX + contextLayoutName , false /* forceFrameworkOnly*/);
+ LAYOUT_PREFIX + contextLayoutName, false /* forceFrameworkOnly*/);
if (contextLayout != null) {
File layoutFile = new File(contextLayout.getValue());
if (layoutFile.isFile()) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java
index 92485cd..ec68323 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java
@@ -1493,7 +1493,12 @@ public class Hyperlinks {
IRegion region = null;
String extension = file.getFileExtension();
if (mType != null && mName != null && EXT_XML.equals(extension)) {
- Pair<IFile, IRegion> target = findValueInXml(mType, mName, file);
+ Pair<IFile, IRegion> target;
+ if (mType == ResourceType.ID) {
+ target = findIdInXml(mName, file);
+ } else {
+ target = findValueInXml(mType, mName, file);
+ }
if (target != null) {
region = target.getSecond();
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java
index 6e9881f..f92f431 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java
@@ -132,7 +132,6 @@ public final class GlobalProjectMonitor {
/**
* Interface for a listener that gets passed the raw delta without processing.
- *
*/
public interface IRawDeltaListener {
public void visitDelta(IResourceDelta delta);
@@ -195,10 +194,6 @@ public final class GlobalProjectMonitor {
private final class DeltaVisitor implements IResourceDeltaVisitor {
public boolean visit(IResourceDelta delta) {
- // notify the raw delta listeners
- for (IRawDeltaListener listener : mRawDeltaListeners) {
- listener.visitDelta(delta);
- }
// Find the other resource listeners to notify
IResource r = delta.getResource();
int type = r.getType();
@@ -480,6 +475,11 @@ public final class GlobalProjectMonitor {
// this a regular resource change. We get the delta and go through it with a visitor.
IResourceDelta delta = event.getDelta();
+ // notify the raw delta listeners
+ for (IRawDeltaListener listener : mRawDeltaListeners) {
+ listener.visitDelta(delta);
+ }
+
DeltaVisitor visitor = new DeltaVisitor();
try {
delta.accept(visitor);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java
new file mode 100644
index 0000000..fb8caf2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.resources.manager;
+
+import static com.android.ide.eclipse.adt.AdtConstants.MARKER_AAPT_COMPILE;
+import static org.eclipse.core.resources.IResource.DEPTH_ONE;
+import static org.eclipse.core.resources.IResource.DEPTH_ZERO;
+
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ScanningContext;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.AaptParser;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link IdeScanningContext} is a specialized {@link ScanningContext} which
+ * carries extra information about the scanning state, such as which file is
+ * currently being scanned, and which files have been scanned in the past, such
+ * that at the end of a scan we can mark and clear errors, etc.
+ */
+public class IdeScanningContext extends ScanningContext {
+ private final IProject mProject;
+ private final List<IResource> mScannedResources = new ArrayList<IResource>();
+ private IResource mCurrentFile;
+ private List<Pair<IResource, String>> mErrors;
+ private Set<IProject> mFullAaptProjects;
+
+ /**
+ * Constructs a new {@link IdeScanningContext}
+ *
+ * @param repository the associated {@link ResourceRepository}
+ * @param project the associated project
+ */
+ public IdeScanningContext(ResourceRepository repository, IProject project) {
+ super(repository);
+ mProject = project;
+ }
+
+ @Override
+ public void addError(String error) {
+ super.addError(error);
+
+ if (mErrors == null) {
+ mErrors = new ArrayList<Pair<IResource,String>>();
+ }
+ mErrors.add(Pair.of(mCurrentFile, error));
+ }
+
+ /**
+ * Notifies the context that the given resource is about to be scanned.
+ *
+ * @param resource the resource about to be scanned
+ */
+ public void startScanning(IResource resource) {
+ assert mCurrentFile == null : mCurrentFile;
+ mCurrentFile = resource;
+ mScannedResources.add(resource);
+ }
+
+ /**
+ * Notifies the context that the given resource has been scanned.
+ *
+ * @param resource the resource that was scanned
+ */
+ public void finishScanning(IResource resource) {
+ assert mCurrentFile != null;
+ mCurrentFile = null;
+ }
+
+ /**
+ * Process any errors found to add error markers in the affected files (and
+ * also clear up any aapt errors in files that are no longer applicable)
+ *
+ * @param async if true, delay updating markers until the next display
+ * thread event loop update
+ */
+ public void updateMarkers(boolean async) {
+ // Run asynchronously? This is necessary for example when adding markers
+ // as the result of a resource change notification, since at that point the
+ // resource tree is locked for modifications and attempting to create a
+ // marker will throw a org.eclipse.core.internal.resources.ResourceException.
+ if (async) {
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ updateMarkers(false);
+ }
+ });
+ return;
+ }
+
+ // First clear out old/previous markers
+ for (IResource resource :mScannedResources) {
+ try {
+ if (resource.exists()) {
+ int depth = resource instanceof IFolder ? DEPTH_ONE : DEPTH_ZERO;
+ resource.deleteMarkers(MARKER_AAPT_COMPILE, true, depth);
+ }
+ } catch (CoreException ce) {
+ // Pass
+ }
+ }
+
+ // Add new errors
+ if (mErrors != null && mErrors.size() > 0) {
+ List<String> errors = new ArrayList<String>();
+ for (Pair<IResource, String> pair : mErrors) {
+ errors.add(pair.getSecond());
+ }
+ AaptParser.parseOutput(errors, mProject);
+ }
+ }
+
+ @Override
+ public boolean needsFullAapt() {
+ return super.needsFullAapt();
+ }
+
+ @Override
+ protected void requestFullAapt() {
+ super.requestFullAapt();
+
+ if (mCurrentFile != null) {
+ if (mFullAaptProjects == null) {
+ mFullAaptProjects = new HashSet<IProject>();
+ }
+ mFullAaptProjects.add(mCurrentFile.getProject());
+ } else {
+ assert false : "No current context to apply IdeScanningContext to";
+ }
+ }
+
+ /**
+ * Returns the collection of projects that scanned resources have requested
+ * a full aapt for.
+ *
+ * @return a collection of projects that scanned resources requested full
+ * aapt runs for, or null
+ */
+ public Collection<IProject> getAaptRequestedProjects() {
+ return mFullAaptProjects;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
index 6967e67..8e3fe24 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
@@ -287,9 +287,5 @@ public class ProjectResources extends ResourceRepository {
mResourceValueMap = resourceValueMap;
mResIdValueToNameMap = resIdValueToNameMap;
mStyleableValueToNameMap = styleableValueMap;
-
- // Our resource IDs should now be in sync
- setIdsRefreshed();
}
-
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java
index edf55cd..d689a10 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java
@@ -20,12 +20,12 @@ import com.android.ide.common.resources.FrameworkResources;
import com.android.ide.common.resources.ResourceFile;
import com.android.ide.common.resources.ResourceFolder;
import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ScanningContext;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IRawDeltaListener;
-import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.io.IFileWrapper;
@@ -46,11 +46,14 @@ import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
-import java.util.List;
+import java.util.Map;
/**
* The ResourceManager tracks resources for all opened projects.
@@ -79,7 +82,7 @@ public final class ResourceManager {
* <p/><b>All accesses must be inside a synchronized(mMap) block</b>, and do as a little as
* possible and <b>not call out to other classes</b>.
*/
- private final HashMap<IProject, ProjectResources> mMap =
+ private final Map<IProject, ProjectResources> mMap =
new HashMap<IProject, ProjectResources>();
/**
@@ -112,7 +115,6 @@ public final class ResourceManager {
* @param monitor The global project monitor
*/
public static void setup(GlobalProjectMonitor monitor) {
- monitor.addResourceEventListener(sThis.mResourceEventListener);
monitor.addProjectListener(sThis.mProjectListener);
monitor.addRawDeltaListener(sThis.mRawDeltaListener);
@@ -159,24 +161,23 @@ public final class ResourceManager {
/**
* Update the resource repository with a delta
+ *
* @param delta the resource changed delta to process.
+ * @param context a context object with state for the current update, such
+ * as a place to stash errors encountered
*/
- public void processDelta(IResourceDelta delta) {
+ public void processDelta(IResourceDelta delta, IdeScanningContext context) {
// Skip over deltas that don't fit our mask
int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED;
int kind = delta.getKind();
if ( (mask & kind) == 0) {
return;
}
- // If our delta was handed to us from the PreCompiler then it's a single delta
- // with lots of children. GlobalProjectMonitor will hand us a delta for each
- // item in the tree of modifications so we only need to recurse into delta
- // children if we're autobuilding.
- if (ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding()) {
- IResourceDelta[] children = delta.getAffectedChildren();
- for (IResourceDelta child : children) {
- processDelta(child);
- }
+
+ // Process children recursively
+ IResourceDelta[] children = delta.getAffectedChildren();
+ for (IResourceDelta child : children) {
+ processDelta(child, context);
}
// Process this delta
@@ -184,55 +185,21 @@ public final class ResourceManager {
int type = r.getType();
if (type == IResource.FILE) {
- updateFile((IFile)r, delta.getMarkerDeltas(), kind);
+ context.startScanning(r);
+ updateFile((IFile)r, delta.getMarkerDeltas(), kind, context);
+ context.finishScanning(r);
} else if (type == IResource.FOLDER) {
- updateFolder((IFolder)r, kind);
+ updateFolder((IFolder)r, kind, context);
} // We only care about files and folders.
// Project deltas are handled by our project listener
}
/**
- * Private implementation of a resource event listener that registers with
- * GlobalProjectMonitor.
- *
- */
- private class ResourceEventListener implements IResourceEventListener {
- private final List<IProject> mChangedProjects = new ArrayList<IProject>();
-
- public void resourceChangeEventEnd() {
- for (IProject project : mChangedProjects) {
- ProjectResources resources;
- synchronized (mMap) {
- resources = mMap.get(project);
- }
- }
-
- mChangedProjects.clear();
- }
-
- public void resourceChangeEventStart() {
- // pass
- }
-
- void addProject(IProject project) {
- if (mChangedProjects.contains(project) == false) {
- mChangedProjects.add(project);
- }
- }
- }
-
- /**
- * Delegate listener for resource changes. This is called before and after any calls to the
- * project and file listeners (for a given resource change event).
- */
- private final ResourceEventListener mResourceEventListener = new ResourceEventListener();
-
- /**
* Update a resource folder that we know about
* @param folder the folder that was updated
* @param kind the delta type (added/removed/updated)
*/
- private void updateFolder(IFolder folder, int kind) {
+ private void updateFolder(IFolder folder, int kind, IdeScanningContext context) {
ProjectResources resources;
final IProject project = folder.getProject();
@@ -246,8 +213,6 @@ public final class ResourceManager {
return;
}
- mResourceEventListener.addProject(project);
-
switch (kind) {
case IResourceDelta.ADDED:
// checks if the folder is under res.
@@ -296,8 +261,10 @@ public final class ResourceManager {
ResourceFolderType type = ResourceFolderType.getFolderType(
folder.getName());
+ context.startScanning(folder);
ResourceFolder removedFolder = resources.removeFolder(type,
- new IFolderWrapper(folder));
+ new IFolderWrapper(folder), context);
+ context.finishScanning(folder);
if (removedFolder != null) {
notifyListenerOnFolderChange(project, removedFolder, kind);
}
@@ -307,17 +274,19 @@ public final class ResourceManager {
}
/**
- * Called when a delta indicates that a file has changed.
- * Depending on the file being changed, and the type of change
- * (ADDED, REMOVED, CHANGED), the file change is processed to update the resource
- * manager data.
+ * Called when a delta indicates that a file has changed. Depending on the
+ * file being changed, and the type of change (ADDED, REMOVED, CHANGED), the
+ * file change is processed to update the resource manager data.
*
* @param file The file that changed.
* @param markerDeltas The marker deltas for the file.
* @param kind The change kind. This is equivalent to
- * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ * @param context a context object with state for the current update, such
+ * as a place to stash errors encountered
*/
- private void updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ private void updateFile(IFile file, IMarkerDelta[] markerDeltas, int kind,
+ ScanningContext context) {
final IProject project = file.getProject();
try {
@@ -356,7 +325,7 @@ public final class ResourceManager {
if (folder != null) {
ResourceFile resFile = folder.processFile(
new IFileWrapper(file),
- ResourceHelper.getResourceDeltaKind(kind));
+ ResourceHelper.getResourceDeltaKind(kind), context);
notifyListenerOnFileChange(project, resFile, kind);
}
}
@@ -400,12 +369,38 @@ public final class ResourceManager {
* accessed through the {@link ResourceManager#visitDelta(IResourceDelta delta)} method.
*/
private final IRawDeltaListener mRawDeltaListener = new IRawDeltaListener() {
- public void visitDelta(IResourceDelta delta) {
- // If we're autobuilding, then PreCompilerBuilder will pass us deltas
- if (ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding()) {
+ public void visitDelta(IResourceDelta workspaceDelta) {
+ // If we're auto-building, then PreCompilerBuilder will pass us deltas and
+ // they will be processed as part of the build.
+ if (isAutoBuilding()) {
return;
}
- processDelta(delta);
+
+ // When *not* auto building, we need to process the deltas immediately on save,
+ // even if the user is not building yet, such that for example resource ids
+ // are updated in the resource repositories so rendering etc. can work for
+ // those new ids.
+
+ IResourceDelta[] projectDeltas = workspaceDelta.getAffectedChildren();
+ for (IResourceDelta delta : projectDeltas) {
+ if (delta.getResource() instanceof IProject) {
+ IProject project = (IProject) delta.getResource();
+ IdeScanningContext context =
+ new IdeScanningContext(getProjectResources(project), project);
+
+ processDelta(delta, context);
+
+ Collection<IProject> projects = context.getAaptRequestedProjects();
+ if (projects != null) {
+ for (IProject p : projects) {
+ markAaptRequested(p);
+ }
+ }
+ } else {
+ AdtPlugin.log(IStatus.WARNING, "Unexpected delta type: %1$s",
+ delta.getResource().toString());
+ }
+ }
}
};
@@ -466,50 +461,6 @@ public final class ResourceManager {
}
/**
- * Checks the ResourceRepositories associated with the given project and its dependencies
- * and returns whether or not a resource regeneration is needed for that project
- * @param project the project to check
- * @return true if the project or any of its dependencies says it has new or deleted resources
- */
- public boolean projectNeedsIdGeneration(IProject project) {
- // Get a list of repositories to check through
- List<ProjectResources> repositories = getAllProjectResourcesAssociatedWith(project);
- for (ProjectResources repository : repositories) {
- if (repository.needsIdRefresh()) {
- return true;
- }
- }
- // If we've gotten to here, all repositories are in sync, return false
- return false;
- }
-
- /**
- * Get all the resource repositories representing this project and any included libraries
- * @param project the project to get along with its dependencies
- * @return a list of all ProjectResources ordered lowest to highest priority that need to be
- * included in this project.
- */
- private List<ProjectResources> getAllProjectResourcesAssociatedWith(IProject project) {
- List<ProjectResources> toRet = new ArrayList<ProjectResources>();
- // if the project contains libraries, we need to add the libraries resources here
- if (project != null) {
- ProjectState state = Sdk.getProjectState(project);
- if (state != null) {
- List<IProject> libraries = state.getFullLibraryProjects();
- for (IProject library : libraries) {
- ProjectResources libRes = mMap.get(library);
- if (libRes != null) {
- toRet.add(libRes);
- }
- }
- }
- }
- // Add the queried current project last
- toRet.add(mMap.get(project));
- return toRet;
- }
-
- /**
* Initial project parsing to gather resource info.
* @param project
*/
@@ -534,6 +485,7 @@ public final class ResourceManager {
mMap.put(project, projectResources);
}
}
+ IdeScanningContext context = new IdeScanningContext(projectResources, project);
if (resourceFolder != null && resourceFolder.exists()) {
try {
@@ -553,9 +505,13 @@ public final class ResourceManager {
if (fileRes.getType() == IResource.FILE) {
IFile file = (IFile)fileRes;
+ context.startScanning(file);
+
resFolder.processFile(new IFileWrapper(file),
ResourceHelper.getResourceDeltaKind(
- IResourceDelta.ADDED));
+ IResourceDelta.ADDED), context);
+
+ context.finishScanning(file);
}
}
}
@@ -625,4 +581,79 @@ public final class ResourceManager {
return Integer.toString(kind);
}
+
+ /**
+ * Returns true if the Project > Build Automatically option is turned on
+ * (default).
+ *
+ * @return true if the Project > Build Automatically option is turned on
+ * (default).
+ */
+ public static boolean isAutoBuilding() {
+ return ResourcesPlugin.getWorkspace().getDescription().isAutoBuilding();
+ }
+
+ /** Qualified name for the per-project persistent property "needs aapt" */
+ private final static QualifiedName NEED_AAPT = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "aapt");//$NON-NLS-1$
+
+ /**
+ * Mark the given project, and any projects which depend on it as a library
+ * project, as needing a full aapt build the next time the project is built.
+ *
+ * @param project the project to mark as needing aapt
+ */
+ public static void markAaptRequested(IProject project) {
+ try {
+ String needsAapt = Boolean.TRUE.toString();
+ project.setPersistentProperty(NEED_AAPT, needsAapt);
+
+ ProjectState state = Sdk.getProjectState(project);
+ if (state.isLibrary()) {
+ // For library projects also mark the dependent projects as needing full aapt
+ for (ProjectState parent : state.getFullParentProjects()) {
+ parent.getProject().setPersistentProperty(NEED_AAPT, needsAapt);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ /**
+ * Clear the "needs aapt" flag set by {@link #markAaptRequested(IProject)}.
+ * This is usually called when a project is built. Note that this will only
+ * clean the build flag on the given project, not on any downstream projects
+ * that depend on this project as a library project.
+ *
+ * @param project the project to clear from the needs aapt list
+ */
+ public static void clearAaptRequest(IProject project) {
+ try {
+ project.setPersistentProperty(NEED_AAPT, null);
+ // Note that even if this project is a library project, we -don't- clear
+ // the aapt flags on the dependent projects since they may still depend
+ // on other dirty projects. When they are built, they will issue their
+ // own clear flag requests.
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ /**
+ * Returns whether the given project needs a full aapt build.
+ *
+ * @param project the project to check
+ * @return true if the project needs a full aapt run
+ */
+ public static boolean isAaptRequested(IProject project) {
+ try {
+ String b = project.getPersistentProperty(NEED_AAPT);
+ return b != null && Boolean.valueOf(b);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return false;
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
index ae606ed..84ee341 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
@@ -28,8 +28,11 @@ import org.eclipse.core.runtime.Status;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.regex.Matcher;
/**
@@ -547,6 +550,30 @@ public final class ProjectState {
}
/**
+ * Computes the transitive closure of projects referencing this project as a
+ * library project
+ *
+ * @return a collection (in any order) of project states for projects that
+ * directly or indirectly include this project state's project as a
+ * library project
+ */
+ public Collection<ProjectState> getFullParentProjects() {
+ Set<ProjectState> result = new HashSet<ProjectState>();
+ addParentProjects(result, this);
+ return result;
+ }
+
+ /** Adds all parent projects of the given project, transitively, into the given parent set */
+ private static void addParentProjects(Set<ProjectState> parents, ProjectState state) {
+ for (ProjectState s : state.mParentProjects) {
+ if (!parents.contains(s)) {
+ parents.add(s);
+ addParentProjects(parents, s);
+ }
+ }
+ }
+
+ /**
* Update the value of a library dependency.
* <p/>This loops on all current dependency looking for the value to replace and then replaces
* it.
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 666860b..550f53f 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
@@ -40,8 +40,8 @@ import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.project.ProjectProperties;
-import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
+import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarkerDelta;
@@ -66,8 +66,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.Map.Entry;
+import java.util.Set;
/**
* Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
@@ -1063,6 +1063,9 @@ public final class Sdk {
}
public void resourceChangeEventEnd() {
+ if (mModifiedProjects.size() == 0) {
+ return;
+ }
// first make sure all the parents are updated
updateParentProjects();
diff --git a/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java b/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java
index 6706715..e4b6730 100644
--- a/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java
+++ b/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java
@@ -24,8 +24,6 @@ import com.android.io.IAbstractFile;
import com.android.io.StreamException;
import com.android.resources.ResourceType;
-import org.xml.sax.SAXException;
-
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
@@ -33,10 +31,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
/**
* Represents a resource file that also generates ID resources.
* <p/>
@@ -45,11 +39,6 @@ import javax.xml.parsers.SAXParserFactory;
public final class IdGeneratingResourceFile extends ResourceFile
implements IValueResourceRepository {
- private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance();
- static {
- sParserFactory.setNamespaceAware(true);
- }
-
private final Map<String, ResourceValue> mIdResources =
new HashMap<String, ResourceValue>();
@@ -75,44 +64,49 @@ public final class IdGeneratingResourceFile extends ResourceFile
mFileName = getFileName(type);
// Get the resource value of this file as a whole layout
- mFileValue = getFileValue(file,folder);
+ mFileValue = getFileValue(file, folder);
}
@Override
- protected void load() {
+ protected void load(ScanningContext context) {
// Parse the file and look for @+id/ entries
- parseFileForIds();
+ parseFileForIds(context);
// create the resource items in the repository
- updateResourceItems();
+ updateResourceItems(context);
}
@Override
- protected void update() {
+ protected void update(ScanningContext context) {
// Copy the previous list of ID names
- Set<String> oldIdNames = mIdResources.keySet();
+ Set<String> oldIdNames = new HashSet<String>(mIdResources.keySet());
// reset current content.
mIdResources.clear();
// need to parse the file and find the IDs.
- parseFileForIds();
+ if (!parseFileForIds(context)) {
+ context.requestFullAapt();
+ return;
+ }
// We only need to update the repository if our IDs have changed
- if (oldIdNames.equals(mIdResources.keySet()) == false) {
- updateResourceItems();
+ Set<String> keySet = mIdResources.keySet();
+ assert keySet != oldIdNames;
+ if (oldIdNames.equals(keySet) == false) {
+ updateResourceItems(context);
}
}
@Override
- protected void dispose() {
+ protected void dispose(ScanningContext context) {
ResourceRepository repository = getRepository();
// Remove declarations from this file from the repository
repository.removeFile(mResourceTypeList, this);
// Ask for an ID refresh since we'll be taking away ID generating items
- repository.markForIdRefresh();
+ context.requestFullAapt();
}
@Override
@@ -145,22 +139,27 @@ public final class IdGeneratingResourceFile extends ResourceFile
/**
* Looks through the file represented for Ids and adds them to
* our id repository
+ *
+ * @return true if parsing succeeds and false if it fails
*/
- private void parseFileForIds() {
+ private boolean parseFileForIds(ScanningContext context) {
+ IdResourceParser parser = new IdResourceParser(this, context, isFramework());
try {
- SAXParser parser = sParserFactory.newSAXParser();
- parser.parse(getFile().getContents(), new IdResourceParser(this, isFramework()));
- } catch (ParserConfigurationException e) {
- } catch (SAXException e) {
+ IAbstractFile file = getFile();
+ return parser.parse(mFileType, file.getOsLocation(), file.getContents());
} catch (IOException e) {
+ // Pass
} catch (StreamException e) {
+ // Pass
}
+
+ return false;
}
/**
* Add the resources represented by this file to the repository
*/
- private void updateResourceItems() {
+ private void updateResourceItems(ScanningContext context) {
ResourceRepository repository = getRepository();
// remove this file from all existing ResourceItem.
@@ -178,7 +177,7 @@ public final class IdGeneratingResourceFile extends ResourceFile
}
// Ask the repository for an ID refresh
- repository.markForIdRefresh();
+ context.requestFullAapt();
}
/**
diff --git a/ide_common/src/com/android/ide/common/resources/IdResourceParser.java b/ide_common/src/com/android/ide/common/resources/IdResourceParser.java
index 27195a9..324ad2b 100644
--- a/ide_common/src/com/android/ide/common/resources/IdResourceParser.java
+++ b/ide_common/src/com/android/ide/common/resources/IdResourceParser.java
@@ -20,35 +20,136 @@ import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository;
import com.android.resources.ResourceType;
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
+import org.kxml2.io.KXmlParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
-public class IdResourceParser extends DefaultHandler {
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+/**
+ * Parser for scanning an id-generating resource file such as a layout or a menu
+ * file, which registers any ids it encounters with an
+ * {@link IValueResourceRepository}, and which registers errors with a
+ * {@link ScanningContext}.
+ */
+public class IdResourceParser {
private final IValueResourceRepository mRepository;
private final boolean mIsFramework;
+ private ScanningContext mContext;
- public IdResourceParser(IValueResourceRepository repository, boolean isFramework) {
- super();
+ /**
+ * Creates a new {@link IdResourceParser}
+ *
+ * @param repository value repository for registering resource declaration
+ * @param context a context object with state for the current update, such
+ * as a place to stash errors encountered
+ * @param isFramework true if scanning a framework resource
+ */
+ public IdResourceParser(IValueResourceRepository repository, ScanningContext context,
+ boolean isFramework) {
mRepository = repository;
+ mContext = context;
mIsFramework = isFramework;
}
- @Override
- public void startElement(String uri, String localName, String qName, Attributes attributes)
- throws SAXException {
- for (int i = 0; i < attributes.getLength(); ++i) {
- // Let's look up the value to look for the @+*id/ pattern
- String candidate = attributes.getValue(i);
- // Right now the only things that start with the @+ pattern are IDs. If this changes
- // in the future we'll have to change this line
- if (candidate != null && candidate.startsWith("@+")) {
- // Strip out the @+id/ or @+android:id/ section
- String id = candidate.substring(candidate.indexOf('/') + 1);
- ResourceValue newId = new ResourceValue(ResourceType.ID, id, mIsFramework);
- mRepository.addResourceValue(newId);
+ /**
+ * Parse the given input and register ids with the given
+ * {@link IValueResourceRepository}.
+ *
+ * @param type the type of resource being scanned
+ * @param path the full OS path to the file being parsed
+ * @param input the input stream of the XML to be parsed
+ * @return true if parsing succeeds and false if it fails
+ * @throws IOException if reading the contents fails
+ */
+ public boolean parse(ResourceType type, final String path, InputStream input)
+ throws IOException {
+ KXmlParser parser = new KXmlParser();
+ try {
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+
+ if (input instanceof FileInputStream) {
+ input = new BufferedInputStream(input);
}
+ parser.setInput(input, "UTF-8"); //$NON-NLS-1$
+
+ return parse(type, path, parser);
+ } catch (XmlPullParserException e) {
+ String message = e.getMessage();
+
+ // Strip off position description
+ int index = message.indexOf("(position:"); //$NON-NLS-1$ (Hardcoded in KXml)
+ if (index != -1) {
+ message = message.substring(0, index);
+ }
+
+ String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$
+ path, parser.getLineNumber(), message);
+ mContext.addError(error);
+ return false;
+ } catch (RuntimeException e) {
+ // Some exceptions are thrown by the KXmlParser that are not XmlPullParserExceptions,
+ // such as this one:
+ // java.lang.RuntimeException: Undefined Prefix: w in org.kxml2.io.KXmlParser@...
+ // at org.kxml2.io.KXmlParser.adjustNsp(Unknown Source)
+ // at org.kxml2.io.KXmlParser.parseStartTag(Unknown Source)
+ String message = e.getMessage();
+ String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$
+ path, parser.getLineNumber(), message);
+ mContext.addError(error);
+ return false;
}
}
+
+ private boolean parse(ResourceType type, String path, KXmlParser parser)
+ throws XmlPullParserException, IOException {
+ boolean valid = true;
+ ResourceRepository resources = mContext.getRepository();
+ boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt();
+
+ while (true) {
+ int event = parser.next();
+ if (event == XmlPullParser.START_TAG) {
+ for (int i = 0, n = parser.getAttributeCount(); i < n; i++) {
+ String attribute = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ assert value != null : attribute;
+
+ if (value.startsWith("@")) { //$NON-NLS-1$
+ // Gather IDs
+ if (value.startsWith("@+")) { //$NON-NLS-1$
+ // Strip out the @+id/ or @+android:id/ section
+ String id = value.substring(value.indexOf('/') + 1);
+ ResourceValue newId = new ResourceValue(ResourceType.ID, id,
+ mIsFramework);
+ mRepository.addResourceValue(newId);
+ } else if (checkForErrors){
+ // Validate resource references (unless we're scanning a framework
+ // resource or if we've already scheduled a full aapt run)
+ boolean exists = resources.hasResourceItem(value);
+ if (!exists) {
+ String error = String.format(
+ // Don't localize because the exact pattern matches AAPT's
+ // output which has hardcoded regexp matching in
+ // AaptParser.
+ "%1$s:%2$d: Error: No resource found that matches " + //$NON-NLS-1$
+ "the given name (at '%3$s' with value '%4$s')", //$NON-NLS-1$
+ path, parser.getLineNumber(),
+ attribute, value);
+ mContext.addError(error);
+ valid = false;
+ }
+ }
+ }
+ }
+ } else if (event == XmlPullParser.END_DOCUMENT) {
+ break;
+ }
+ }
+
+ return valid;
+ }
}
diff --git a/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java b/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java
index b3e35d9..b95a98c 100644
--- a/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java
+++ b/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java
@@ -59,7 +59,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou
private boolean mNeedIdRefresh;
@Override
- protected void load() {
+ protected void load(ScanningContext context) {
// need to parse the file and find the content.
parseFile();
@@ -70,11 +70,11 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou
mNeedIdRefresh = true;
// create/update the resource items.
- updateResourceItems();
+ updateResourceItems(context);
}
@Override
- protected void update() {
+ protected void update(ScanningContext context) {
// Reset the ID generation flag
mNeedIdRefresh = false;
@@ -107,18 +107,18 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou
mNeedIdRefresh = true;
}
// create/update the resource items.
- updateResourceItems();
+ updateResourceItems(context);
}
@Override
- protected void dispose() {
+ protected void dispose(ScanningContext context) {
ResourceRepository repository = getRepository();
// only remove this file from all existing ResourceItem.
repository.removeFile(mResourceTypeList, this);
// We'll need an ID refresh because we deleted items
- repository.markForIdRefresh();
+ context.requestFullAapt();
// don't need to touch the content, it'll get reclaimed as this objects disappear.
// In the mean time other objects may need to access it.
@@ -135,7 +135,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou
return (list != null && list.size() > 0);
}
- private void updateResourceItems() {
+ private void updateResourceItems(ScanningContext context) {
ResourceRepository repository = getRepository();
// remove this file from all existing ResourceItem.
@@ -157,7 +157,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou
// If we need an ID refresh, ask the repository for that now
if (mNeedIdRefresh) {
- repository.markForIdRefresh();
+ context.requestFullAapt();
}
}
diff --git a/ide_common/src/com/android/ide/common/resources/ResourceFile.java b/ide_common/src/com/android/ide/common/resources/ResourceFile.java
index 03f0b34..bc5b750 100644
--- a/ide_common/src/com/android/ide/common/resources/ResourceFile.java
+++ b/ide_common/src/com/android/ide/common/resources/ResourceFile.java
@@ -37,9 +37,9 @@ public abstract class ResourceFile implements Configurable {
mFolder = folder;
}
- protected abstract void load();
- protected abstract void update();
- protected abstract void dispose();
+ protected abstract void load(ScanningContext context);
+ protected abstract void update(ScanningContext context);
+ protected abstract void dispose(ScanningContext context);
public FolderConfiguration getConfiguration() {
return mFolder.getConfiguration();
diff --git a/ide_common/src/com/android/ide/common/resources/ResourceFolder.java b/ide_common/src/com/android/ide/common/resources/ResourceFolder.java
index e55e14c..b8e0cda 100644
--- a/ide_common/src/com/android/ide/common/resources/ResourceFolder.java
+++ b/ide_common/src/com/android/ide/common/resources/ResourceFolder.java
@@ -47,7 +47,7 @@ public final class ResourceFolder implements Configurable {
* @param type The type of the folder
* @param config The configuration of the folder
* @param folder The associated {@link IAbstractFolder} object.
- * @param isFrameworkRepository
+ * @param repository The associated {@link ResourceRepository}
*/
protected ResourceFolder(ResourceFolderType type, FolderConfiguration config,
IAbstractFolder folder, ResourceRepository repository) {
@@ -59,12 +59,15 @@ public final class ResourceFolder implements Configurable {
/**
* Processes a file and adds it to its parent folder resource.
+ *
* @param file the underlying resource file.
- * @param folder the parent of the resource file.
* @param kind the file change kind.
+ * @param context a context object with state for the current update, such
+ * as a place to stash errors encountered
* @return the {@link ResourceFile} that was created.
*/
- public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind) {
+ public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind,
+ ScanningContext context) {
// look for this file if it's already been created
ResourceFile resFile = getFile(file);
@@ -84,7 +87,7 @@ public final class ResourceFolder implements Configurable {
if (types.size() == 1) {
resFile = new SingleResourceFile(file, this);
- } else if (types.contains(ResourceType.LAYOUT)){
+ } else if (types.contains(ResourceType.LAYOUT)) {
resFile = new IdGeneratingResourceFile(file, this, ResourceType.LAYOUT);
} else if (types.contains(ResourceType.MENU)) {
resFile = new IdGeneratingResourceFile(file, this, ResourceType.MENU);
@@ -92,16 +95,16 @@ public final class ResourceFolder implements Configurable {
resFile = new MultiResourceFile(file, this);
}
- resFile.load();
+ resFile.load(context);
// add it to the folder
addFile(resFile);
}
} else {
if (kind == ResourceDeltaKind.REMOVED) {
- removeFile(resFile);
+ removeFile(resFile, context);
} else {
- resFile.update();
+ resFile.update(context);
}
}
@@ -122,15 +125,15 @@ public final class ResourceFolder implements Configurable {
mFiles.add(file);
}
- protected void removeFile(ResourceFile file) {
- file.dispose();
+ protected void removeFile(ResourceFile file, ScanningContext context) {
+ file.dispose(context);
mFiles.remove(file);
}
- protected void dispose() {
+ protected void dispose(ScanningContext context) {
if (mFiles != null) {
for (ResourceFile file : mFiles) {
- file.dispose();
+ file.dispose(context);
}
mFiles.clear();
diff --git a/ide_common/src/com/android/ide/common/resources/ResourceRepository.java b/ide_common/src/com/android/ide/common/resources/ResourceRepository.java
index 4af4a1a..d78f1d1 100644
--- a/ide_common/src/com/android/ide/common/resources/ResourceRepository.java
+++ b/ide_common/src/com/android/ide/common/resources/ResourceRepository.java
@@ -68,8 +68,6 @@ public abstract class ResourceRepository {
protected final IntArrayWrapper mWrapper = new IntArrayWrapper(null);
- private boolean mNeedsIdRefresh;
-
/**
* Makes a resource repository
* @param isFrameworkRepository whether the repository is for framework resources.
@@ -129,7 +127,8 @@ public abstract class ResourceRepository {
* @param removedFolder the IAbstractFolder object.
* @return the {@link ResourceFolder} that was removed, or null if no matches were found.
*/
- public ResourceFolder removeFolder(ResourceFolderType type, IAbstractFolder removedFolder) {
+ public ResourceFolder removeFolder(ResourceFolderType type, IAbstractFolder removedFolder,
+ ScanningContext context) {
// get the list of folders for the resource type.
List<ResourceFolder> list = mFolderMap.get(type);
@@ -143,7 +142,7 @@ public abstract class ResourceRepository {
list.remove(i);
// remove its content
- resFolder.dispose();
+ resFolder.dispose(context);
return resFolder;
}
@@ -154,6 +153,60 @@ public abstract class ResourceRepository {
}
/**
+ * Returns true if this resource repository contains a resource of the given
+ * name.
+ *
+ * @param url the resource URL
+ * @return true if the resource is known
+ */
+ public boolean hasResourceItem(String url) {
+ assert url.startsWith("@") : url;
+
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd != -1) {
+ int nameBegin = typeEnd + 1;
+
+ // Skip @ and @+
+ int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$
+
+ int colon = url.lastIndexOf(':', typeEnd);
+ if (colon != -1) {
+ typeBegin = colon + 1;
+ }
+ String typeName = url.substring(typeBegin, typeEnd);
+ ResourceType type = ResourceType.getEnum(typeName);
+ if (type != null) {
+ String name = url.substring(nameBegin);
+ return hasResourceItem(type, name);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this resource repository contains a resource of the given
+ * name.
+ *
+ * @param type the type of resource to look up
+ * @param name the name of the resource
+ * @return true if the resource is known
+ */
+ public boolean hasResourceItem(ResourceType type, String name) {
+ List<ResourceItem> list = mResourceMap.get(type);
+
+ if (list != null) {
+ for (ResourceItem item : list) {
+ if (name.equals(item.getName())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
* Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none
* exist, it creates one.
*
@@ -196,29 +249,6 @@ public abstract class ResourceRepository {
protected abstract ResourceItem createResourceItem(String name);
/**
- * Sets a flag which determines whether aapt needs to be run to regenerate resource IDs
- */
- protected void markForIdRefresh() {
- mNeedsIdRefresh = true;
- }
-
- /**
- * Returns whether this repository has been marked as "dirty"; if one or more of the constituent
- * files have declared that the resource item names that they provide have changed.
- */
- public boolean needsIdRefresh() {
- return mNeedsIdRefresh;
- }
-
- /**
- * Indicates that the resources IDs have been regenerated, so the repository is now in a clean
- * state
- */
- public void setIdsRefreshed() {
- mNeedsIdRefresh = false;
- }
-
- /**
* Processes a folder and adds it to the list of existing folders.
* @param folder the folder to process
* @return the ResourceFolder created from this folder, or null if the process failed.
@@ -496,6 +526,8 @@ public abstract class ResourceRepository {
*/
public void loadResources(IAbstractFolder rootFolder)
throws IOException {
+ ScanningContext context = new ScanningContext(this);
+
IAbstractResource[] files = rootFolder.listMembers();
for (IAbstractResource file : files) {
if (file instanceof IAbstractFolder) {
@@ -509,7 +541,7 @@ public abstract class ResourceRepository {
for (IAbstractResource childRes : children) {
if (childRes instanceof IAbstractFile) {
resFolder.processFile((IAbstractFile) childRes,
- ResourceDeltaKind.ADDED);
+ ResourceDeltaKind.ADDED, context);
}
}
}
diff --git a/ide_common/src/com/android/ide/common/resources/ScanningContext.java b/ide_common/src/com/android/ide/common/resources/ScanningContext.java
new file mode 100644
index 0000000..e4ed275
--- /dev/null
+++ b/ide_common/src/com/android/ide/common/resources/ScanningContext.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.resources;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link ScanningContext} keeps track of state during a resource file scan,
+ * such as any parsing errors encountered, whether Android ids have changed, and
+ * so on.
+ */
+public class ScanningContext {
+ private final ResourceRepository mRepository;
+ private boolean mNeedsFullAapt;
+ private List<String> mErrors = null;
+
+ /**
+ * Constructs a new {@link ScanningContext}
+ *
+ * @param repository the associated resource repository
+ */
+ public ScanningContext(ResourceRepository repository) {
+ super();
+ mRepository = repository;
+ }
+
+ /**
+ * Returns a list of errors encountered during scanning
+ *
+ * @return a list of errors encountered during scanning (or null)
+ */
+ public List<String> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Adds the given error to the scanning context. The error should use the
+ * same syntax as real aapt error messages such that the aapt parser can
+ * properly detect the filename, line number, etc.
+ *
+ * @param error the error message, including file name and line number at
+ * the beginning
+ */
+ public void addError(String error) {
+ if (mErrors == null) {
+ mErrors = new ArrayList<String>();
+ }
+ mErrors.add(error);
+ }
+
+ /**
+ * Returns the repository associated with this scanning context
+ *
+ * @return the associated repository, never null
+ */
+ public ResourceRepository getRepository() {
+ return mRepository;
+ }
+
+ /**
+ * Marks that a full aapt compilation of the resources is necessary because it has
+ * detected a change that cannot be incrementally handled.
+ */
+ protected void requestFullAapt() {
+ mNeedsFullAapt = true;
+ }
+
+ /**
+ * Returns whether this repository has been marked as "dirty"; if one or
+ * more of the constituent files have declared that the resource item names
+ * that they provide have changed.
+ *
+ * @return true if a full aapt compilation is required
+ */
+ public boolean needsFullAapt() {
+ return mNeedsFullAapt;
+ }
+}
diff --git a/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java b/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java
index b589b35..6b663e9 100644
--- a/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java
+++ b/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java
@@ -73,7 +73,7 @@ public class SingleResourceFile extends ResourceFile {
}
@Override
- protected void load() {
+ protected void load(ScanningContext context) {
// get a resource item matching the given type and name
ResourceItem item = getRepository().getResourceItem(mType, mResourceName);
@@ -81,22 +81,22 @@ public class SingleResourceFile extends ResourceFile {
item.add(this);
// Ask for an ID refresh since we're adding an item that will generate an ID
- getRepository().markForIdRefresh();
+ context.requestFullAapt();
}
@Override
- protected void update() {
+ protected void update(ScanningContext context) {
// when this happens, nothing needs to be done since the file only generates
// a single resources that doesn't actually change (its content is the file path)
}
@Override
- protected void dispose() {
+ protected void dispose(ScanningContext context) {
// only remove this file from the existing ResourceItem.
getFolder().getRepository().removeFile(mType, this);
// Ask for an ID refresh since we're removing an item that previously generated an ID
- getRepository().markForIdRefresh();
+ context.requestFullAapt();
// don't need to touch the content, it'll get reclaimed as this objects disappear.
// In the mean time other objects may need to access it.