blob: 3429e43a0df78b1251064d027a313088e33ce52b [file] [log] [blame]
/*
* 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);
}
}
}