aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources
diff options
context:
space:
mode:
authorBob Badour <bbadour@google.com>2020-05-06 15:09:12 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-05-06 15:09:12 +0000
commitf1a59c98333d28b04b74772f204bcc1df6e83634 (patch)
treefd845444b59dfc72656b7781596e0b1a0662c4c7 /eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources
parent14a008655dafe4ca1fc7d014a16a1c401761675c (diff)
parentd58f8ba3b1869530926bd5f167103dfa161787a1 (diff)
downloadsdk-f1a59c98333d28b04b74772f204bcc1df6e83634.tar.gz
Merge "Revert "Remove unused project."" am: fc7cda06f5 am: d3c69fa48e am: d58f8ba3b1
Change-Id: I297730ce4f9da2bf30dde696439ab825cd642b00
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/CyclicDependencyValidator.java66
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java628
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java242
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java313
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java546
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java234
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java376
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java271
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java655
10 files changed, 3406 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/CyclicDependencyValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/CyclicDependencyValidator.java
new file mode 100644
index 000000000..a2b13c6a8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/CyclicDependencyValidator.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.dialogs.IInputValidator;
+
+import java.util.Collection;
+
+/** A validator which checks for cyclic dependencies */
+public class CyclicDependencyValidator implements IInputValidator {
+ private final Collection<String> mInvalidIds;
+
+ private CyclicDependencyValidator(Collection<String> invalid) {
+ this.mInvalidIds = invalid;
+ }
+
+ @Override
+ public String isValid(String newText) {
+ if (mInvalidIds.contains(newText)) {
+ return "Cyclic include, not valid";
+ }
+ return null;
+ }
+
+ /**
+ * Creates a validator which ensures that the chosen id is not for a layout that is
+ * directly or indirectly including the given layout. Used to avoid cyclic
+ * dependencies when offering layouts to be included within a given file, etc.
+ *
+ * @param file the target file that candidate layouts should not directly or
+ * indirectly include
+ * @return a validator which checks whether resource ids are valid or whether they
+ * could result in a cyclic dependency
+ */
+ @Nullable
+ public static IInputValidator create(@Nullable IFile file) {
+ if (file == null) {
+ return null;
+ }
+
+ IProject project = file.getProject();
+ IncludeFinder includeFinder = IncludeFinder.get(project);
+ final Collection<String> invalid =
+ includeFinder.getInvalidIncludes(file);
+
+ return new CyclicDependencyValidator(invalid);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java
new file mode 100644
index 000000000..b0e3d43d0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java
@@ -0,0 +1,628 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_COLOR;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_VALUES;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TAG_ITEM;
+import static com.android.SdkConstants.TAG_RESOURCES;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.ResourceDeltaKind;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.ResourceUrl;
+import com.android.ide.common.resources.configuration.CountryCodeQualifier;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.KeyboardStateQualifier;
+import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.NavigationMethodQualifier;
+import com.android.ide.common.resources.configuration.NavigationStateQualifier;
+import com.android.ide.common.resources.configuration.NetworkCodeQualifier;
+import com.android.ide.common.resources.configuration.NightModeQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.common.resources.configuration.ScreenDimensionQualifier;
+import com.android.ide.common.resources.configuration.ScreenHeightQualifier;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.common.resources.configuration.ScreenRatioQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.common.resources.configuration.ScreenWidthQualifier;
+import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier;
+import com.android.ide.common.resources.configuration.TextInputMethodQualifier;
+import com.android.ide.common.resources.configuration.TouchScreenQualifier;
+import com.android.ide.common.resources.configuration.UiModeQualifier;
+import com.android.ide.common.resources.configuration.VersionQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.Hyperlinks;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.VisualRefactoring;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+import com.android.resources.FolderTypeRelationship;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.ElementImpl;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.InputSource;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * Helper class to deal with SWT specifics for the resources.
+ */
+@SuppressWarnings("restriction") // XML model
+public class ResourceHelper {
+
+ private final static Map<Class<?>, Image> sIconMap = new HashMap<Class<?>, Image>(
+ FolderConfiguration.getQualifierCount());
+
+ static {
+ try {
+ IconFactory factory = IconFactory.getInstance();
+ sIconMap.put(CountryCodeQualifier.class, factory.getIcon("mcc")); //$NON-NLS-1$
+ sIconMap.put(NetworkCodeQualifier.class, factory.getIcon("mnc")); //$NON-NLS-1$
+ sIconMap.put(LocaleQualifier.class, factory.getIcon("language")); //$NON-NLS-1$
+ sIconMap.put(LayoutDirectionQualifier.class, factory.getIcon("bidi")); //$NON-NLS-1$
+ sIconMap.put(ScreenSizeQualifier.class, factory.getIcon("size")); //$NON-NLS-1$
+ sIconMap.put(ScreenRatioQualifier.class, factory.getIcon("ratio")); //$NON-NLS-1$
+ sIconMap.put(ScreenOrientationQualifier.class, factory.getIcon("orientation")); //$NON-NLS-1$
+ sIconMap.put(UiModeQualifier.class, factory.getIcon("dockmode")); //$NON-NLS-1$
+ sIconMap.put(NightModeQualifier.class, factory.getIcon("nightmode")); //$NON-NLS-1$
+ sIconMap.put(DensityQualifier.class, factory.getIcon("dpi")); //$NON-NLS-1$
+ sIconMap.put(TouchScreenQualifier.class, factory.getIcon("touch")); //$NON-NLS-1$
+ sIconMap.put(KeyboardStateQualifier.class, factory.getIcon("keyboard")); //$NON-NLS-1$
+ sIconMap.put(TextInputMethodQualifier.class, factory.getIcon("text_input")); //$NON-NLS-1$
+ sIconMap.put(NavigationStateQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$
+ sIconMap.put(NavigationMethodQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$
+ sIconMap.put(ScreenDimensionQualifier.class, factory.getIcon("dimension")); //$NON-NLS-1$
+ sIconMap.put(VersionQualifier.class, factory.getIcon("version")); //$NON-NLS-1$
+ sIconMap.put(ScreenWidthQualifier.class, factory.getIcon("width")); //$NON-NLS-1$
+ sIconMap.put(ScreenHeightQualifier.class, factory.getIcon("height")); //$NON-NLS-1$
+ sIconMap.put(SmallestScreenWidthQualifier.class,factory.getIcon("swidth")); //$NON-NLS-1$
+ } catch (Throwable t) {
+ AdtPlugin.log(t , null);
+ }
+ }
+
+ /**
+ * Returns the icon for the qualifier.
+ */
+ public static Image getIcon(Class<? extends ResourceQualifier> theClass) {
+ return sIconMap.get(theClass);
+ }
+
+ /**
+ * Returns a {@link ResourceDeltaKind} from an {@link IResourceDelta} value.
+ * @param kind a {@link IResourceDelta} integer constant.
+ * @return a matching {@link ResourceDeltaKind} or null.
+ *
+ * @see IResourceDelta#ADDED
+ * @see IResourceDelta#REMOVED
+ * @see IResourceDelta#CHANGED
+ */
+ public static ResourceDeltaKind getResourceDeltaKind(int kind) {
+ switch (kind) {
+ case IResourceDelta.ADDED:
+ return ResourceDeltaKind.ADDED;
+ case IResourceDelta.REMOVED:
+ return ResourceDeltaKind.REMOVED;
+ case IResourceDelta.CHANGED:
+ return ResourceDeltaKind.CHANGED;
+ }
+
+ return null;
+ }
+
+ /**
+ * Is this a resource that can be defined in any file within the "values" folder?
+ * <p>
+ * Some resource types can be defined <b>both</b> as a separate XML file as well
+ * as defined within a value XML file. This method will return true for these types
+ * as well. In other words, a ResourceType can return true for both
+ * {@link #isValueBasedResourceType} and {@link #isFileBasedResourceType}.
+ *
+ * @param type the resource type to check
+ * @return true if the given resource type can be represented as a value under the
+ * values/ folder
+ */
+ public static boolean isValueBasedResourceType(ResourceType type) {
+ List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+ for (ResourceFolderType folderType : folderTypes) {
+ if (folderType == ResourceFolderType.VALUES) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Is this a resource that is defined in a file named by the resource plus the XML
+ * extension?
+ * <p>
+ * Some resource types can be defined <b>both</b> as a separate XML file as well as
+ * defined within a value XML file along with other properties. This method will
+ * return true for these resource types as well. In other words, a ResourceType can
+ * return true for both {@link #isValueBasedResourceType} and
+ * {@link #isFileBasedResourceType}.
+ *
+ * @param type the resource type to check
+ * @return true if the given resource type is stored in a file named by the resource
+ */
+ public static boolean isFileBasedResourceType(ResourceType type) {
+ List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+ for (ResourceFolderType folderType : folderTypes) {
+ if (folderType != ResourceFolderType.VALUES) {
+
+ if (type == ResourceType.ID) {
+ // The folder types for ID is not only VALUES but also
+ // LAYOUT and MENU. However, unlike resources, they are only defined
+ // inline there so for the purposes of isFileBasedResourceType
+ // (where the intent is to figure out files that are uniquely identified
+ // by a resource's name) this method should return false anyway.
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this class can create the given resource
+ *
+ * @param resource the resource to be created
+ * @return true if the {@link #createResource} method can create this resource
+ */
+ public static boolean canCreateResource(String resource) {
+ // Cannot create framework resources
+ if (resource.startsWith(ANDROID_PREFIX)) {
+ return false;
+ }
+
+ ResourceUrl parsed = ResourceUrl.parse(resource);
+ if (parsed != null) {
+ if (parsed.framework) {
+ return false;
+ }
+ ResourceType type = parsed.type;
+ String name = parsed.name;
+
+ // Make sure the name is valid
+ ResourceNameValidator validator =
+ ResourceNameValidator.create(false, (Set<String>) null /* existing */, type);
+ if (validator.isValid(name) != null) {
+ return false;
+ }
+
+ return canCreateResourceType(type);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this class can create resources of the given resource
+ * type
+ *
+ * @param type the type of resource to be created
+ * @return true if the {@link #createResource} method can create resources
+ * of this type (provided the name parameter is also valid)
+ */
+ public static boolean canCreateResourceType(ResourceType type) {
+ // We can create all value types
+ if (isValueBasedResourceType(type)) {
+ return true;
+ }
+
+ // We can create -some- file-based types - those supported by the New XML wizard:
+ for (ResourceFolderType folderType : FolderTypeRelationship.getRelatedFolders(type)) {
+ if (NewXmlFileWizard.canCreateXmlFile(folderType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Creates a file-based resource, like a layout. Used by {@link #createResource} */
+ private static Pair<IFile,IRegion> createFileResource(IProject project, ResourceType type,
+ String name) {
+
+ ResourceFolderType folderType = null;
+ for (ResourceFolderType f : FolderTypeRelationship.getRelatedFolders(type)) {
+ if (NewXmlFileWizard.canCreateXmlFile(f)) {
+ folderType = f;
+ break;
+ }
+ }
+ if (folderType == null) {
+ return null;
+ }
+
+ // Find "dimens.xml" file in res/values/ (or corresponding name for other
+ // value types)
+ IPath projectPath = new Path(FD_RESOURCES + WS_SEP + folderType.getName() + WS_SEP
+ + name + '.' + EXT_XML);
+ IFile file = project.getFile(projectPath);
+ return NewXmlFileWizard.createXmlFile(project, file, folderType);
+ }
+
+ /**
+ * Creates a resource of a given type, name and (if applicable) value
+ *
+ * @param project the project to contain the resource
+ * @param type the type of resource
+ * @param name the name of the resource
+ * @param value the value of the resource, if it is a value-type resource
+ * @return a pair of the file containing the resource and a region where the value
+ * appears
+ */
+ public static Pair<IFile,IRegion> createResource(IProject project, ResourceType type,
+ String name, String value) {
+ if (!isValueBasedResourceType(type)) {
+ return createFileResource(project, type, name);
+ }
+
+ // Find "dimens.xml" file in res/values/ (or corresponding name for other
+ // value types)
+ String typeName = type.getName();
+ String fileName = typeName + 's';
+ String projectPath = FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP
+ + fileName + '.' + EXT_XML;
+ Object editRequester = project;
+ IResource member = project.findMember(projectPath);
+ String tagName = Hyperlinks.getTagName(type);
+ boolean createEmptyTag = type == ResourceType.ID;
+ if (member != null) {
+ if (member instanceof IFile) {
+ IFile file = (IFile) member;
+ // File exists: Must add item to the XML
+ IModelManager manager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = manager.getExistingModelForEdit(file);
+ if (model == null) {
+ model = manager.getModelForEdit(file);
+ }
+ if (model instanceof IDOMModel) {
+ model.beginRecording(editRequester, String.format("Add %1$s",
+ type.getDisplayName()));
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ Element root = document.getDocumentElement();
+ IStructuredDocument structuredDocument = model.getStructuredDocument();
+ Node lastElement = null;
+ NodeList childNodes = root.getChildNodes();
+ String indent = null;
+ for (int i = childNodes.getLength() - 1; i >= 0; i--) {
+ Node node = childNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ lastElement = node;
+ indent = AndroidXmlEditor.getIndent(structuredDocument, node);
+ break;
+ }
+ }
+ if (indent == null || indent.length() == 0) {
+ indent = " "; //$NON-NLS-1$
+ }
+ Node nextChild = lastElement != null ? lastElement.getNextSibling() : null;
+ Text indentNode = document.createTextNode('\n' + indent);
+ root.insertBefore(indentNode, nextChild);
+ Element element = document.createElement(tagName);
+ if (createEmptyTag) {
+ if (element instanceof ElementImpl) {
+ ElementImpl elementImpl = (ElementImpl) element;
+ elementImpl.setEmptyTag(true);
+ }
+ }
+ element.setAttribute(ATTR_NAME, name);
+ if (!tagName.equals(typeName)) {
+ element.setAttribute(ATTR_TYPE, typeName);
+ }
+ root.insertBefore(element, nextChild);
+ IRegion region = null;
+
+ if (createEmptyTag) {
+ IndexedRegion domRegion = VisualRefactoring.getRegion(element);
+ int endOffset = domRegion.getEndOffset();
+ region = new Region(endOffset, 0);
+ } else {
+ Node valueNode = document.createTextNode(value);
+ element.appendChild(valueNode);
+
+ IndexedRegion domRegion = VisualRefactoring.getRegion(valueNode);
+ int startOffset = domRegion.getStartOffset();
+ int length = domRegion.getLength();
+ region = new Region(startOffset, length);
+ }
+ model.save();
+ return Pair.of(file, region);
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Cannot access XML value model");
+ } finally {
+ if (model != null) {
+ model.endRecording(editRequester);
+ model.releaseFromEdit();
+ }
+ }
+ }
+
+ return null;
+ } else {
+ // No such file exists: just create it
+ String prolog = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$
+ StringBuilder sb = new StringBuilder(prolog);
+
+ String root = TAG_RESOURCES;
+ sb.append('<').append(root).append('>').append('\n');
+ sb.append(" "); //$NON-NLS-1$
+ sb.append('<');
+ sb.append(tagName);
+ sb.append(" name=\""); //$NON-NLS-1$
+ sb.append(name);
+ sb.append('"');
+ if (!tagName.equals(typeName)) {
+ sb.append(" type=\""); //$NON-NLS-1$
+ sb.append(typeName);
+ sb.append('"');
+ }
+ int start, end;
+ if (createEmptyTag) {
+ sb.append("/>"); //$NON-NLS-1$
+ start = sb.length();
+ end = sb.length();
+ } else {
+ sb.append('>');
+ start = sb.length();
+ sb.append(value);
+ end = sb.length();
+ sb.append('<').append('/');
+ sb.append(tagName);
+ sb.append('>');
+ }
+ sb.append('\n').append('<').append('/').append(root).append('>').append('\n');
+ String result = sb.toString();
+ // TODO: Pretty print string (wait until that CL is integrated)
+ String error = null;
+ try {
+ byte[] buf = result.getBytes("UTF8"); //$NON-NLS-1$
+ InputStream stream = new ByteArrayInputStream(buf);
+ IFile file = project.getFile(new Path(projectPath));
+ file.create(stream, true /*force*/, null /*progress*/);
+ IRegion region = new Region(start, end - start);
+ return Pair.of(file, region);
+ } catch (UnsupportedEncodingException e) {
+ error = e.getMessage();
+ } catch (CoreException e) {
+ error = e.getMessage();
+ }
+
+ error = String.format("Failed to generate %1$s: %2$s", name, error);
+ AdtPlugin.displayError("New Android XML File", error);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the theme name to be shown for theme styles, e.g. for "@style/Theme" it
+ * returns "Theme"
+ *
+ * @param style a theme style string
+ * @return the user visible theme name
+ */
+ public static String styleToTheme(String style) {
+ if (style.startsWith(STYLE_RESOURCE_PREFIX)) {
+ style = style.substring(STYLE_RESOURCE_PREFIX.length());
+ } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
+ style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length());
+ } else if (style.startsWith(PREFIX_RESOURCE_REF)) {
+ // @package:style/foo
+ int index = style.indexOf('/');
+ if (index != -1) {
+ style = style.substring(index + 1);
+ }
+ }
+ return style;
+ }
+
+ /**
+ * Returns true if the given style represents a project theme
+ *
+ * @param style a theme style string
+ * @return true if the style string represents a project theme, as opposed
+ * to a framework theme
+ */
+ public static boolean isProjectStyle(String style) {
+ assert style.startsWith(STYLE_RESOURCE_PREFIX)
+ || style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : style;
+
+ return style.startsWith(STYLE_RESOURCE_PREFIX);
+ }
+
+ /**
+ * Returns the layout resource name for the given layout file, e.g. for
+ * /res/layout/foo.xml returns foo.
+ *
+ * @param layoutFile the layout file whose name we want to look up
+ * @return the layout name
+ */
+ public static String getLayoutName(IFile layoutFile) {
+ String layoutName = layoutFile.getName();
+ int dotIndex = layoutName.indexOf('.');
+ if (dotIndex != -1) {
+ layoutName = layoutName.substring(0, dotIndex);
+ }
+ return layoutName;
+ }
+
+ /**
+ * Tries to resolve the given resource value to an actual RGB color. For state lists
+ * it will pick the simplest/fallback color.
+ *
+ * @param resources the resource resolver to use to follow color references
+ * @param color the color to resolve
+ * @return the corresponding {@link RGB} color, or null
+ */
+ public static RGB resolveColor(ResourceResolver resources, ResourceValue color) {
+ color = resources.resolveResValue(color);
+ if (color == null) {
+ return null;
+ }
+ String value = color.getValue();
+
+ while (value != null) {
+ if (value.startsWith("#")) { //$NON-NLS-1$
+ try {
+ int rgba = ImageUtils.getColor(value);
+ // Drop alpha channel
+ return ImageUtils.intToRgb(rgba);
+ } catch (NumberFormatException nfe) {
+ // Pass
+ }
+ return null;
+ }
+ if (value.startsWith(PREFIX_RESOURCE_REF)) {
+ boolean isFramework = color.isFramework();
+ color = resources.findResValue(value, isFramework);
+ if (color != null) {
+ value = color.getValue();
+ } else {
+ break;
+ }
+ } else {
+ File file = new File(value);
+ if (file.exists() && file.getName().endsWith(DOT_XML)) {
+ // Parse
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ BufferedInputStream bis = null;
+ try {
+ bis = new BufferedInputStream(new FileInputStream(file));
+ InputSource is = new InputSource(bis);
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document document = builder.parse(is);
+ NodeList items = document.getElementsByTagName(TAG_ITEM);
+
+ value = findColorValue(items);
+ continue;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Failed parsing color file %1$s", file.getName());
+ } finally {
+ if (bis != null) {
+ try {
+ bis.close();
+ } catch (IOException e) {
+ // Nothing useful can be done here
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Searches a color XML file for the color definition element that does not
+ * have an associated state and returns its color
+ */
+ private static String findColorValue(NodeList items) {
+ for (int i = 0, n = items.getLength(); i < n; i++) {
+ // Find non-state color definition
+ Node item = items.item(i);
+ boolean hasState = false;
+ if (item.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) item;
+ if (element.hasAttributeNS(ANDROID_URI, ATTR_COLOR)) {
+ NamedNodeMap attributes = element.getAttributes();
+ for (int j = 0, m = attributes.getLength(); j < m; j++) {
+ Attr attribute = (Attr) attributes.item(j);
+ if (attribute.getLocalName().startsWith("state_")) { //$NON-NLS-1$
+ hasState = true;
+ break;
+ }
+ }
+
+ if (!hasState) {
+ return element.getAttributeNS(ANDROID_URI, ATTR_COLOR);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java
new file mode 100644
index 000000000..5ea1edc0e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.DOT_XML;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.JavaConventions;
+import org.eclipse.jface.dialogs.IInputValidator;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Validator which ensures that new Android resource names are valid.
+ */
+public class ResourceNameValidator implements IInputValidator {
+ /** Set of existing names to check for conflicts with */
+ private Set<String> mExisting;
+
+ /** If true, the validated name must be unique */
+ private boolean mUnique = true;
+
+ /** If true, the validated name must exist */
+ private boolean mExist;
+
+ /**
+ * True if the resource name being considered is a "file" based resource (where the
+ * resource name is the actual file name, rather than just a value attribute inside an
+ * XML file name of arbitrary name
+ */
+ private boolean mIsFileType;
+
+ /**
+ * True if the resource type can point to image resources
+ */
+ private boolean mIsImageType;
+
+ /** If true, allow .xml as a name suffix */
+ private boolean mAllowXmlExtension;
+
+ private ResourceNameValidator(boolean allowXmlExtension, Set<String> existing,
+ boolean isFileType, boolean isImageType) {
+ mAllowXmlExtension = allowXmlExtension;
+ mExisting = existing;
+ mIsFileType = isFileType;
+ mIsImageType = isImageType;
+ }
+
+ /**
+ * Makes the resource name validator require that names are unique.
+ *
+ * @return this, for construction chaining
+ */
+ public ResourceNameValidator unique() {
+ mUnique = true;
+ mExist = false;
+
+ return this;
+ }
+
+ /**
+ * Makes the resource name validator require that names already exist
+ *
+ * @return this, for construction chaining
+ */
+ public ResourceNameValidator exist() {
+ mExist = true;
+ mUnique = false;
+
+ return this;
+ }
+
+ @Override
+ public String isValid(String newText) {
+ // IValidator has the same interface as SWT's IInputValidator
+ try {
+ if (newText == null || newText.trim().length() == 0) {
+ return "Enter a new name";
+ }
+
+ if (mAllowXmlExtension && newText.endsWith(DOT_XML)) {
+ newText = newText.substring(0, newText.length() - DOT_XML.length());
+ }
+
+ if (mAllowXmlExtension && mIsImageType
+ && ImageUtils.hasImageExtension(newText)) {
+ newText = newText.substring(0, newText.lastIndexOf('.'));
+ }
+
+ if (!mIsFileType) {
+ newText = newText.replace('.', '_');
+ }
+
+ if (newText.indexOf('.') != -1 && !newText.endsWith(DOT_XML)) {
+ if (mIsImageType) {
+ return "The filename must end with .xml or .png";
+ } else {
+ return "The filename must end with .xml";
+ }
+ }
+
+ // Resource names must be valid Java identifiers, since they will
+ // be represented as Java identifiers in the R file:
+ if (!Character.isJavaIdentifierStart(newText.charAt(0))) {
+ return "The resource name must begin with a character";
+ }
+ for (int i = 1, n = newText.length(); i < n; i++) {
+ char c = newText.charAt(i);
+ if (!Character.isJavaIdentifierPart(c)) {
+ return String.format("'%1$c' is not a valid resource name character", c);
+ }
+ }
+
+ if (mIsFileType) {
+ char first = newText.charAt(0);
+ if (!(first >= 'a' && first <= 'z')) {
+ return String.format(
+ "File-based resource names must start with a lowercase letter.");
+ }
+
+ // AAPT only allows lowercase+digits+_:
+ // "%s: Invalid file name: must contain only [a-z0-9_.]","
+ for (int i = 0, n = newText.length(); i < n; i++) {
+ char c = newText.charAt(i);
+ if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) {
+ return String.format(
+ "File-based resource names must contain only lowercase a-z, 0-9, or _.");
+ }
+ }
+ }
+
+ String level = "1.5"; //$NON-NLS-1$
+ IStatus validIdentifier = JavaConventions.validateIdentifier(newText, level, level);
+ if (!validIdentifier.isOK()) {
+ return String.format("%1$s is not a valid name (reserved Java keyword)", newText);
+ }
+
+
+ if (mExisting != null && (mUnique || mExist)) {
+ boolean exists = mExisting.contains(newText);
+ if (mUnique && exists) {
+ return String.format("%1$s already exists", newText);
+ } else if (mExist && !exists) {
+ return String.format("%1$s does not exist", newText);
+ }
+ }
+
+ return null;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Validation failed: %s", e.toString());
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension,
+ ResourceFolderType type) {
+ boolean isFileType = type != ResourceFolderType.VALUES;
+ return new ResourceNameValidator(allowXmlExtension, null, isFileType,
+ type == ResourceFolderType.DRAWABLE);
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param existing An optional set of names that already exist (and therefore will not
+ * be considered valid if entered as the new name)
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension, Set<String> existing,
+ ResourceType type) {
+ boolean isFileType = ResourceHelper.isFileBasedResourceType(type);
+ return new ResourceNameValidator(allowXmlExtension, existing, isFileType,
+ type == ResourceType.DRAWABLE).unique();
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}. By default, the name will need to be
+ * unique in the project.
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param project the project to validate new resource names for
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension,
+ @Nullable IProject project,
+ @NonNull ResourceType type) {
+ Set<String> existing = null;
+ if (project != null) {
+ existing = new HashSet<String>();
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources projectResources = manager.getProjectResources(project);
+ Collection<ResourceItem> items = projectResources.getResourceItemsOfType(type);
+ for (ResourceItem item : items) {
+ existing.add(item.getName());
+ }
+ }
+
+ boolean isFileType = ResourceHelper.isFileBasedResourceType(type);
+ return new ResourceNameValidator(allowXmlExtension, existing, isFileType,
+ type == ResourceType.DRAWABLE);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java
new file mode 100644
index 000000000..ab5ae4070
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2007 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 com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.IntArrayWrapper;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
+import com.android.resources.ResourceType;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * A monitor for the compiled resources. This only monitors changes in the resources of type
+ * {@link ResourceType#ID}.
+ */
+public final class CompiledResourcesMonitor implements IFileListener, IProjectListener {
+
+ private final static CompiledResourcesMonitor sThis = new CompiledResourcesMonitor();
+
+ /**
+ * Sets up the monitoring system.
+ * @param monitor The main Resource Monitor.
+ */
+ public static void setupMonitor(GlobalProjectMonitor monitor) {
+ monitor.addFileListener(sThis, IResourceDelta.ADDED | IResourceDelta.CHANGED);
+ monitor.addProjectListener(sThis);
+ }
+
+ /**
+ * private constructor to prevent construction.
+ */
+ private CompiledResourcesMonitor() {
+ }
+
+
+ /* (non-Javadoc)
+ * Sent when a file changed : if the file is the R class, then it is parsed again to update
+ * the internal 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)}
+ *
+ * @see IFileListener#fileChanged
+ */
+ @Override
+ public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
+ int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
+ if (!isAndroidProject || flags == IResourceDelta.MARKERS) {
+ // Not Android or only the markers changed: not relevant
+ return;
+ }
+
+ IProject project = file.getProject();
+
+ if (file.getName().equals(SdkConstants.FN_COMPILED_RESOURCE_CLASS)) {
+ // create the classname
+ String className = getRClassName(project);
+ if (className == null) {
+ // We need to abort.
+ AdtPlugin.log(IStatus.ERROR,
+ "fileChanged: failed to find manifest package for project %1$s", //$NON-NLS-1$
+ project.getName());
+ return;
+ }
+ // path will begin with /projectName/bin/classes so we'll ignore that
+ IPath relativeClassPath = file.getFullPath().removeFirstSegments(3);
+ if (packagePathMatches(relativeClassPath.toString(), className)) {
+ loadAndParseRClass(project, className);
+ }
+ }
+ }
+
+ /**
+ * Check to see if the package section of the given path matches the packageName.
+ * For example, /project/bin/classes/com/foo/app/R.class should match com.foo.app.R
+ * @param path the pathname of the file to look at
+ * @param packageName the package qualified name of the class
+ * @return true if the package section of the path matches the package qualified name
+ */
+ private boolean packagePathMatches(String path, String packageName) {
+ // First strip the ".class" off the end of the path
+ String pathWithoutExtension = path.substring(0, path.indexOf(SdkConstants.DOT_CLASS));
+
+ // then split the components of each path by their separators
+ String [] pathArray = pathWithoutExtension.split(Pattern.quote(File.separator));
+ String [] packageArray = packageName.split(AdtConstants.RE_DOT);
+
+
+ int pathIndex = 0;
+ int packageIndex = 0;
+
+ while (pathIndex < pathArray.length && packageIndex < packageArray.length) {
+ if (pathArray[pathIndex].equals(packageArray[packageIndex]) == false) {
+ return false;
+ }
+ pathIndex++;
+ packageIndex++;
+ }
+ // We may have matched all the way up to this point, but we're not sure it's a match
+ // unless BOTH paths done
+ return (pathIndex == pathArray.length && packageIndex == packageArray.length);
+ }
+
+ /**
+ * Processes project close event.
+ */
+ @Override
+ public void projectClosed(IProject project) {
+ // the ProjectResources object will be removed by the ResourceManager.
+ }
+
+ /**
+ * Processes project delete event.
+ */
+ @Override
+ public void projectDeleted(IProject project) {
+ // the ProjectResources object will be removed by the ResourceManager.
+ }
+
+ /**
+ * Processes project open event.
+ */
+ @Override
+ public void projectOpened(IProject project) {
+ // when the project is opened, we get an ADDED event for each file, so we don't
+ // need to do anything here.
+ }
+
+ @Override
+ public void projectRenamed(IProject project, IPath from) {
+ // renamed projects also trigger delete/open event,
+ // so nothing to be done here.
+ }
+
+ /**
+ * Processes existing project at init time.
+ */
+ @Override
+ public void projectOpenedWithWorkspace(IProject project) {
+ try {
+ // check this is an android project
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ String className = getRClassName(project);
+ // Find the classname
+ if (className == null) {
+ // We need to abort.
+ AdtPlugin.log(IStatus.ERROR,
+ "projectOpenedWithWorkspace: failed to find manifest package for project %1$s", //$NON-NLS-1$
+ project.getName());
+ return;
+ }
+ loadAndParseRClass(project, className);
+ }
+ } catch (CoreException e) {
+ // pass
+ }
+ }
+
+ @Override
+ public void allProjectsOpenedWithWorkspace() {
+ // nothing to do.
+ }
+
+
+ private void loadAndParseRClass(IProject project, String className) {
+ try {
+ // first check there's a ProjectResources to store the content
+ ProjectResources projectResources = ResourceManager.getInstance().getProjectResources(
+ project);
+
+ if (projectResources != null) {
+ // create a temporary class loader to load the class
+ ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */,
+ project);
+
+ try {
+ Class<?> clazz = loader.loadClass(className);
+
+ if (clazz != null) {
+ // create the maps to store the result of the parsing
+ Map<ResourceType, Map<String, Integer>> resourceValueMap =
+ new EnumMap<ResourceType, Map<String, Integer>>(ResourceType.class);
+ Map<Integer, Pair<ResourceType, String>> genericValueToNameMap =
+ new HashMap<Integer, Pair<ResourceType, String>>();
+ Map<IntArrayWrapper, String> styleableValueToNameMap =
+ new HashMap<IntArrayWrapper, String>();
+
+ // parse the class
+ if (parseClass(clazz, genericValueToNameMap, styleableValueToNameMap,
+ resourceValueMap)) {
+ // now we associate the maps to the project.
+ projectResources.setCompiledResources(genericValueToNameMap,
+ styleableValueToNameMap, resourceValueMap);
+ }
+ }
+ } catch (Error e) {
+ // Log this error with the class name we're trying to load and abort.
+ AdtPlugin.log(e, "loadAndParseRClass failed to find class %1$s", className); //$NON-NLS-1$
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Parses a R class, and fills maps.
+ * @param rClass the class to parse
+ * @param genericValueToNameMap
+ * @param styleableValueToNameMap
+ * @param resourceValueMap
+ * @return True if we managed to parse the R class.
+ */
+ private boolean parseClass(Class<?> rClass,
+ Map<Integer, Pair<ResourceType, String>> genericValueToNameMap,
+ Map<IntArrayWrapper, String> styleableValueToNameMap, Map<ResourceType,
+ Map<String, Integer>> resourceValueMap) {
+ try {
+ for (Class<?> inner : rClass.getDeclaredClasses()) {
+ String resTypeName = inner.getSimpleName();
+ ResourceType resType = ResourceType.getEnum(resTypeName);
+
+ if (resType != null) {
+ Map<String, Integer> fullMap = new HashMap<String, Integer>();
+ resourceValueMap.put(resType, fullMap);
+
+ for (Field f : inner.getDeclaredFields()) {
+ // only process static final fields.
+ int modifiers = f.getModifiers();
+ if (Modifier.isStatic(modifiers)) {
+ Class<?> type = f.getType();
+ if (type.isArray() && type.getComponentType() == int.class) {
+ // if the object is an int[] we put it in the styleable map
+ styleableValueToNameMap.put(
+ new IntArrayWrapper((int[]) f.get(null)),
+ f.getName());
+ } else if (type == int.class) {
+ Integer value = (Integer) f.get(null);
+ genericValueToNameMap.put(value, Pair.of(resType, f.getName()));
+ fullMap.put(f.getName(), value);
+ } else {
+ assert false;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ } catch (IllegalArgumentException e) {
+ } catch (IllegalAccessException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Returns the class name of the R class, based on the project's manifest's package.
+ *
+ * @return A class name (e.g. "my.app.R") or null if there's no valid package in the manifest.
+ */
+ private String getRClassName(IProject project) {
+ IFile manifestFile = ProjectHelper.getManifest(project);
+ if (manifestFile != null && manifestFile.isSynchronized(IResource.DEPTH_ZERO)) {
+ ManifestData data = AndroidManifestHelper.parseForData(manifestFile);
+ if (data != null) {
+ String javaPackage = data.getPackage();
+ return javaPackage + ".R"; //$NON-NLS-1$
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java
new file mode 100644
index 000000000..7bab4fd54
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/DynamicIdMap.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.resources.manager;
+
+import com.android.resources.ResourceType;
+import com.android.util.Pair;
+import com.android.utils.SparseArray;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DynamicIdMap {
+
+ private final Map<Pair<ResourceType, String>, Integer> mDynamicIds = new HashMap<Pair<ResourceType, String>, Integer>();
+ private final SparseArray<Pair<ResourceType, String>> mRevDynamicIds = new SparseArray<Pair<ResourceType, String>>();
+ private int mDynamicSeed;
+
+ public DynamicIdMap(int seed) {
+ mDynamicSeed = seed;
+ }
+
+ public void reset(int seed) {
+ mDynamicIds.clear();
+ mRevDynamicIds.clear();
+ mDynamicSeed = seed;
+ }
+
+ /**
+ * Returns a dynamic integer for the given resource type/name, creating it if it doesn't
+ * already exist.
+ *
+ * @param type the type of the resource
+ * @param name the name of the resource
+ * @return an integer.
+ */
+ public Integer getId(ResourceType type, String name) {
+ return getId(Pair.of(type, name));
+ }
+
+ /**
+ * Returns a dynamic integer for the given resource type/name, creating it if it doesn't
+ * already exist.
+ *
+ * @param resource the type/name of the resource
+ * @return an integer.
+ */
+ public Integer getId(Pair<ResourceType, String> resource) {
+ Integer value = mDynamicIds.get(resource);
+ if (value == null) {
+ value = Integer.valueOf(++mDynamicSeed);
+ mDynamicIds.put(resource, value);
+ mRevDynamicIds.put(value, resource);
+ }
+
+ return value;
+ }
+
+ public Pair<ResourceType, String> resolveId(int id) {
+ return mRevDynamicIds.get(id);
+ }
+}
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
new file mode 100644
index 000000000..674a601d0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2007 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 com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.util.ArrayList;
+
+/**
+ * The Global Project Monitor tracks project file changes, and forward them to simple project,
+ * file, and folder listeners.
+ * Those listeners can be setup with masks to listen to particular events.
+ * <p/>
+ * To track project resource changes, use the monitor in the {@link ResourceManager}. It is more
+ * efficient and while the global ProjectMonitor can track any file, deleted resource files
+ * cannot be matched to previous {@link ResourceFile} or {@link ResourceFolder} objects by the
+ * time the listeners get the event notifications.
+ *
+ * @see IProjectListener
+ * @see IFolderListener
+ * @see IFileListener
+ */
+public final class GlobalProjectMonitor {
+
+ private final static GlobalProjectMonitor sThis = new GlobalProjectMonitor();
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with file change events.
+ */
+ public interface IFileListener {
+ /**
+ * Sent when a file changed.
+ *
+ * @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)}
+ * @param extension the extension of the file or null if the file does
+ * not have an extension
+ * @param flags the {@link IResourceDelta#getFlags()} value with details
+ * on what changed in the file
+ * @param isAndroidProject whether the parent project is an Android Project
+ */
+ public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
+ int kind, @Nullable String extension, int flags, boolean isAndroidProject);
+ }
+
+ /**
+ * Classes which implements this interface provide methods dealing with project events.
+ */
+ public interface IProjectListener {
+ /**
+ * Sent for each opened android project at the time the listener is put in place.
+ * @param project the opened project.
+ */
+ public void projectOpenedWithWorkspace(IProject project);
+
+ /**
+ * Sent once after all Android projects have been opened,
+ * at the time the listener is put in place.
+ * <p/>
+ * This is called after {@link #projectOpenedWithWorkspace(IProject)} has
+ * been called on all known Android projects.
+ */
+ public void allProjectsOpenedWithWorkspace();
+
+ /**
+ * Sent when a project is opened.
+ * @param project the project being opened.
+ */
+ public void projectOpened(IProject project);
+
+ /**
+ * Sent when a project is closed.
+ * @param project the project being closed.
+ */
+ public void projectClosed(IProject project);
+
+ /**
+ * Sent when a project is deleted.
+ * @param project the project about to be deleted.
+ */
+ public void projectDeleted(IProject project);
+
+ /**
+ * Sent when a project is renamed. During a project rename
+ * {@link #projectDeleted(IProject)} and {@link #projectOpened(IProject)} are also called.
+ * This is called last.
+ *
+ * @param project the new {@link IProject} object.
+ * @param from the path of the project before the rename action.
+ */
+ public void projectRenamed(IProject project, IPath from);
+ }
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with folder change events
+ */
+ public interface IFolderListener {
+ /**
+ * Sent when a folder changed.
+ * @param folder The file that was changed
+ * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()}
+ * @param isAndroidProject whether the parent project is an Android Project
+ */
+ public void folderChanged(IFolder folder, int kind, boolean isAndroidProject);
+ }
+
+ /**
+ * Interface for a listener to be notified when resource change event starts and ends.
+ */
+ public interface IResourceEventListener {
+ public void resourceChangeEventStart();
+ public void resourceChangeEventEnd();
+ }
+
+ /**
+ * Interface for a listener that gets passed the raw delta without processing.
+ */
+ public interface IRawDeltaListener {
+ public void visitDelta(IResourceDelta delta);
+ }
+
+ /**
+ * Base listener bundle to associate a listener to an event mask.
+ */
+ private static class ListenerBundle {
+ /** Mask value to accept all events */
+ public final static int MASK_NONE = -1;
+
+ /**
+ * Event mask. Values accepted are IResourceDelta.###
+ * @see IResourceDelta#ADDED
+ * @see IResourceDelta#REMOVED
+ * @see IResourceDelta#CHANGED
+ * @see IResourceDelta#ADDED_PHANTOM
+ * @see IResourceDelta#REMOVED_PHANTOM
+ * */
+ int kindMask;
+ }
+
+ /**
+ * Listener bundle for file event.
+ */
+ private static class FileListenerBundle extends ListenerBundle {
+
+ /** The file listener */
+ IFileListener listener;
+ }
+
+ /**
+ * Listener bundle for folder event.
+ */
+ private static class FolderListenerBundle extends ListenerBundle {
+ /** The file listener */
+ IFolderListener listener;
+ }
+
+ private final ArrayList<FileListenerBundle> mFileListeners =
+ new ArrayList<FileListenerBundle>();
+
+ private final ArrayList<FolderListenerBundle> mFolderListeners =
+ new ArrayList<FolderListenerBundle>();
+
+ private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>();
+
+ private final ArrayList<IResourceEventListener> mEventListeners =
+ new ArrayList<IResourceEventListener>();
+
+ private final ArrayList<IRawDeltaListener> mRawDeltaListeners =
+ new ArrayList<IRawDeltaListener>();
+
+ private IWorkspace mWorkspace;
+
+ private boolean mIsAndroidProject;
+
+ /**
+ * Delta visitor for resource changes.
+ */
+ private final class DeltaVisitor implements IResourceDeltaVisitor {
+
+ @Override
+ public boolean visit(IResourceDelta delta) {
+ // Find the other resource listeners to notify
+ IResource r = delta.getResource();
+ int type = r.getType();
+ if (type == IResource.FILE) {
+ int kind = delta.getKind();
+ // notify the listeners.
+ for (FileListenerBundle bundle : mFileListeners) {
+ if (bundle.kindMask == ListenerBundle.MASK_NONE
+ || (bundle.kindMask & kind) != 0) {
+ try {
+ bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind,
+ r.getFileExtension(), delta.getFlags(), mIsAndroidProject);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IFileListener.fileChanged");
+ }
+ }
+ }
+ return false;
+ } else if (type == IResource.FOLDER) {
+ int kind = delta.getKind();
+ // notify the listeners.
+ for (FolderListenerBundle bundle : mFolderListeners) {
+ if (bundle.kindMask == ListenerBundle.MASK_NONE
+ || (bundle.kindMask & kind) != 0) {
+ try {
+ bundle.listener.folderChanged((IFolder)r, kind, mIsAndroidProject);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IFileListener.folderChanged");
+ }
+ }
+ }
+ return true;
+ } else if (type == IResource.PROJECT) {
+ IProject project = (IProject)r;
+
+ try {
+ mIsAndroidProject = project.hasNature(AdtConstants.NATURE_DEFAULT);
+ } catch (CoreException e) {
+ // this can only happen if the project does not exist or is not open, neither
+ // of which can happen here since we are processing changes in the project
+ // or at worst a project post-open event.
+ return false;
+ }
+
+ if (mIsAndroidProject == false) {
+ // for non android project, skip the project listeners but return true
+ // to visit the children and notify the IFileListeners
+ return true;
+ }
+
+ int flags = delta.getFlags();
+
+ if ((flags & IResourceDelta.OPEN) != 0) {
+ // the project is opening or closing.
+
+ if (project.isOpen()) {
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ try {
+ pl.projectOpened(project);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IProjectListener.projectOpened");
+ }
+ }
+ } else {
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ try {
+ pl.projectClosed(project);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IProjectListener.projectClosed");
+ }
+ }
+ }
+
+ if ((flags & IResourceDelta.MOVED_FROM) != 0) {
+ IPath from = delta.getMovedFromPath();
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ try {
+ pl.projectRenamed(project, from);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IProjectListener.projectRenamed");
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static GlobalProjectMonitor getMonitor() {
+ return sThis;
+ }
+
+
+ /**
+ * Starts the resource monitoring.
+ * @param ws The current workspace.
+ * @return The monitor object.
+ */
+ public static GlobalProjectMonitor startMonitoring(IWorkspace ws) {
+ if (sThis != null) {
+ ws.addResourceChangeListener(sThis.mResourceChangeListener,
+ IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE);
+ sThis.mWorkspace = ws;
+ }
+ return sThis;
+ }
+
+ /**
+ * Stops the resource monitoring.
+ * @param ws The current workspace.
+ */
+ public static void stopMonitoring(IWorkspace ws) {
+ if (sThis != null) {
+ ws.removeResourceChangeListener(sThis.mResourceChangeListener);
+
+ synchronized (sThis) {
+ sThis.mFileListeners.clear();
+ sThis.mProjectListeners.clear();
+ }
+ }
+ }
+
+ /**
+ * Adds a file listener.
+ * @param listener The listener to receive the events.
+ * @param kindMask The event mask to filter out specific events.
+ * {@link ListenerBundle#MASK_NONE} will forward all events.
+ * See {@link ListenerBundle#kindMask} for more values.
+ */
+ public synchronized void addFileListener(IFileListener listener, int kindMask) {
+ FileListenerBundle bundle = new FileListenerBundle();
+ bundle.listener = listener;
+ bundle.kindMask = kindMask;
+
+ mFileListeners.add(bundle);
+ }
+
+ /**
+ * Removes an existing file listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeFileListener(IFileListener listener) {
+ for (int i = 0 ; i < mFileListeners.size() ; i++) {
+ FileListenerBundle bundle = mFileListeners.get(i);
+ if (bundle.listener == listener) {
+ mFileListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Adds a folder listener.
+ * @param listener The listener to receive the events.
+ * @param kindMask The event mask to filter out specific events.
+ * {@link ListenerBundle#MASK_NONE} will forward all events.
+ * See {@link ListenerBundle#kindMask} for more values.
+ */
+ public synchronized void addFolderListener(IFolderListener listener, int kindMask) {
+ FolderListenerBundle bundle = new FolderListenerBundle();
+ bundle.listener = listener;
+ bundle.kindMask = kindMask;
+
+ mFolderListeners.add(bundle);
+ }
+
+ /**
+ * Removes an existing folder listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeFolderListener(IFolderListener listener) {
+ for (int i = 0 ; i < mFolderListeners.size() ; i++) {
+ FolderListenerBundle bundle = mFolderListeners.get(i);
+ if (bundle.listener == listener) {
+ mFolderListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Adds a project listener.
+ * @param listener The listener to receive the events.
+ */
+ public synchronized void addProjectListener(IProjectListener listener) {
+ mProjectListeners.add(listener);
+
+ // we need to look at the opened projects and give them to the listener.
+
+ // get the list of opened android projects.
+ IWorkspaceRoot workspaceRoot = mWorkspace.getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+ IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel,
+ null /*filter*/);
+
+
+ notifyResourceEventStart();
+
+ for (IJavaProject androidProject : androidProjects) {
+ listener.projectOpenedWithWorkspace(androidProject.getProject());
+ }
+
+ listener.allProjectsOpenedWithWorkspace();
+
+ notifyResourceEventEnd();
+ }
+
+ /**
+ * Removes an existing project listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeProjectListener(IProjectListener listener) {
+ mProjectListeners.remove(listener);
+ }
+
+ /**
+ * Adds a resource event listener.
+ * @param listener The listener to receive the events.
+ */
+ public synchronized void addResourceEventListener(IResourceEventListener listener) {
+ mEventListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing Resource Event listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeResourceEventListener(IResourceEventListener listener) {
+ mEventListeners.remove(listener);
+ }
+
+ /**
+ * Adds a raw delta listener.
+ * @param listener The listener to receive the deltas.
+ */
+ public synchronized void addRawDeltaListener(IRawDeltaListener listener) {
+ mRawDeltaListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing Raw Delta listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeRawDeltaListener(IRawDeltaListener listener) {
+ mRawDeltaListeners.remove(listener);
+ }
+
+ private void notifyResourceEventStart() {
+ for (IResourceEventListener listener : mEventListeners) {
+ try {
+ listener.resourceChangeEventStart();
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventStart");
+ }
+ }
+ }
+
+ private void notifyResourceEventEnd() {
+ for (IResourceEventListener listener : mEventListeners) {
+ try {
+ listener.resourceChangeEventEnd();
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IResourceEventListener.resourceChangeEventEnd");
+ }
+ }
+ }
+
+ private IResourceChangeListener mResourceChangeListener = new IResourceChangeListener() {
+ /**
+ * Processes the workspace resource change events.
+ *
+ * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
+ */
+ @Override
+ public synchronized void resourceChanged(IResourceChangeEvent event) {
+ // notify the event listeners of a start.
+ notifyResourceEventStart();
+
+ if (event.getType() == IResourceChangeEvent.PRE_DELETE) {
+ // a project is being deleted. Lets get the project object and remove
+ // its compiled resource list.
+ IResource r = event.getResource();
+ IProject project = r.getProject();
+
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ try {
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ try {
+ pl.projectDeleted(project);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted");
+ }
+ }
+ } catch (CoreException e) {
+ // just ignore this project.
+ }
+ }
+ } else {
+ // 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);
+ } catch (CoreException e) {
+ }
+ }
+
+ // we're done, notify the event listeners.
+ notifyResourceEventEnd();
+ }
+ };
+}
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 000000000..d61324937
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdeScanningContext.java
@@ -0,0 +1,234 @@
+/*
+ * 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.SdkConstants.ANDROID_URI;
+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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ScanningContext;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.AaptParser;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+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.Map;
+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;
+ private boolean mValidate;
+ private Map<String, AttributeInfo> mAttributeMap;
+ private ResourceRepository mFrameworkResources;
+
+ /**
+ * Constructs a new {@link IdeScanningContext}
+ *
+ * @param repository the associated {@link ResourceRepository}
+ * @param project the associated project
+ * @param validate if true, check that the attributes and resources are
+ * valid and if not request a full AAPT check
+ */
+ public IdeScanningContext(@NonNull ResourceRepository repository, @NonNull IProject project,
+ boolean validate) {
+ super(repository);
+ mProject = project;
+ mValidate = validate;
+
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AndroidTargetData targetData = sdk.getTargetData(project);
+ if (targetData != null) {
+ mAttributeMap = targetData.getAttributeMap();
+ mFrameworkResources = targetData.getFrameworkResources();
+ }
+ }
+ }
+
+ @Override
+ public void addError(@NonNull 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(@NonNull 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(@NonNull 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() {
+ @Override
+ 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() {
+ // returns true if it was explicitly requested or if a file that has errors was modified.
+ // This handles the case where an edit doesn't add any new id but fix a compile error.
+ return super.needsFullAapt() || hasModifiedFilesWithErrors();
+ }
+
+ /**
+ * Returns true if any of the scanned resources has an error marker on it.
+ */
+ private boolean hasModifiedFilesWithErrors() {
+ for (IResource resource : mScannedResources) {
+ try {
+ int depth = resource instanceof IFolder ? DEPTH_ONE : DEPTH_ZERO;
+ if (resource.exists()) {
+ IMarker[] markers = resource.findMarkers(IMarker.PROBLEM,
+ true /*includeSubtypes*/, depth);
+ for (IMarker marker : markers) {
+ if (marker.getAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO) ==
+ IMarker.SEVERITY_ERROR) {
+ return true;
+ }
+ }
+ }
+ } catch (CoreException ce) {
+ // Pass
+ }
+ }
+
+ return false;
+ }
+
+ @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;
+ }
+
+ @Override
+ public boolean checkValue(@Nullable String uri, @NonNull String name, @NonNull String value) {
+ if (!mValidate) {
+ return true;
+ }
+
+ if (!needsFullAapt() && mAttributeMap != null && ANDROID_URI.equals(uri)) {
+ AttributeInfo info = mAttributeMap.get(name);
+ if (info != null && !info.isValid(value, mRepository, mFrameworkResources)) {
+ return false;
+ }
+ }
+
+ return super.checkValue(uri, name, value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java
new file mode 100644
index 000000000..e07f09927
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2008 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 com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IClasspathContainer;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ClassLoader able to load class from output of an Eclipse project.
+ */
+public final class ProjectClassLoader extends ClassLoader {
+
+ private final IJavaProject mJavaProject;
+ private URLClassLoader mJarClassLoader;
+ private boolean mInsideJarClassLoader = false;
+
+ public ProjectClassLoader(ClassLoader parentClassLoader, IProject project) {
+ super(parentClassLoader);
+ mJavaProject = JavaCore.create(project);
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ // if we are here through a child classloader, throw an exception.
+ if (mInsideJarClassLoader) {
+ throw new ClassNotFoundException(name);
+ }
+
+ // attempt to load the class from the main project
+ Class<?> clazz = loadFromProject(mJavaProject, name);
+
+ if (clazz != null) {
+ return clazz;
+ }
+
+ // attempt to load the class from the jar dependencies
+ clazz = loadClassFromJar(name);
+ if (clazz != null) {
+ return clazz;
+ }
+
+ // attempt to load the class from the libraries
+ try {
+ // get the project info
+ ProjectState projectState = Sdk.getProjectState(mJavaProject.getProject());
+
+ // this can happen if the project has no project.properties.
+ if (projectState != null) {
+
+ List<IProject> libProjects = projectState.getFullLibraryProjects();
+ List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
+ libProjects);
+
+ for (IJavaProject javaProject : referencedJavaProjects) {
+ clazz = loadFromProject(javaProject, name);
+
+ if (clazz != null) {
+ return clazz;
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // log exception?
+ }
+
+ throw new ClassNotFoundException(name);
+ }
+
+ /**
+ * Attempts to load a class from a project output folder.
+ * @param project the project to load the class from
+ * @param name the name of the class
+ * @return a class object if found, null otherwise.
+ */
+ private Class<?> loadFromProject(IJavaProject project, String name) {
+ try {
+ // get the project output folder.
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IPath outputLocation = project.getOutputLocation();
+ IResource outRes = root.findMember(outputLocation);
+ if (outRes == null) {
+ return null;
+ }
+
+ File outFolder = new File(outRes.getLocation().toOSString());
+
+ // get the class name segments
+ String[] segments = name.split("\\."); //$NON-NLS-1$
+
+ // try to load the class from the bin folder of the project.
+ File classFile = getFile(outFolder, segments, 0);
+ if (classFile == null) {
+ return null;
+ }
+
+ // load the content of the file and create the class.
+ FileInputStream fis = new FileInputStream(classFile);
+ byte[] data = new byte[(int)classFile.length()];
+ int read = 0;
+ try {
+ read = fis.read(data);
+ } catch (IOException e) {
+ data = null;
+ }
+ fis.close();
+
+ if (data != null) {
+ try {
+ Class<?> clazz = defineClass(null, data, 0, read);
+ if (clazz != null) {
+ return clazz;
+ }
+ } catch (UnsupportedClassVersionError e) {
+ // Attempt to reload on lower version
+ int maxVersion = 50; // JDK 1.6
+ try {
+ byte[] rewritten = rewriteClass(data, maxVersion, 0);
+ return defineClass(null, rewritten, 0, rewritten.length);
+ } catch (UnsupportedClassVersionError e2) {
+ throw e; // throw *original* exception, not attempt to rewrite
+ }
+ }
+ }
+ } catch (Exception e) {
+ // log the exception?
+ }
+
+ return null;
+ }
+
+ /**
+ * Rewrites the given class to the given target class file version.
+ */
+ public static byte[] rewriteClass(byte[] classData, final int maxVersion, final int minVersion) {
+ assert maxVersion >= minVersion;
+ ClassWriter classWriter = new ClassWriter(0);
+ ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ if (version > maxVersion) {
+ version = maxVersion;
+ }
+ if (version < minVersion) {
+ version = minVersion;
+ }
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+ };
+ ClassReader reader = new ClassReader(classData);
+ reader.accept(classVisitor, 0);
+ return classWriter.toByteArray();
+ }
+
+ /**
+ * Returns the File matching the a certain path from a root {@link File}.
+ * <p/>The methods checks that the file ends in .class even though the last segment
+ * does not.
+ * @param parent the root of the file.
+ * @param segments the segments containing the path of the file
+ * @param index the offset at which to start looking into segments.
+ * @throws FileNotFoundException
+ */
+ private File getFile(File parent, String[] segments, int index)
+ throws FileNotFoundException {
+ // reached the end with no match?
+ if (index == segments.length) {
+ throw new FileNotFoundException();
+ }
+
+ String toMatch = segments[index];
+ File[] files = parent.listFiles();
+
+ // we're at the last segments. we look for a matching <file>.class
+ if (index == segments.length - 1) {
+ toMatch = toMatch + ".class";
+
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().equals(toMatch)) {
+ return file;
+ }
+ }
+ }
+
+ // no match? abort.
+ throw new FileNotFoundException();
+ }
+
+ String innerClassName = null;
+
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ if (toMatch.equals(file.getName())) {
+ return getFile(file, segments, index+1);
+ }
+ } else if (file.getName().startsWith(toMatch)) {
+ if (innerClassName == null) {
+ StringBuilder sb = new StringBuilder(segments[index]);
+ for (int i = index + 1 ; i < segments.length ; i++) {
+ sb.append('$');
+ sb.append(segments[i]);
+ }
+ sb.append(".class");
+
+ innerClassName = sb.toString();
+ }
+
+ if (file.getName().equals(innerClassName)) {
+ return file;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads a class from the 3rd party jar present in the project
+ *
+ * @return the class loader or null if not found.
+ */
+ private Class<?> loadClassFromJar(String name) {
+ if (mJarClassLoader == null) {
+ // get the OS path to all the external jars
+ URL[] jars = getExternalJars();
+
+ mJarClassLoader = new URLClassLoader(jars, this /* parent */);
+ }
+
+ try {
+ // because a class loader always look in its parent loader first, we need to know
+ // that we are querying the jar classloader. This will let us know to not query
+ // it again for classes we don't find, or this would create an infinite loop.
+ mInsideJarClassLoader = true;
+ return mJarClassLoader.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // not found? return null.
+ return null;
+ } finally {
+ mInsideJarClassLoader = false;
+ }
+ }
+
+ /**
+ * Returns an array of external jar files used by the project.
+ * @return an array of OS-specific absolute file paths
+ */
+ private final URL[] getExternalJars() {
+ // get a java project from it
+ IJavaProject javaProject = JavaCore.create(mJavaProject.getProject());
+
+ ArrayList<URL> oslibraryList = new ArrayList<URL>();
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY ||
+ e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ // if this is a classpath variable reference, we resolve it.
+ if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ e = JavaCore.getResolvedClasspathEntry(e);
+ }
+
+ handleClassPathEntry(e, oslibraryList);
+ } else if (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
+ // get the container.
+ try {
+ IClasspathContainer container = JavaCore.getClasspathContainer(
+ e.getPath(), javaProject);
+ // ignore the system and default_system types as they represent
+ // libraries that are part of the runtime.
+ if (container != null &&
+ container.getKind() == IClasspathContainer.K_APPLICATION) {
+ IClasspathEntry[] entries = container.getClasspathEntries();
+ for (IClasspathEntry entry : entries) {
+ // TODO: Xav -- is this necessary?
+ if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ entry = JavaCore.getResolvedClasspathEntry(entry);
+ }
+
+ handleClassPathEntry(entry, oslibraryList);
+ }
+ }
+ } catch (JavaModelException jme) {
+ // can't resolve the container? ignore it.
+ AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s",
+ e.getPath());
+ }
+ }
+ }
+ }
+
+ return oslibraryList.toArray(new URL[oslibraryList.size()]);
+ }
+
+ private void handleClassPathEntry(IClasspathEntry e, ArrayList<URL> oslibraryList) {
+ // get the IPath
+ IPath path = e.getPath();
+
+ // check the name ends with .jar
+ if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
+ boolean local = false;
+ IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FILE) {
+ local = true;
+ try {
+ oslibraryList.add(new File(resource.getLocation().toOSString())
+ .toURI().toURL());
+ } catch (MalformedURLException mue) {
+ // pass
+ }
+ }
+
+ if (local == false) {
+ // if the jar path doesn't match a workspace resource,
+ // then we get an OSString and check if this links to a valid file.
+ String osFullPath = path.toOSString();
+
+ File f = new File(osFullPath);
+ if (f.exists()) {
+ try {
+ oslibraryList.add(f.toURI().toURL());
+ } catch (MalformedURLException mue) {
+ // pass
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
new file mode 100644
index 000000000..7c3fd4c13
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2007 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 com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.resources.IntArrayWrapper;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+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.IFolderWrapper;
+import com.android.io.IAbstractFolder;
+import com.android.resources.ResourceType;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Represents the resources of a project.
+ * On top of the regular {@link ResourceRepository} features it provides:
+ *<ul>
+ *<li>configured resources contain the resources coming from the libraries.</li>
+ *<li>resolution to and from resource integer (compiled value in R.java).</li>
+ *<li>handles resource integer for non existing values of type ID. This is used when rendering.</li>
+ *<li>layouts that have no been saved yet. This is handled by generating dynamic IDs
+ * on the fly.</li>
+ *</ul>
+ */
+@SuppressWarnings("deprecation")
+public class ProjectResources extends ResourceRepository {
+ // project resources are defined as 0x7FXX#### where XX is the resource type (layout, drawable,
+ // etc...). Using FF as the type allows for 255 resource types before we get a collision
+ // which should be fine.
+ private final static int DYNAMIC_ID_SEED_START = 0x7fff0000;
+
+ /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */
+ private Map<ResourceType, Map<String, Integer>> mResourceValueMap;
+ /** Map of (id, [name, resType]) for all resources coming from R.java */
+ private Map<Integer, Pair<ResourceType, String>> mResIdValueToNameMap;
+ /** Map of (int[], name) for styleable resources coming from R.java */
+ private Map<IntArrayWrapper, String> mStyleableValueToNameMap;
+
+ private final DynamicIdMap mDynamicIdMap = new DynamicIdMap(DYNAMIC_ID_SEED_START);
+ private final IntArrayWrapper mWrapper = new IntArrayWrapper(null);
+ private final IProject mProject;
+
+ public static ProjectResources create(IProject project) {
+ IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ return new ProjectResources(project, new IFolderWrapper(resFolder));
+ }
+
+ /**
+ * Makes a ProjectResources for a given <var>project</var>.
+ * @param project the project.
+ */
+ private ProjectResources(IProject project, IAbstractFolder resFolder) {
+ super(resFolder, false /*isFrameworkRepository*/);
+ mProject = project;
+ }
+
+ /**
+ * Returns the resources values matching a given {@link FolderConfiguration}, this will
+ * include library dependency.
+ *
+ * @param referenceConfig the configuration that each value must match.
+ * @return a map with guaranteed to contain an entry for each {@link ResourceType}
+ */
+ @Override
+ @NonNull
+ public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources(
+ @NonNull FolderConfiguration referenceConfig) {
+ ensureInitialized();
+
+ Map<ResourceType, Map<String, ResourceValue>> resultMap =
+ new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class);
+
+ // if the project contains libraries, we need to add the libraries resources here
+ // so that they are accessible to the layout rendering.
+ if (mProject != null) {
+ ProjectState state = Sdk.getProjectState(mProject);
+ if (state != null) {
+ List<IProject> libraries = state.getFullLibraryProjects();
+
+ ResourceManager resMgr = ResourceManager.getInstance();
+
+ // because aapt put all the library in their order in this array, the first
+ // one will have priority over the 2nd one. So it's better to loop in the inverse
+ // order and fill the map with resources that will be overwritten by higher
+ // priority resources
+ for (int i = libraries.size() - 1 ; i >= 0 ; i--) {
+ IProject library = libraries.get(i);
+
+ ProjectResources libRes = resMgr.getProjectResources(library);
+ if (libRes != null) {
+ // get the library resources, and only the library, not the dependencies
+ // so call doGetConfiguredResources() directly.
+ Map<ResourceType, Map<String, ResourceValue>> libMap =
+ libRes.doGetConfiguredResources(referenceConfig);
+
+ // we don't want to simply replace the whole map, but instead merge the
+ // content of any sub-map
+ for (Entry<ResourceType, Map<String, ResourceValue>> libEntry :
+ libMap.entrySet()) {
+
+ // get the map currently in the result map for this resource type
+ Map<String, ResourceValue> tempMap = resultMap.get(libEntry.getKey());
+ if (tempMap == null) {
+ // since there's no current map for this type, just add the map
+ // directly coming from the library resources
+ resultMap.put(libEntry.getKey(), libEntry.getValue());
+ } else {
+ // already a map for this type. add the resources from the
+ // library, this will override existing value, which is why
+ // we loop in a specific library order.
+ tempMap.putAll(libEntry.getValue());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // now the project resources themselves.
+ Map<ResourceType, Map<String, ResourceValue>> thisProjectMap =
+ doGetConfiguredResources(referenceConfig);
+
+ // now merge the maps.
+ for (Entry<ResourceType, Map<String, ResourceValue>> entry : thisProjectMap.entrySet()) {
+ ResourceType type = entry.getKey();
+ Map<String, ResourceValue> typeMap = resultMap.get(type);
+ if (typeMap == null) {
+ resultMap.put(type, entry.getValue());
+ } else {
+ typeMap.putAll(entry.getValue());
+ }
+ }
+
+ return resultMap;
+ }
+
+ /**
+ * Returns the {@link ResourceFolder} associated with a {@link IFolder}.
+ * @param folder The {@link IFolder} object.
+ * @return the {@link ResourceFolder} or null if it was not found.
+ *
+ * @see ResourceRepository#getResourceFolder(com.android.io.IAbstractFolder)
+ */
+ public ResourceFolder getResourceFolder(IFolder folder) {
+ return getResourceFolder(new IFolderWrapper(folder));
+ }
+
+ /**
+ * Resolves a compiled resource id into the resource name and type
+ * @param id the resource integer id.
+ * @return a {@link Pair} of 2 strings { name, type } or null if the id could not be resolved
+ */
+ public Pair<ResourceType, String> resolveResourceId(int id) {
+ Pair<ResourceType, String> result = null;
+ if (mResIdValueToNameMap != null) {
+ result = mResIdValueToNameMap.get(id);
+ }
+
+ if (result == null) {
+ synchronized (mDynamicIdMap) {
+ result = mDynamicIdMap.resolveId(id);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Resolves a compiled styleable id of type int[] into the styleable name.
+ */
+ public String resolveStyleable(int[] id) {
+ if (mStyleableValueToNameMap != null) {
+ mWrapper.set(id);
+ return mStyleableValueToNameMap.get(mWrapper);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the integer id of a resource given its type and name.
+ * <p/>If the resource is of type {@link ResourceType#ID} and does not exist in the
+ * internal map, then new id values are dynamically generated (and stored so that queries
+ * with the same names will return the same value).
+ */
+ public Integer getResourceId(ResourceType type, String name) {
+ Integer result = null;
+ if (mResourceValueMap != null) {
+ Map<String, Integer> map = mResourceValueMap.get(type);
+ if (map != null) {
+ result = map.get(name);
+ }
+ }
+
+ if (result == null) {
+ synchronized (mDynamicIdMap) {
+ result = mDynamicIdMap.getId(type, name);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Resets the list of dynamic Ids. This list is used by
+ * {@link #getResourceId(String, String)} when the resource query is an ID that doesn't
+ * exist (for example for ID automatically generated in layout files that are not saved yet.)
+ * <p/>This method resets those dynamic ID and must be called whenever the actual list of IDs
+ * change.
+ */
+ public void resetDynamicIds() {
+ synchronized (mDynamicIdMap) {
+ mDynamicIdMap.reset(DYNAMIC_ID_SEED_START);
+ }
+ }
+
+ @Override
+ @NonNull
+ protected ResourceItem createResourceItem(@NonNull String name) {
+ return new ResourceItem(name);
+ }
+
+ /**
+ * Sets compiled resource information.
+ *
+ * @param resIdValueToNameMap a map of compiled resource id to resource name.
+ * The map is acquired by the {@link ProjectResources} object.
+ * @param styleableValueMap a map of (int[], name) for the styleable information. The map is
+ * acquired by the {@link ProjectResources} object.
+ * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}.
+ * The list is acquired by the {@link ProjectResources} object.
+ */
+ void setCompiledResources(Map<Integer, Pair<ResourceType, String>> resIdValueToNameMap,
+ Map<IntArrayWrapper, String> styleableValueMap,
+ Map<ResourceType, Map<String, Integer>> resourceValueMap) {
+ mResourceValueMap = resourceValueMap;
+ mResIdValueToNameMap = resIdValueToNameMap;
+ mStyleableValueToNameMap = styleableValueMap;
+
+ resetDynamicIds();
+ }
+}
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
new file mode 100644
index 000000000..e407b6a78
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java
@@ -0,0 +1,655 @@
+/*
+ * Copyright (C) 2007 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 com.android.SdkConstants;
+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.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.io.FolderWrapper;
+import com.android.resources.ResourceFolderType;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The ResourceManager tracks resources for all opened projects.
+ * <p/>
+ * It provide direct access to all the resources of a project as a {@link ProjectResources}
+ * object that allows accessing the resources through their file representation or as Android
+ * resources (similar to what is seen by an Android application).
+ * <p/>
+ * The ResourceManager automatically tracks file changes to update its internal representation
+ * of the resources so that they are always up to date.
+ * <p/>
+ * It also gives access to a monitor that is more resource oriented than the
+ * {@link GlobalProjectMonitor}.
+ * This monitor will let you track resource changes by giving you direct access to
+ * {@link ResourceFile}, or {@link ResourceFolder}.
+ *
+ * @see ProjectResources
+ */
+public final class ResourceManager {
+ public final static boolean DEBUG = false;
+
+ private final static ResourceManager sThis = new ResourceManager();
+
+ /**
+ * Map associating project resource with project objects.
+ * <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 Map<IProject, ProjectResources> mMap =
+ new HashMap<IProject, ProjectResources>();
+
+ /**
+ * Interface to be notified of resource changes.
+ *
+ * @see ResourceManager#addListener(IResourceListener)
+ * @see ResourceManager#removeListener(IResourceListener)
+ */
+ public interface IResourceListener {
+ /**
+ * Notification for resource file change.
+ * @param project the project of the file.
+ * @param file the {@link ResourceFile} representing the file.
+ * @param eventType the type of event. See {@link IResourceDelta}.
+ */
+ void fileChanged(IProject project, ResourceFile file, int eventType);
+ /**
+ * Notification for resource folder change.
+ * @param project the project of the file.
+ * @param folder the {@link ResourceFolder} representing the folder.
+ * @param eventType the type of event. See {@link IResourceDelta}.
+ */
+ void folderChanged(IProject project, ResourceFolder folder, int eventType);
+ }
+
+ private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>();
+
+ /**
+ * Sets up the resource manager with the global project monitor.
+ * @param monitor The global project monitor
+ */
+ public static void setup(GlobalProjectMonitor monitor) {
+ monitor.addProjectListener(sThis.mProjectListener);
+ monitor.addRawDeltaListener(sThis.mRawDeltaListener);
+
+ CompiledResourcesMonitor.setupMonitor(monitor);
+ }
+
+ /**
+ * Returns the singleton instance.
+ */
+ public static ResourceManager getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Adds a new {@link IResourceListener} to be notified of resource changes.
+ * @param listener the listener to be added.
+ */
+ public void addListener(IResourceListener listener) {
+ synchronized (mListeners) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore.
+ * @param listener the listener to be removed.
+ */
+ public void removeListener(IResourceListener listener) {
+ synchronized (mListeners) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Returns the resources of a project.
+ * @param project The project
+ * @return a ProjectResources object
+ */
+ public ProjectResources getProjectResources(IProject project) {
+ synchronized (mMap) {
+ ProjectResources resources = mMap.get(project);
+
+ if (resources == null) {
+ resources = ProjectResources.create(project);
+ mMap.put(project, resources);
+ }
+
+ return resources;
+ }
+ }
+
+ /**
+ * 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, IdeScanningContext context) {
+ doProcessDelta(delta, context);
+
+ // when a project is added to the workspace it is possible this is called before the
+ // repo is actually created so this will return null.
+ ResourceRepository repo = context.getRepository();
+ if (repo != null) {
+ repo.postUpdateCleanUp();
+ }
+ }
+
+ /**
+ * 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
+ */
+ private void doProcessDelta(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;
+ }
+
+ // Process this delta first as we need to make sure new folders are created before
+ // we process their content
+ IResource r = delta.getResource();
+ int type = r.getType();
+
+ if (type == IResource.FILE) {
+ context.startScanning(r);
+ updateFile((IFile)r, delta.getMarkerDeltas(), kind, context);
+ context.finishScanning(r);
+ } else if (type == IResource.FOLDER) {
+ updateFolder((IFolder)r, kind, context);
+ } // We only care about files and folders.
+ // Project deltas are handled by our project listener
+
+ // Now, process children recursively
+ IResourceDelta[] children = delta.getAffectedChildren();
+ for (IResourceDelta child : children) {
+ processDelta(child, context);
+ }
+ }
+
+ /**
+ * 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, IdeScanningContext context) {
+ ProjectResources resources;
+
+ final IProject project = folder.getProject();
+
+ try {
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // can't get the project nature? return!
+ return;
+ }
+
+ switch (kind) {
+ case IResourceDelta.ADDED:
+ // checks if the folder is under res.
+ IPath path = folder.getFullPath();
+
+ // the path will be project/res/<something>
+ if (path.segmentCount() == 3) {
+ if (isInResFolder(path)) {
+ // get the project and its resource object.
+ synchronized (mMap) {
+ resources = mMap.get(project);
+
+ // if it doesn't exist, we create it.
+ if (resources == null) {
+ resources = ProjectResources.create(project);
+ mMap.put(project, resources);
+ }
+ }
+
+ ResourceFolder newFolder = resources.processFolder(
+ new IFolderWrapper(folder));
+ if (newFolder != null) {
+ notifyListenerOnFolderChange(project, newFolder, kind);
+ }
+ }
+ }
+ break;
+ case IResourceDelta.CHANGED:
+ // only call the listeners.
+ synchronized (mMap) {
+ resources = mMap.get(folder.getProject());
+ }
+ if (resources != null) {
+ ResourceFolder resFolder = resources.getResourceFolder(folder);
+ if (resFolder != null) {
+ notifyListenerOnFolderChange(project, resFolder, kind);
+ }
+ }
+ break;
+ case IResourceDelta.REMOVED:
+ synchronized (mMap) {
+ resources = mMap.get(folder.getProject());
+ }
+ if (resources != null) {
+ // lets get the folder type
+ ResourceFolderType type = ResourceFolderType.getFolderType(
+ folder.getName());
+
+ context.startScanning(folder);
+ ResourceFolder removedFolder = resources.removeFolder(type,
+ new IFolderWrapper(folder), context);
+ context.finishScanning(folder);
+ if (removedFolder != null) {
+ notifyListenerOnFolderChange(project, removedFolder, kind);
+ }
+ }
+ break;
+ }
+ }
+
+ /**
+ * 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)}
+ * @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,
+ ScanningContext context) {
+ final IProject project = file.getProject();
+
+ try {
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // can't get the project nature? return!
+ return;
+ }
+
+ // get the project resources
+ ProjectResources resources;
+ synchronized (mMap) {
+ resources = mMap.get(project);
+ }
+
+ if (resources == null) {
+ return;
+ }
+
+ // checks if the file is under res/something or bin/res/something
+ IPath path = file.getFullPath();
+
+ if (path.segmentCount() == 4 || path.segmentCount() == 5) {
+ if (isInResFolder(path)) {
+ IContainer container = file.getParent();
+ if (container instanceof IFolder) {
+
+ ResourceFolder folder = resources.getResourceFolder(
+ (IFolder)container);
+
+ // folder can be null as when the whole folder is deleted, the
+ // REMOVED event for the folder comes first. In this case, the
+ // folder will have taken care of things.
+ if (folder != null) {
+ ResourceFile resFile = folder.processFile(
+ new IFileWrapper(file),
+ ResourceHelper.getResourceDeltaKind(kind), context);
+ notifyListenerOnFileChange(project, resFile, kind);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementation of the {@link IProjectListener} as an internal class so that the methods
+ * do not appear in the public API of {@link ResourceManager}.
+ */
+ private final IProjectListener mProjectListener = new IProjectListener() {
+ @Override
+ public void projectClosed(IProject project) {
+ synchronized (mMap) {
+ mMap.remove(project);
+ }
+ }
+
+ @Override
+ public void projectDeleted(IProject project) {
+ synchronized (mMap) {
+ mMap.remove(project);
+ }
+ }
+
+ @Override
+ public void projectOpened(IProject project) {
+ createProject(project);
+ }
+
+ @Override
+ public void projectOpenedWithWorkspace(IProject project) {
+ createProject(project);
+ }
+
+ @Override
+ public void allProjectsOpenedWithWorkspace() {
+ // nothing to do.
+ }
+
+ @Override
+ public void projectRenamed(IProject project, IPath from) {
+ // renamed project get a delete/open event too, so this can be ignored.
+ }
+ };
+
+ /**
+ * Implementation of {@link IRawDeltaListener} as an internal class so that the methods
+ * do not appear in the public API of {@link ResourceManager}. Delta processing can be
+ * accessed through the {@link ResourceManager#visitDelta(IResourceDelta delta)} method.
+ */
+ private final IRawDeltaListener mRawDeltaListener = new IRawDeltaListener() {
+ @Override
+ 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;
+ }
+
+ // 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();
+
+ try {
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ continue;
+ }
+ } catch (CoreException e) {
+ // only happens if the project is closed or doesn't exist.
+ }
+
+ IdeScanningContext context =
+ new IdeScanningContext(getProjectResources(project), project, true);
+
+ 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());
+ }
+ }
+ }
+ };
+
+ /**
+ * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists.
+ */
+ public ResourceFolder getResourceFolder(IFile file) {
+ IContainer container = file.getParent();
+ if (container.getType() == IResource.FOLDER) {
+ IFolder parent = (IFolder)container;
+ IProject project = file.getProject();
+
+ ProjectResources resources = getProjectResources(project);
+ if (resources != null) {
+ return resources.getResourceFolder(parent);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists.
+ */
+ public ResourceFolder getResourceFolder(IFolder folder) {
+ IProject project = folder.getProject();
+
+ ProjectResources resources = getProjectResources(project);
+ if (resources != null) {
+ return resources.getResourceFolder(folder);
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads and returns the resources for a given {@link IAndroidTarget}
+ * @param androidTarget the target from which to load the framework resources
+ */
+ public ResourceRepository loadFrameworkResources(IAndroidTarget androidTarget) {
+ String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES);
+
+ FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath);
+ if (frameworkRes.exists()) {
+ FrameworkResources resources = new FrameworkResources(frameworkRes);
+
+ resources.loadResources();
+ resources.loadPublicResources(AdtPlugin.getDefault());
+ return resources;
+ }
+
+ return null;
+ }
+
+ /**
+ * Initial project parsing to gather resource info.
+ * @param project
+ */
+ private void createProject(IProject project) {
+ if (project.isOpen()) {
+ synchronized (mMap) {
+ ProjectResources projectResources = mMap.get(project);
+ if (projectResources == null) {
+ projectResources = ProjectResources.create(project);
+ mMap.put(project, projectResources);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns true if the path is under /project/res/
+ * @param path a workspace relative path
+ * @return true if the path is under /project res/
+ */
+ private boolean isInResFolder(IPath path) {
+ return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
+ }
+
+ private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder,
+ int eventType) {
+ synchronized (mListeners) {
+ for (IResourceListener listener : mListeners) {
+ try {
+ listener.folderChanged(project, folder, eventType);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,
+ "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) {
+ synchronized (mListeners) {
+ for (IResourceListener listener : mListeners) {
+ try {
+ listener.fileChanged(project, file, eventType);
+ } catch (Throwable t) {
+ AdtPlugin.log(t,
+ "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ /**
+ * Private constructor to enforce singleton design.
+ */
+ private ResourceManager() {
+ }
+
+ // debug only
+ @SuppressWarnings("unused")
+ private String getKindString(int kind) {
+ if (DEBUG) {
+ switch (kind) {
+ case IResourceDelta.ADDED: return "ADDED";
+ case IResourceDelta.REMOVED: return "REMOVED";
+ case IResourceDelta.CHANGED: return "CHANGED";
+ }
+ }
+
+ 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()) {
+ IProject parentProject = parent.getProject();
+ // Mark the project, but only if it's open. Resource#setPersistentProperty
+ // only works on open projects.
+ if (parentProject.isOpen()) {
+ parentProject.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;
+ }
+}