blob: 72dce4a74ce34f1dedaf8fd90f62b6fc2a89bcbe [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.tools.idea.rendering;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.res2.ValueXmlHelper;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.tools.idea.AndroidPsiUtils;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Computable;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.xmlpull.v1.XmlPullParserException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static com.android.SdkConstants.*;
import static com.android.tools.idea.rendering.RenderTask.AttributeFilter;
/**
* {@link ILayoutPullParser} implementation on top of the PSI {@link XmlTag}.
* <p/>
* It's designed to work on layout files, and will not work on other resource files (no text event
* support for example).
* <p/>
* This pull parser generates {@link com.android.ide.common.rendering.api.ViewInfo}s whose keys
* are of type {@link XmlTag}.
*/
public class LayoutPsiPullParser extends LayoutPullParser {
@NotNull
private final LayoutLog myLogger;
@NotNull
private final List<TagSnapshot> myNodeStack = new ArrayList<TagSnapshot>();
@Nullable
protected final TagSnapshot myRoot;
@Nullable
private String myToolsPrefix;
@Nullable
protected String myAndroidPrefix;
protected boolean myProvideViewCookies = true;
/**
* Constructs a new {@link LayoutPsiPullParser}, a parser dedicated to the special case of
* parsing a layout resource files.
*
* @param file The {@link XmlTag} for the root node.
* @param logger The logger to emit warnings too, such as missing fragment associations
*/
@NotNull
public static LayoutPsiPullParser create(@NotNull XmlFile file, @NotNull RenderLogger logger) {
if (ResourceHelper.getFolderType(file) == ResourceFolderType.MENU) {
return new MenuPsiPullParser(file, logger);
}
return new LayoutPsiPullParser(file, logger);
}
/**
* Constructs a new {@link LayoutPsiPullParser}, a parser dedicated to the special case of
* parsing a layout resource files, and handling "exploded rendering" - adding padding on views
* to make them easier to see and operate on.
*
* @param file The {@link XmlTag} for the root node.
* @param logger The logger to emit warnings too, such as missing fragment associations
* @param explodeNodes A set of individual nodes that should be assigned a fixed amount of
* padding ({@link PaddingLayoutPsiPullParser#FIXED_PADDING_VALUE}).
* This is intended for use with nodes that (without padding) would be
* invisible.
* @param density the density factor for the screen.
*/
@NotNull
public static LayoutPsiPullParser create(@NotNull XmlFile file,
@NotNull RenderLogger logger,
@Nullable Set<XmlTag> explodeNodes,
@NotNull Density density) {
if (explodeNodes != null && !explodeNodes.isEmpty()) {
return new PaddingLayoutPsiPullParser(file, logger, explodeNodes, density);
} else {
return new LayoutPsiPullParser(file, logger);
}
}
@NotNull
public static LayoutPsiPullParser create(@Nullable final AttributeFilter filter,
@NotNull XmlTag root,
@NotNull RenderLogger logger) {
return new AttributeFilteredLayoutParser(root, logger, filter);
}
/** Use one of the {@link #create} factory methods instead */
protected LayoutPsiPullParser(@NotNull XmlFile file, @NotNull LayoutLog logger) {
this(AndroidPsiUtils.getRootTagSafely(file), logger);
}
protected LayoutPsiPullParser(@Nullable final XmlTag root, @NotNull LayoutLog logger) {
myLogger = logger;
if (root != null) {
if (ApplicationManager.getApplication().isReadAccessAllowed()) {
if (root.isValid()) {
myAndroidPrefix = root.getPrefixByNamespace(ANDROID_URI);
myToolsPrefix = root.getPrefixByNamespace(TOOLS_URI);
myRoot = createSnapshot(root);
} else {
myRoot = null;
}
} else {
myRoot = ApplicationManager.getApplication().runReadAction(new Computable<TagSnapshot>() {
@Override
public TagSnapshot compute() {
if (root.isValid()) {
myAndroidPrefix = root.getPrefixByNamespace(ANDROID_URI);
myToolsPrefix = root.getPrefixByNamespace(TOOLS_URI);
return createSnapshot(root);
} else {
return null;
}
}
});
}
} else {
myRoot = null;
}
}
@Nullable
protected final TagSnapshot getCurrentNode() {
if (myNodeStack.size() > 0) {
return myNodeStack.get(myNodeStack.size() - 1);
}
return null;
}
@Nullable
protected final TagSnapshot getPreviousNode() {
if (myNodeStack.size() > 1) {
return myNodeStack.get(myNodeStack.size() - 2);
}
return null;
}
@Nullable
protected final AttributeSnapshot getAttribute(int i) {
if (myParsingState != START_TAG) {
throw new IndexOutOfBoundsException();
}
// get the current uiNode
TagSnapshot uiNode = getCurrentNode();
if (uiNode != null) {
return uiNode.attributes.get(i);
}
return null;
}
protected void push(@NotNull TagSnapshot node) {
myNodeStack.add(node);
}
@NotNull
protected TagSnapshot pop() {
return myNodeStack.remove(myNodeStack.size() - 1);
}
// ------------- IXmlPullParser --------
/**
* {@inheritDoc}
* <p/>
* This implementation returns the underlying DOM node of type {@link XmlTag}.
* Note that the link between the GLE and the parsing code depends on this being the actual
* type returned, so you can't just randomly change it here.
*/
@Nullable
@Override
public Object getViewCookie() {
if (myProvideViewCookies) {
TagSnapshot element = getCurrentNode();
if (element != null) {
return element.tag;
}
}
return null;
}
/**
* Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
*/
@SuppressWarnings("deprecation")
@Nullable
@Override
public Object getViewKey() {
return getViewCookie();
}
/**
* This implementation does nothing for now as all the embedded XML will use a normal KXML
* parser.
*/
@Nullable
@Override
public ILayoutPullParser getParser(String layoutName) {
return null;
}
// ------------- XmlPullParser --------
@Override
public String getPositionDescription() {
return "XML DOM element depth:" + myNodeStack.size();
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public int getAttributeCount() {
TagSnapshot node = getCurrentNode();
if (node != null) {
return node.attributes.size();
}
return 0;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Nullable
@Override
public String getAttributeName(int i) {
AttributeSnapshot attribute = getAttribute(i);
if (attribute != null) {
return attribute.name;
}
return null;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Override
public String getAttributeNamespace(int i) {
AttributeSnapshot attribute = getAttribute(i);
if (attribute != null) {
return attribute.namespace;
}
return ""; //$NON-NLS-1$
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Nullable
@Override
public String getAttributePrefix(int i) {
AttributeSnapshot attribute = getAttribute(i);
if (attribute != null) {
return attribute.prefix;
}
return null;
}
/*
* This does not seem to be called by the layoutlib, but we keep this (and maintain
* it) just in case.
*/
@Nullable
@Override
public String getAttributeValue(int i) {
AttributeSnapshot attribute = getAttribute(i);
if (attribute != null) {
return attribute.value;
}
return null;
}
/*
* This is the main method used by the LayoutInflater to query for attributes.
*/
@Nullable
@Override
public String getAttributeValue(String namespace, String localName) {
// get the current uiNode
TagSnapshot tag = getCurrentNode();
if (tag != null) {
if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(tag.tagName)) {
String layout = tag.getAttribute(LayoutMetadata.KEY_FRAGMENT_LAYOUT, TOOLS_URI);
if (layout != null) {
return layout;
}
}
String value = null;
if (namespace == null) {
value = tag.getAttribute(localName);
} else if (namespace.equals(ANDROID_URI)) {
if (myAndroidPrefix != null) {
if (myToolsPrefix != null) {
for (AttributeSnapshot attribute : tag.attributes) {
if (localName.equals(attribute.name)) {
if (myToolsPrefix.equals(attribute.prefix)) {
value = attribute.value;
if (value != null && value.isEmpty()) {
// Empty when there is a runtime attribute set means unset the runtime attribute
value = tag.getAttribute(localName, ANDROID_URI) != null ? null : value;
}
break;
} else if (myAndroidPrefix.equals(attribute.prefix)) {
value = attribute.value;
// Don't break: continue searching in case we find a tools design time attribute
}
}
}
} else {
value = tag.getAttribute(localName, ANDROID_URI);
}
} else {
value = tag.getAttribute(localName, namespace);
}
} else {
// Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup
// will be for the current application's resource package, e.g.
// http://schemas.android.com/apk/res/foo.bar, but the XML document will
// be using http://schemas.android.com/apk/res-auto in library projects:
for (AttributeSnapshot attribute : tag.attributes) {
if (localName.equals(attribute.name) && (namespace.equals(attribute.namespace) ||
AUTO_URI.equals(attribute.namespace))) {
value = attribute.value;
break;
}
}
}
if (value != null) {
// on the fly convert match_parent to fill_parent for compatibility with older
// platforms.
if (VALUE_MATCH_PARENT.equals(value) &&
(ATTR_LAYOUT_WIDTH.equals(localName) || ATTR_LAYOUT_HEIGHT.equals(localName)) &&
ANDROID_URI.equals(namespace)) {
return VALUE_FILL_PARENT;
}
// Handle unicode and XML escapes
for (int i = 0, n = value.length(); i < n; i++) {
char c = value.charAt(i);
if (c == '&' || c == '\\') {
value = ValueXmlHelper.unescapeResourceString(value, true, false);
break;
}
}
}
return value;
}
return null;
}
@Override
public int getDepth() {
return myNodeStack.size();
}
@Nullable
@Override
public String getName() {
if (myParsingState == START_TAG || myParsingState == END_TAG) {
TagSnapshot currentNode = getCurrentNode();
assert currentNode != null; // Should only be called when START_TAG
String name = currentNode.tagName;
if (name.equals(VIEW_FRAGMENT)) {
// Temporarily translate <fragment> to <include> (and in getAttribute
// we will also provide a layout-attribute for the corresponding
// fragment name attribute)
String layout = currentNode.getAttribute(LayoutMetadata.KEY_FRAGMENT_LAYOUT, TOOLS_URI);
if (layout != null) {
return VIEW_INCLUDE;
} else {
String fragmentId = currentNode.getAttribute(ATTR_CLASS);
if (fragmentId == null || fragmentId.isEmpty()) {
fragmentId = currentNode.getAttribute(ATTR_NAME, ANDROID_URI);
if (fragmentId == null || fragmentId.isEmpty()) {
fragmentId = currentNode.getAttribute(ATTR_ID, ANDROID_URI);
}
}
myLogger.warning(RenderLogger.TAG_MISSING_FRAGMENT, "Missing fragment association", fragmentId);
}
}
return name;
}
return null;
}
@Nullable
@Override
public String getNamespace() {
if (myParsingState == START_TAG || myParsingState == END_TAG) {
TagSnapshot currentNode = getCurrentNode();
assert currentNode != null; // Should only be called when START_TAG
return currentNode.namespace;
}
return null;
}
@Nullable
@Override
public String getPrefix() {
if (myParsingState == START_TAG || myParsingState == END_TAG) {
TagSnapshot currentNode = getCurrentNode();
assert currentNode != null; // Should only be called when START_TAG
return currentNode.prefix;
}
return null;
}
@Override
public boolean isEmptyElementTag() throws XmlPullParserException {
if (myParsingState == START_TAG) {
TagSnapshot currentNode = getCurrentNode();
assert currentNode != null; // Should only be called when START_TAG
// This isn't quite right; if layoutlib starts needing this, stash XmlTag#isEmpty() in snapshot
return currentNode.children.isEmpty();
}
throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG", this, null);
}
@Override
protected void onNextFromStartDocument() {
if (myRoot != null) {
push(myRoot);
myParsingState = START_TAG;
} else {
myParsingState = END_DOCUMENT;
}
}
@Override
protected void onNextFromStartTag() {
// get the current node, and look for text or children (children first)
TagSnapshot node = getCurrentNode();
assert node != null; // Should only be called when START_TAG
List<TagSnapshot> children = node.children;
if (!children.isEmpty()) {
// move to the new child, and don't change the state.
push(children.get(0));
// in case the current state is CURRENT_DOC, we set the proper state.
myParsingState = START_TAG;
}
else {
if (myParsingState == START_DOCUMENT) {
// this handles the case where there's no node.
myParsingState = END_DOCUMENT;
}
else {
myParsingState = END_TAG;
}
}
}
@Override
protected void onNextFromEndTag() {
// look for a sibling. if no sibling, go back to the parent
TagSnapshot node = getCurrentNode();
assert node != null; // Should only be called when END_TAG
TagSnapshot sibling = node.getNextSibling();
if (sibling != null) {
node = sibling;
// to go to the sibling, we need to remove the current node,
pop();
// and add its sibling.
push(node);
myParsingState = START_TAG;
}
else {
// move back to the parent
pop();
// we have only one element left (myRoot), then we're done with the document.
if (myNodeStack.isEmpty()) {
myParsingState = END_DOCUMENT;
}
else {
myParsingState = END_TAG;
}
}
}
/** Sets whether this parser will provide view cookies */
public void setProvideViewCookies(boolean provideViewCookies) {
myProvideViewCookies = provideViewCookies;
}
@Nullable
private static TagSnapshot createSnapshot(@NotNull XmlTag tag) {
// <include> tags can't be at the root level; handle <fragment> rewriting here such that we don't
// need to handle it as a tag name rewrite (where it's harder to change the structure)
// https://code.google.com/p/android/issues/detail?id=67910
String rootTag = tag.getName();
assert rootTag != null;
if (rootTag.equals(VIEW_FRAGMENT)) {
XmlAttribute[] psiAttributes = tag.getAttributes();
List<AttributeSnapshot> attributes = Lists.newArrayListWithExpectedSize(psiAttributes.length);
for (XmlAttribute psiAttribute : psiAttributes) {
AttributeSnapshot attribute = AttributeSnapshot.createAttributeSnapshot(psiAttribute);
if (attribute != null) {
attributes.add(attribute);
}
}
List<AttributeSnapshot> includeAttributes = Lists.newArrayListWithExpectedSize(psiAttributes.length);
for (XmlAttribute psiAttribute : psiAttributes) {
String name = psiAttribute.getName();
assert name != null;
if (name.startsWith(XMLNS_PREFIX)) {
continue;
}
String localName = psiAttribute.getLocalName();
if (localName.startsWith(ATTR_LAYOUT_MARGIN) || localName.startsWith(ATTR_PADDING) ||
localName.equals(ATTR_ID)) {
continue;
}
AttributeSnapshot attribute = AttributeSnapshot.createAttributeSnapshot(psiAttribute);
if (attribute != null) {
includeAttributes.add(attribute);
}
}
TagSnapshot include = TagSnapshot.createSyntheticTag(null, VIEW_FRAGMENT, "", "", includeAttributes,
Collections.<TagSnapshot>emptyList());
return TagSnapshot.createSyntheticTag(tag, FRAME_LAYOUT, "", "", attributes, Collections.singletonList(include));
} else if (rootTag.equals(FRAME_LAYOUT)) {
TagSnapshot root = TagSnapshot.createTagSnapshot(tag);
// tools:layout on a <FrameLayout> acts like an <include> child. This
// lets you preview runtime additions on FrameLayouts.
String layout = tag.getAttributeValue(ATTR_LAYOUT, TOOLS_URI);
if (layout != null && root.children.isEmpty()) {
String prefix = tag.getPrefixByNamespace(ANDROID_URI);
if (prefix != null) {
List<TagSnapshot> children = Lists.newArrayList();
root.children = children;
List<AttributeSnapshot> attributes = Lists.newArrayListWithExpectedSize(3);
attributes.add(new AttributeSnapshot("", "", ATTR_LAYOUT, layout));
attributes.add(new AttributeSnapshot(ANDROID_URI, prefix, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT));
attributes.add(new AttributeSnapshot(ANDROID_URI, prefix, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT));
TagSnapshot element = TagSnapshot.createSyntheticTag(null, VIEW_INCLUDE, "", "", attributes,
Collections.<TagSnapshot>emptyList());
children.add(element);
}
}
// Allow <FrameLayout tools:visibleChildren="1,3,5"> to make all but the given children visible
String visibleChild = tag.getAttributeValue("visibleChildren", TOOLS_URI);
if (visibleChild != null) {
Set<Integer> indices = Sets.newHashSet();
for (String s : Splitter.on(',').trimResults().omitEmptyStrings().split(visibleChild)) {
try {
indices.add(Integer.parseInt(s));
} catch (NumberFormatException e) {
// ignore metadata if it's incorrect
}
}
String prefix = tag.getPrefixByNamespace(ANDROID_URI);
if (prefix != null) {
for (int i = 0, n = root.children.size(); i < n; i++) {
TagSnapshot child = root.children.get(i);
boolean visible = indices.contains(i);
child.setAttribute(ATTR_VISIBILITY, ANDROID_URI, prefix, visible ? "visible" : "gone");
}
}
}
return root;
} else if (rootTag.equals(TAG_LAYOUT)) {
for (XmlTag subTag : tag.getSubTags()) {
String subTagName = subTag.getName();
assert subTagName != null;
if (!subTagName.equals(TAG_DATA)) {
return createSnapshot(subTag);
}
}
// Rely on code inspection to log errors in the layout.
return null;
} else {
TagSnapshot root = TagSnapshot.createTagSnapshot(tag);
// Ensure that root tags that qualify for adapter binding specify an id attribute, since that is required for
// attribute binding to work. (Without this, a <ListView> at the root level will not show Item 1, Item 2, etc.
if (rootTag.equals(LIST_VIEW) || rootTag.equals(EXPANDABLE_LIST_VIEW) || rootTag.equals(GRID_VIEW) || rootTag.equals(SPINNER)) {
XmlAttribute id = tag.getAttribute(ATTR_ID, ANDROID_URI);
if (id == null) {
String prefix = tag.getPrefixByNamespace(ANDROID_URI);
if (prefix != null) {
root.setAttribute(ATTR_ID, ANDROID_URI, prefix, "@+id/_dynamic");
}
}
}
return root;
}
}
static class AttributeFilteredLayoutParser extends LayoutPsiPullParser {
@Nullable
private final AttributeFilter myFilter;
public AttributeFilteredLayoutParser(@NotNull XmlTag root, @NotNull LayoutLog logger, @Nullable AttributeFilter filter) {
super(root, logger);
this.myFilter = filter;
}
public AttributeFilteredLayoutParser(@NotNull XmlFile file, @NotNull LayoutLog logger, @Nullable AttributeFilter filter) {
super(file, logger);
this.myFilter = filter;
}
@Nullable
@Override
public String getAttributeValue(final String namespace, final String localName) {
if (myFilter != null) {
TagSnapshot element = getCurrentNode();
if (element != null) {
final XmlTag tag = element.tag;
if (tag != null) {
String value;
if (ApplicationManager.getApplication().isReadAccessAllowed()) {
value = myFilter.getAttribute(tag, namespace, localName);
}
else {
value = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
@Nullable
public String compute() {
return myFilter.getAttribute(tag, namespace, localName);
}
});
}
if (value != null) {
if (value.isEmpty()) { // empty means unset
return null;
}
return value;
}
// null means no preference, not "unset".
}
}
}
return super.getAttributeValue(namespace, localName);
}
}
}