diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java new file mode 100644 index 000000000..3429e43a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java @@ -0,0 +1,628 @@ +/* + * 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.editors.manifest.descriptors; + +import com.android.SdkConstants; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.common.resources.platform.AttrsXmlParser; +import com.android.ide.common.resources.platform.DeclareStyleableInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; + +import org.eclipse.core.runtime.IStatus; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + + +/** + * Complete description of the AndroidManifest.xml structure. + * <p/> + * The root element are static instances which always exists. + * However their sub-elements and attributes are created only when the SDK changes or is + * loaded the first time. + */ +public final class AndroidManifestDescriptors implements IDescriptorProvider { + /** Name of the {@code <uses-permission>} */ + public static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$ + private static final String MANIFEST_NODE_NAME = "manifest"; //$NON-NLS-1$ + private static final String ANDROID_MANIFEST_STYLEABLE = + AttrsXmlParser.ANDROID_MANIFEST_STYLEABLE; + + // Public attributes names, attributes descriptors and elements descriptors + + public static final String ANDROID_LABEL_ATTR = "label"; //$NON-NLS-1$ + public static final String ANDROID_NAME_ATTR = "name"; //$NON-NLS-1$ + public static final String PACKAGE_ATTR = "package"; //$NON-NLS-1$ + + /** The {@link ElementDescriptor} for the root Manifest element. */ + private final ElementDescriptor MANIFEST_ELEMENT; + /** The {@link ElementDescriptor} for the root Application element. */ + private final ElementDescriptor APPLICATION_ELEMENT; + + /** The {@link ElementDescriptor} for the root Instrumentation element. */ + private final ElementDescriptor INTRUMENTATION_ELEMENT; + /** The {@link ElementDescriptor} for the root Permission element. */ + private final ElementDescriptor PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesPermission element. */ + private final ElementDescriptor USES_PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesSdk element. */ + private final ElementDescriptor USES_SDK_ELEMENT; + + /** The {@link ElementDescriptor} for the root PermissionGroup element. */ + private final ElementDescriptor PERMISSION_GROUP_ELEMENT; + /** The {@link ElementDescriptor} for the root PermissionTree element. */ + private final ElementDescriptor PERMISSION_TREE_ELEMENT; + + /** Private package attribute for the manifest element. Needs to be handled manually. */ + private final TextAttributeDescriptor PACKAGE_ATTR_DESC; + + public AndroidManifestDescriptors() { + APPLICATION_ELEMENT = createElement("application", null, Mandatory.MANDATORY_LAST); //$NON-NLS-1$ + no child & mandatory + INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$ + + PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$ + USES_PERMISSION_ELEMENT = createElement(USES_PERMISSION); + USES_SDK_ELEMENT = createElement("uses-sdk", null, Mandatory.MANDATORY); //$NON-NLS-1$ + no child & mandatory + + PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$ + PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$ + + MANIFEST_ELEMENT = createElement( + MANIFEST_NODE_NAME, // xml name + new ElementDescriptor[] { + APPLICATION_ELEMENT, + INTRUMENTATION_ELEMENT, + PERMISSION_ELEMENT, + USES_PERMISSION_ELEMENT, + PERMISSION_GROUP_ELEMENT, + PERMISSION_TREE_ELEMENT, + USES_SDK_ELEMENT, + }, + Mandatory.MANDATORY); + + // The "package" attribute is treated differently as it doesn't have the standard + // Android XML namespace. + PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR, + null /* nsUri */, + new AttributeInfo(PACKAGE_ATTR, Format.REFERENCE_SET)).setTooltip( + "This attribute gives a unique name for the package, using a Java-style " + + "naming convention to avoid name collisions.\nFor example, applications " + + "published by Google could have names of the form com.google.app.appname"); + } + + @Override + public ElementDescriptor[] getRootElementDescriptors() { + return new ElementDescriptor[] { MANIFEST_ELEMENT }; + } + + @Override + public ElementDescriptor getDescriptor() { + return getManifestElement(); + } + + public ElementDescriptor getApplicationElement() { + return APPLICATION_ELEMENT; + } + + public ElementDescriptor getManifestElement() { + return MANIFEST_ELEMENT; + } + + public ElementDescriptor getUsesSdkElement() { + return USES_SDK_ELEMENT; + } + + public ElementDescriptor getInstrumentationElement() { + return INTRUMENTATION_ELEMENT; + } + + public ElementDescriptor getPermissionElement() { + return PERMISSION_ELEMENT; + } + + public ElementDescriptor getUsesPermissionElement() { + return USES_PERMISSION_ELEMENT; + } + + public ElementDescriptor getPermissionGroupElement() { + return PERMISSION_GROUP_ELEMENT; + } + + public ElementDescriptor getPermissionTreeElement() { + return PERMISSION_TREE_ELEMENT; + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then updates them + * all at once. + * + * @param manifestMap The map style => attributes from the attrs_manifest.xml file + */ + public synchronized void updateDescriptors( + Map<String, DeclareStyleableInfo> manifestMap) { + + // -- setup the required attributes overrides -- + + Set<String> required = new HashSet<String>(); + required.add("provider/authorities"); //$NON-NLS-1$ + + // -- setup the various attribute format overrides -- + + // The key for each override is "element1,element2,.../attr-xml-local-name" or + // "*/attr-xml-local-name" to match the attribute in any element. + + Map<String, ITextAttributeCreator> overrides = new HashMap<String, ITextAttributeCreator>(); + + overrides.put("*/icon", ReferenceAttributeDescriptor.CREATOR); //$NON-NLS-1$ + + overrides.put("*/theme", ThemeAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("*/permission", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("*/targetPackage", ManifestPkgAttrDescriptor.CREATOR); //$NON-NLS-1$ + + overrides.put("uses-library/name", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$ + ListAttributeDescriptor.CREATOR); + + overrideClassName(overrides, "application", //$NON-NLS-1$ + SdkConstants.CLASS_APPLICATION, + false /*mandatory*/); + overrideClassName(overrides, "application/backupAgent", //$NON-NLS-1$ + "android.app.backup.BackupAgent", //$NON-NLS-1$ + false /*mandatory*/); + overrideClassName(overrides, "activity", SdkConstants.CLASS_ACTIVITY); //$NON-NLS-1$ + overrideClassName(overrides, "receiver", SdkConstants.CLASS_BROADCASTRECEIVER);//$NON-NLS-1$ + overrideClassName(overrides, "service", SdkConstants.CLASS_SERVICE); //$NON-NLS-1$ + overrideClassName(overrides, "provider", SdkConstants.CLASS_CONTENTPROVIDER); //$NON-NLS-1$ + overrideClassName(overrides, "instrumentation", + SdkConstants.CLASS_INSTRUMENTATION); //$NON-NLS-1$ + + // -- list element nodes already created -- + // These elements are referenced by already opened editors, so we want to update them + // but not re-create them when reloading an SDK on the fly. + + HashMap<String, ElementDescriptor> elementDescs = + new HashMap<String, ElementDescriptor>(); + elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(), MANIFEST_ELEMENT); + elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(), APPLICATION_ELEMENT); + elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(), INTRUMENTATION_ELEMENT); + elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(), PERMISSION_ELEMENT); + elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(), USES_PERMISSION_ELEMENT); + elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(), USES_SDK_ELEMENT); + elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT); + elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(), PERMISSION_TREE_ELEMENT); + + // -- + + inflateElement(manifestMap, + overrides, + required, + elementDescs, + MANIFEST_ELEMENT, + "AndroidManifest"); //$NON-NLS-1$ + insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC); + + XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor( + SdkConstants.ANDROID_NS_NAME, SdkConstants.ANDROID_URI); + insertAttribute(MANIFEST_ELEMENT, xmlns); + + /* + * + * + */ + assert sanityCheck(manifestMap, MANIFEST_ELEMENT); + } + + /** + * Sets up a mandatory attribute override using a ClassAttributeDescriptor + * with the specified class name. + * + * @param overrides The current map of overrides. + * @param elementName The element name to override, e.g. "application". + * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden. + * Otherwise, if it contains a (/) the format is "element/attribute", for example + * "application/name" vs "application/backupAgent". + * @param className The fully qualified name of the base class of the attribute. + */ + private static void overrideClassName( + Map<String, ITextAttributeCreator> overrides, + String elementName, + final String className) { + overrideClassName(overrides, elementName, className, true); + } + + /** + * Sets up an attribute override using a ClassAttributeDescriptor + * with the specified class name. + * + * @param overrides The current map of overrides. + * @param elementName The element name to override, e.g. "application". + * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden. + * Otherwise, if it contains a (/) the format is "element/attribute", for example + * "application/name" vs "application/backupAgent". + * @param className The fully qualified name of the base class of the attribute. + * @param mandatory True if this attribute is mandatory, false if optional. + */ + private static void overrideClassName( + Map<String, ITextAttributeCreator> overrides, + String elementName, + final String className, + final boolean mandatory) { + if (elementName.indexOf('/') == -1) { + elementName = elementName + '/' + ANDROID_NAME_ATTR; + } + overrides.put(elementName, + new ITextAttributeCreator() { + @Override + public TextAttributeDescriptor create(String xmlName, String nsUri, + IAttributeInfo attrInfo) { + if (attrInfo == null) { + attrInfo = new AttributeInfo(xmlName, Format.STRING_SET ); + } + + if (SdkConstants.CLASS_ACTIVITY.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostActivityCreationAction.getAction(), + xmlName, + nsUri, + attrInfo, + mandatory, + true /*defaultToProjectOnly*/); + } else if (SdkConstants.CLASS_BROADCASTRECEIVER.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostReceiverCreationAction.getAction(), + xmlName, + nsUri, + attrInfo, + mandatory, + true /*defaultToProjectOnly*/); + } else if (SdkConstants.CLASS_INSTRUMENTATION.equals(className)) { + return new ClassAttributeDescriptor( + className, + null, // no post action + xmlName, + nsUri, + attrInfo, + mandatory, + false /*defaultToProjectOnly*/); + } else { + return new ClassAttributeDescriptor( + className, + xmlName, + nsUri, + attrInfo, + mandatory); + } + } + }); + } + + /** + * Returns a new ElementDescriptor constructed from the information given here + * and the javadoc & attributes extracted from the style map if any. + * <p/> + * Creates an element with no attribute overrides. + */ + private ElementDescriptor createElement( + String xmlName, + ElementDescriptor[] childrenElements, + Mandatory mandatory) { + // Creates an element with no attribute overrides. + String styleName = guessStyleName(xmlName); + String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName; + String uiName = getUiName(xmlName); + + ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl, + null, childrenElements, mandatory); + + return element; + } + + /** + * Returns a new ElementDescriptor constructed from its XML local name. + * <p/> + * This version creates an element not mandatory. + */ + private ElementDescriptor createElement(String xmlName) { + // Creates an element with no child and not mandatory + return createElement(xmlName, null, Mandatory.NOT_MANDATORY); + } + + /** + * Inserts an attribute in this element attribute list if it is not present there yet + * (based on the attribute XML name.) + * The attribute is inserted at the beginning of the attribute list. + */ + private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) { + AttributeDescriptor[] attributes = element.getAttributes(); + for (AttributeDescriptor attr : attributes) { + if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) { + return; + } + } + + AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1]; + newArray[0] = newAttr; + System.arraycopy(attributes, 0, newArray, 1, attributes.length); + element.setAttributes(newArray); + } + + /** + * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration. + * <p/> + * This first creates all the attributes for the given ElementDescriptor. + * It then finds all children of the descriptor, inflates them recursively and sets them + * as child to this ElementDescriptor. + * + * @param styleMap The input styleable map for manifest elements & attributes. + * @param overrides A list of attribute overrides (to customize the type of the attribute + * descriptors). + * @param requiredAttributes Set of attributes to be marked as required. + * @param existingElementDescs A map of already created element descriptors, keyed by + * XML local name. This is used to use the static elements created initially by this + * class, which are referenced directly by editors (so that reloading an SDK won't + * break these references). + * @param elemDesc The current {@link ElementDescriptor} to inflate. + * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name + * will be guessed automatically from the style name. + */ + private void inflateElement( + Map<String, DeclareStyleableInfo> styleMap, + Map<String, ITextAttributeCreator> overrides, + Set<String> requiredAttributes, + HashMap<String, ElementDescriptor> existingElementDescs, + ElementDescriptor elemDesc, + String styleName) { + assert elemDesc != null; + assert styleName != null; + assert styleMap != null; + + if (styleMap == null) { + return; + } + + // define attributes + DeclareStyleableInfo style = styleMap.get(styleName); + if (style != null) { + ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>(); + DescriptorsUtils.appendAttributes(attrDescs, + elemDesc.getXmlLocalName(), + SdkConstants.NS_RESOURCES, + style.getAttributes(), + requiredAttributes, + overrides); + elemDesc.setTooltip(style.getJavaDoc()); + elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()])); + } + + // find all elements that have this one as parent + ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>(); + for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) { + DeclareStyleableInfo childStyle = entry.getValue(); + boolean isParent = false; + String[] parents = childStyle.getParents(); + if (parents != null) { + for (String parent: parents) { + if (styleName.equals(parent)) { + isParent = true; + break; + } + } + } + if (isParent) { + String childStyleName = entry.getKey(); + String childXmlName = guessXmlName(childStyleName); + + // create or re-use element + ElementDescriptor child = existingElementDescs.get(childXmlName); + if (child == null) { + child = createElement(childXmlName); + existingElementDescs.put(childXmlName, child); + } + children.add(child); + + inflateElement(styleMap, + overrides, + requiredAttributes, + existingElementDescs, + child, + childStyleName); + } + } + elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()])); + } + + /** + * Get an UI name from the element XML name. + * <p/> + * Capitalizes the first letter and replace non-alphabet by a space followed by a capital. + */ + private static String getUiName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + sb.append(' '); + capitalize = true; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Guesses the style name for a given XML element name. + * <p/> + * The rules are: + * - capitalize the first letter: + * - if there's a dash, skip it and capitalize the next one + * - prefix AndroidManifest + * The exception is "manifest" which just becomes AndroidManifest. + * <p/> + * Examples: + * - manifest => AndroidManifest + * - application => AndroidManifestApplication + * - uses-permission => AndroidManifestUsesPermission + */ + private String guessStyleName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + if (!xmlName.equals(MANIFEST_NODE_NAME)) { + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + // not a letter -- skip the character and capitalize the next one + capitalize = true; + } else { + sb.append(c); + } + } + } + + sb.insert(0, ANDROID_MANIFEST_STYLEABLE); + return sb.toString(); + } + + /** + * This method performs a sanity check to make sure all the styles declared in the + * manifestMap are actually defined in the actual element descriptors and reachable from + * the manifestElement root node. + */ + private boolean sanityCheck(Map<String, DeclareStyleableInfo> manifestMap, + ElementDescriptor manifestElement) { + TreeSet<String> elementsDeclared = new TreeSet<String>(); + findAllElementNames(manifestElement, elementsDeclared); + + TreeSet<String> stylesDeclared = new TreeSet<String>(); + for (String styleName : manifestMap.keySet()) { + if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) { + stylesDeclared.add(styleName); + } + } + + for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) { + String xmlName = it.next(); + String styleName = guessStyleName(xmlName); + if (stylesDeclared.remove(styleName)) { + it.remove(); + } + } + + StringBuilder sb = new StringBuilder(); + if (!stylesDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: "); + for (String name : stylesDeclared) { + sb.append(guessXmlName(name)); + + if (!name.equals(stylesDeclared.last())) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + sb.setLength(0); + } + + if (!elementsDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: "); + for (String name : elementsDeclared) { + sb.append(name); + if (!name.equals(elementsDeclared.last())) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + } + + return true; + } + + /** + * Performs an approximate translation of the style name into a potential + * xml name. This is more or less the reverse from guessStyleName(). + * + * @return The XML local name for a given style name. + */ + private String guessXmlName(String name) { + StringBuilder sb = new StringBuilder(); + if (ANDROID_MANIFEST_STYLEABLE.equals(name)) { + sb.append(MANIFEST_NODE_NAME); + } else { + name = name.replace(ANDROID_MANIFEST_STYLEABLE, ""); //$NON-NLS-1$ + boolean first_char = true; + for (char c : name.toCharArray()) { + if (c >= 'A' && c <= 'Z') { + if (!first_char) { + sb.append('-'); + } + c = (char) (c - 'A' + 'a'); + } + sb.append(c); + first_char = false; + } + } + return sb.toString(); + } + + /** + * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the + * {@link ElementDescriptor} names defined by the tree of descriptors. + * <p/> + * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s. + */ + private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) { + declared.add(element.getXmlName()); + for (ElementDescriptor desc : element.getChildren()) { + findAllElementNames(desc, declared); + } + } + + +} |