diff options
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. |