From e91096c03dda225240b1487f019bf3d082854587 Mon Sep 17 00:00:00 2001 From: Deepanshu Gupta Date: Mon, 28 Sep 2015 18:03:18 -0700 Subject: Support databinding in listitem layouts. Add a wrapper around the PullParser to support stripping out databinding parts. Bug: http://b.android.com/187428 Change-Id: I88080d8f4108cb5ae27a137ad20c5dd7d516f3ea --- .../src/android/content/res/BridgeResources.java | 2 +- .../bridge/src/android/view/BridgeInflater.java | 2 +- .../layoutlib/bridge/android/BridgeContext.java | 2 +- .../layoutlib/bridge/impl/LayoutParserWrapper.java | 378 +++++++++++++++++++++ .../layoutlib/bridge/impl/ParserFactory.java | 19 +- .../bridge/impl/LayoutParserWrapperTest.java | 183 ++++++++++ 6 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java create mode 100644 tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java (limited to 'tools') diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java index 163fbcb..7a69a06 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java @@ -404,7 +404,7 @@ public final class BridgeResources extends Resources { if (xml.isFile()) { // we need to create a pull parser around the layout XML file, and then // give that to our XmlBlockParser - parser = ParserFactory.create(xml); + parser = ParserFactory.create(xml, true); } } diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index f1726eb..5db1bde 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -206,7 +206,7 @@ public final class BridgeInflater extends LayoutInflater { File f = new File(value.getValue()); if (f.isFile()) { try { - XmlPullParser parser = ParserFactory.create(f); + XmlPullParser parser = ParserFactory.create(f, true); BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( parser, bridgeContext, value.isFramework()); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 689e359..b2dc29a 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -436,7 +436,7 @@ public final class BridgeContext extends Context { // we need to create a pull parser around the layout XML file, and then // give that to our XmlBlockParser try { - XmlPullParser parser = ParserFactory.create(xml); + XmlPullParser parser = ParserFactory.create(xml, true); // set the resource ref to have correct view cookies mBridgeInflater.setResourceReference(resource); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java new file mode 100644 index 0000000..3d2a238 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2015 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.layoutlib.bridge.impl; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A wrapper around XmlPullParser that can peek forward to inspect if the file is a data-binding + * layout and some parts need to be stripped. + */ +public class LayoutParserWrapper implements XmlPullParser { + + // Data binding constants. + private static final String TAG_LAYOUT = "layout"; + private static final String TAG_DATA = "data"; + private static final String DEFAULT = "default="; + + private final XmlPullParser mDelegate; + + // Storage for peeked values. + private boolean mPeeked; + private int mEventType; + private int mDepth; + private int mNext; + private List mAttributes; + private String mText; + private String mName; + + // Used to end the document before the actual parser ends. + private int mFinalDepth = -1; + private boolean mEndNow; + + public LayoutParserWrapper(XmlPullParser delegate) { + mDelegate = delegate; + } + + public LayoutParserWrapper peekTillLayoutStart() throws IOException, XmlPullParserException { + final int STATE_LAYOUT_NOT_STARTED = 0; // tag not encountered yet. + final int STATE_ROOT_NOT_STARTED = 1; // the main view root not found yet. + final int STATE_INSIDE_DATA = 2; // START_TAG for found, but not END_TAG. + + int state = STATE_LAYOUT_NOT_STARTED; + int dataDepth = -1; // depth of the tag. Should be two. + while (true) { + int peekNext = peekNext(); + switch (peekNext) { + case START_TAG: + if (state == STATE_LAYOUT_NOT_STARTED) { + if (mName.equals(TAG_LAYOUT)) { + state = STATE_ROOT_NOT_STARTED; + } else { + return this; // no layout tag in the file. + } + } else if (state == STATE_ROOT_NOT_STARTED) { + if (mName.equals(TAG_DATA)) { + state = STATE_INSIDE_DATA; + dataDepth = mDepth; + } else { + mFinalDepth = mDepth; + return this; + } + } + break; + case END_TAG: + if (state == STATE_INSIDE_DATA) { + if (mDepth <= dataDepth) { + state = STATE_ROOT_NOT_STARTED; + } + } + break; + case END_DOCUMENT: + // No layout start found. + return this; + } + // consume the peeked tag. + next(); + } + } + + private int peekNext() throws IOException, XmlPullParserException { + if (mPeeked) { + return mNext; + } + mEventType = mDelegate.getEventType(); + mNext = mDelegate.next(); + if (mEventType == START_TAG) { + int count = mDelegate.getAttributeCount(); + mAttributes = count > 0 ? new ArrayList(count) : + Collections.emptyList(); + for (int i = 0; i < count; i++) { + mAttributes.add(new Attribute(mDelegate.getAttributeNamespace(i), + mDelegate.getAttributeName(i), mDelegate.getAttributeValue(i))); + } + } + mDepth = mDelegate.getDepth(); + mText = mDelegate.getText(); + mName = mDelegate.getName(); + mPeeked = true; + return mNext; + } + + private void reset() { + mAttributes = null; + mText = null; + mName = null; + mPeeked = false; + } + + @Override + public int next() throws XmlPullParserException, IOException { + int returnValue; + int depth; + if (mPeeked) { + returnValue = mNext; + depth = mDepth; + reset(); + } else if (mEndNow) { + return END_DOCUMENT; + } else { + returnValue = mDelegate.next(); + depth = getDepth(); + } + if (returnValue == END_TAG && depth <= mFinalDepth) { + mEndNow = true; + } + return returnValue; + } + + @Override + public int getEventType() throws XmlPullParserException { + return mPeeked ? mEventType : mDelegate.getEventType(); + } + + @Override + public int getDepth() { + return mPeeked ? mDepth : mDelegate.getDepth(); + } + + @Override + public String getName() { + return mPeeked ? mName : mDelegate.getName(); + } + + @Override + public String getText() { + return mPeeked ? mText : mDelegate.getText(); + } + + @Override + public String getAttributeValue(@Nullable String namespace, String name) { + String returnValue = null; + if (mPeeked) { + if (mAttributes == null) { + if (mEventType != START_TAG) { + throw new IndexOutOfBoundsException("getAttributeValue() called when not at " + + "START_TAG."); + } else { + return null; + } + } else { + for (Attribute attribute : mAttributes) { + //noinspection StringEquality for nullness check. + if (attribute.name.equals(name) && (attribute.namespace == namespace || + attribute.namespace != null && attribute.namespace.equals(namespace))) { + returnValue = attribute.value; + break; + } + } + } + } else { + returnValue = mDelegate.getAttributeValue(namespace, name); + } + // Check if the value is bound via data-binding, if yes get the default value. + if (returnValue != null && mFinalDepth >= 0 && returnValue.startsWith("@{")) { + // TODO: Improve the detection of default keyword. + int i = returnValue.lastIndexOf(DEFAULT); + return i > 0 ? returnValue.substring(i + DEFAULT.length(), returnValue.length() - 1) + : null; + } + return returnValue; + } + + private static class Attribute { + @Nullable + public final String namespace; + public final String name; + public final String value; + + public Attribute(@Nullable String namespace, String name, String value) { + this.namespace = namespace; + this.name = name; + this.value = value; + } + } + + // Not affected by peeking. + + @Override + public void setFeature(String s, boolean b) throws XmlPullParserException { + mDelegate.setFeature(s, b); + } + + @Override + public void setProperty(String s, Object o) throws XmlPullParserException { + mDelegate.setProperty(s, o); + } + + @Override + public void setInput(InputStream inputStream, String s) throws XmlPullParserException { + mDelegate.setInput(inputStream, s); + } + + @Override + public void setInput(Reader reader) throws XmlPullParserException { + mDelegate.setInput(reader); + } + + @Override + public String getInputEncoding() { + return mDelegate.getInputEncoding(); + } + + @Override + public String getNamespace(String s) { + return mDelegate.getNamespace(s); + } + + @Override + public String getPositionDescription() { + return mDelegate.getPositionDescription(); + } + + @Override + public int getLineNumber() { + return mDelegate.getLineNumber(); + } + + @Override + public String getNamespace() { + return mDelegate.getNamespace(); + } + + @Override + public int getColumnNumber() { + return mDelegate.getColumnNumber(); + } + + // -- We don't care much about the methods that follow. + + @Override + public void require(int i, String s, String s1) throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean getFeature(String s) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public void defineEntityReplacementText(String s, String s1) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public Object getProperty(String s) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int nextToken() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int getNamespaceCount(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getNamespacePrefix(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getNamespaceUri(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isWhitespace() throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public char[] getTextCharacters(int[] ints) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getPrefix() { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isEmptyElementTag() throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int getAttributeCount() { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeNamespace(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeName(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributePrefix(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeType(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isAttributeDefault(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeValue(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String nextText() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int nextTag() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java index 6e67f59..e273b2c 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java @@ -53,24 +53,35 @@ public class ParserFactory { @NonNull public static XmlPullParser create(@NonNull File f) throws XmlPullParserException, FileNotFoundException { - InputStream stream = new FileInputStream(f); - return create(stream, f.getName(), f.length()); + return create(f, false); } + public static XmlPullParser create(@NonNull File f, boolean isLayout) + throws XmlPullParserException, FileNotFoundException { + InputStream stream = new FileInputStream(f); + return create(stream, f.getName(), f.length(), isLayout); + } @NonNull public static XmlPullParser create(@NonNull InputStream stream, @Nullable String name) throws XmlPullParserException { - return create(stream, name, -1); + return create(stream, name, -1, false); } @NonNull private static XmlPullParser create(@NonNull InputStream stream, @Nullable String name, - long size) throws XmlPullParserException { + long size, boolean isLayout) throws XmlPullParserException { XmlPullParser parser = instantiateParser(name); stream = readAndClose(stream, name, size); parser.setInput(stream, ENCODING); + if (isLayout) { + try { + return new LayoutParserWrapper(parser).peekTillLayoutStart(); + } catch (IOException e) { + throw new XmlPullParserException(null, parser, e); + } + } return parser; } diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java new file mode 100644 index 0000000..2c33862 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2015 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.layoutlib.bridge.impl; + +import org.junit.Test; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.StringReader; + +import static com.android.SdkConstants.NS_RESOURCES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + + +public class LayoutParserWrapperTest { + @Test + @SuppressWarnings("StatementWithEmptyBody") // some for loops need to be empty statements. + public void testDataBindingLayout() throws Exception { + LayoutParserWrapper parser = getParserFromString(sDataBindingLayout); + parser.peekTillLayoutStart(); + assertEquals("Expected START_TAG", START_TAG, parser.next()); + assertEquals("RelativeLayout", parser.getName()); + for (int next = parser.next(); next != START_TAG && next != END_DOCUMENT; + next = parser.next()); + assertEquals("Expected START_TAG", START_TAG, parser.getEventType()); + assertEquals("TextView", parser.getName()); + assertEquals("layout_width incorrect for first text view.", "wrap_content", + parser.getAttributeValue(NS_RESOURCES, "layout_width")); + // Ensure that data-binding part is stripped. + assertEquals("Bound attribute android:text incorrect", "World", + parser.getAttributeValue(NS_RESOURCES, "text")); + assertEquals("resource attribute 'id' for first text view incorrect.", "@+id/first", + parser.getAttributeValue(NS_RESOURCES, "id")); + for (int next = parser.next(); + (next != END_TAG || !"RelativeLayout".equals(parser.getName())) && next != END_DOCUMENT; + next = parser.next()); + assertNotSame("Unexpected end of document", END_DOCUMENT, parser.getEventType()); + assertEquals("Document didn't end when expected.", END_DOCUMENT, parser.next()); + } + + @Test + @SuppressWarnings("StatementWithEmptyBody") + public void testNonDataBindingLayout() throws Exception { + LayoutParserWrapper parser = getParserFromString(sNonDataBindingLayout); + parser.peekTillLayoutStart(); + assertEquals("Expected START_TAG", START_TAG, parser.next()); + assertEquals("RelativeLayout", parser.getName()); + for (int next = parser.next(); next != START_TAG && next != END_DOCUMENT; + next = parser.next()); + assertEquals("Expected START_TAG", START_TAG, parser.getEventType()); + assertEquals("TextView", parser.getName()); + assertEquals("layout_width incorrect for first text view.", "wrap_content", + parser.getAttributeValue(NS_RESOURCES, "layout_width")); + // Ensure that value isn't modified. + assertEquals("Bound attribute android:text incorrect", "@{user.firstName,default=World}", + parser.getAttributeValue(NS_RESOURCES, "text")); + assertEquals("resource attribute 'id' for first text view incorrect.", "@+id/first", + parser.getAttributeValue(NS_RESOURCES, "id")); + for (int next = parser.next(); + (next != END_TAG || !"RelativeLayout".equals(parser.getName())) && next != END_DOCUMENT; + next = parser.next()); + assertNotSame("Unexpected end of document", END_DOCUMENT, parser.getEventType()); + assertEquals("Document didn't end when expected.", END_DOCUMENT, parser.next()); + } + + private static LayoutParserWrapper getParserFromString(String layoutContent) throws + XmlPullParserException { + XmlPullParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new StringReader(layoutContent)); + return new LayoutParserWrapper(parser); + } + + private static final String sDataBindingLayout = + //language=XML + "\n" + + "\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + " \n" + + ""; + + private static final String sNonDataBindingLayout = + //language=XML + "\n" + + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + + " \n" + + ""; +} -- cgit v1.1