/* * 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."); } }