New layout optimization tool. Run layoutopt on the command line.

Change-Id: I8e4697e19ca8a203dc8a41b464f7cb46d52184b0
This commit is contained in:
Romain Guy
2009-10-05 02:21:30 -07:00
parent d13d440d43
commit 3958d08fd4
27 changed files with 1857 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Copyright 2009 The Android Open Source Project
#
UIX_LOCAL_DIR := $(call my-dir)
include $(UIX_LOCAL_DIR)/src/Android.mk

View File

@@ -0,0 +1,13 @@
# Copyright 2009 The Android Open Source Project
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_JAVA_RESOURCE_DIRS := resources
LOCAL_MODULE := uix
LOCAL_JAVA_LIBRARIES := \
groovy-all-1.6.5
include $(BUILD_HOST_JAVA_LIBRARY)

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2009 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.layoutopt.uix;
import java.util.List;
import java.util.ArrayList;
/**
* Contains the results of a layout analysis. Instances of this class are
* generated by {@link com.android.layoutopt.uix.LayoutAnalyzer}.
*
* @see com.android.layoutopt.uix.LayoutAnalyzer
*/
public class LayoutAnalysis {
/**
* Default layout analysis used to describe a problem with the
* analysis process.
*/
static final LayoutAnalysis ERROR = new LayoutAnalysis("");
static {
ERROR.mAnalyzed = false;
ERROR.addIssue("The layout could not be analyzed. Check if you specified a valid "
+ "XML layout, if the specified file exists, etc.");
}
private final List<Issue> mIssues = new ArrayList<Issue>();
private String mName;
private boolean mAnalyzed;
/**
* Creates a new analysis. An analysis is always considered invalid by default.
*
* @see #validate()
* @see #isValid()
*/
LayoutAnalysis(String name) {
mName = name;
}
/**
* Returns the name of this analysis.
*/
public String getName() {
return mName;
}
void setName(String name) {
mName = name;
}
/**
* Adds an issue to the layout analysis.
*
* @param description Description of the issue.
*/
public void addIssue(String description) {
mIssues.add(new Issue(description));
}
/**
* Adds an issue to the layout analysis.
*
* @param node The layout node containing the issue.
* @param description Description of the issue.
*/
public void addIssue(LayoutNode node, String description) {
mIssues.add(new Issue(node, description));
}
/**
* Returns the list of issues found during the analysis.
*
* @return A non-null array of {@link com.android.layoutopt.uix.LayoutAnalysis.Issue}.
*/
public Issue[] getIssues() {
return mIssues.toArray(new Issue[mIssues.size()]);
}
/**
* Indicates whether the layout was analyzed. If this method returns false,
* a probleme occured during the analysis (missing file, invalid document, etc.)
*
* @return True if the layout was analyzed, false otherwise.
*/
public boolean isValid() {
return mAnalyzed;
}
/**
* Validates the analysis. This must be call before this analysis can
* be considered valid.
*/
void validate() {
mAnalyzed = true;
}
/**
* Represents an issue discovered during the analysis process.
* An issue provides a human-readable description as well as optional solutions.
*/
public static class Issue {
private final String mDescription;
private final LayoutNode mNode;
Issue(String description) {
mNode = null;
if (description == null) {
throw new IllegalArgumentException("The description must be non-null");
}
mDescription = description;
}
public Issue(LayoutNode node, String description) {
mNode = node;
if (description == null) {
throw new IllegalArgumentException("The description must be non-null");
}
mDescription = description;
}
/**
* Describes this issue to the user.
*
* @return A String describing the issue, always non-null.
*/
public String getDescription() {
return mDescription;
}
/**
* Returns the start line of this node.
*
* @return The start line or -1 if the line is unknown.
*/
public int getStartLine() {
return mNode == null ? -1 : mNode.getStartLine();
}
/**
* Returns the end line of this node.
*
* @return The end line or -1 if the line is unknown.
*/
public int getEndLine() {
return mNode == null ? -1 : mNode.getEndLine();
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright (C) 2009 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.layoutopt.uix;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.util.Enumeration;
import java.util.List;
import java.util.ArrayList;
import com.android.layoutopt.uix.xml.XmlDocumentBuilder;
import com.android.layoutopt.uix.rules.Rule;
import com.android.layoutopt.uix.rules.GroovyRule;
import com.android.layoutopt.uix.util.IOUtilities;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
/**
* Analysis engine used to discover inefficiencies in Android XML
* layout documents.
*
* Anaylizing an Android XML layout produces a list of explicit messages
* as well as possible solutions.
*/
public class LayoutAnalyzer {
private static final String RULES_PREFIX = "rules/";
private final XmlDocumentBuilder mBuilder = new XmlDocumentBuilder();
private final List<Rule> mRules = new ArrayList<Rule>();
/**
* Creates a new layout analyzer. This constructor takes no argument
* and will use the default options.
*/
public LayoutAnalyzer() {
loadRules();
}
private void loadRules() {
ClassLoader parent = getClass().getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
GroovyShell shell = new GroovyShell(loader);
URL jar = getClass().getProtectionDomain().getCodeSource().getLocation();
ZipFile zip = null;
try {
zip = new ZipFile(new File(jar.toURI()));
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && entry.getName().startsWith(RULES_PREFIX)) {
loadRule(shell, entry.getName(), zip.getInputStream(entry));
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
try {
if (zip != null) zip.close();
} catch (IOException e) {
// Ignore
}
}
}
private void loadRule(GroovyShell shell, String name, InputStream stream) {
try {
Script script = shell.parse(stream);
mRules.add(new GroovyRule(name, script));
} catch (Exception e) {
System.err.println("Could not load rule " + name + ":");
e.printStackTrace();
} finally {
IOUtilities.close(stream);
}
}
public void addRule(Rule rule) {
if (rule == null) {
throw new IllegalArgumentException("A rule must be non-null");
}
mRules.add(rule);
}
/**
* Analyzes the specified file.
*
* @param file The file to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(File file) {
if (file != null && file.exists()) {
InputStream in = null;
try {
in = new FileInputStream(file);
return analyze(file.getPath(), in);
} catch (FileNotFoundException e) {
// Ignore, cannot happen
} finally {
IOUtilities.close(in);
}
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML stream.
*
* @param stream The stream to analyze.
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(InputStream stream) {
return analyze("<unknown>", stream);
}
private LayoutAnalysis analyze(String name, InputStream stream) {
try {
Document document = mBuilder.parse(stream);
return analyze(name, document);
} catch (SAXException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML document.
*
* @param content The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String content) {
return analyze("<unknown>", content);
}
/**
* Analyzes the specified XML document.
*
* @param name The name of the document.
* @param content The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String name, String content) {
try {
Document document = mBuilder.parse(content);
return analyze(name, document);
} catch (SAXException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
return LayoutAnalysis.ERROR;
}
/**
* Analyzes the specified XML document.
*
* @param document The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(Document document) {
return analyze("<unknown>", document);
}
/**
* Analyzes the specified XML document.
*
* @param name The name of the document.
* @param document The XML document to analyze.
*
* @return A {@link com.android.layoutopt.uix.LayoutAnalysis} which
* cannot be null.
*/
public LayoutAnalysis analyze(String name, Document document) {
LayoutAnalysis analysis = new LayoutAnalysis(name);
try {
Element root = document.getDocumentElement();
analyze(analysis, root);
} finally {
analysis.validate();
}
return analysis;
}
private void analyze(LayoutAnalysis analysis, Node node) {
NodeList list = node.getChildNodes();
int count = list.getLength();
// Depth first
for (int i = 0; i < count; i++) {
Node child = list.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
analyze(analysis, child);
}
}
applyRules(analysis, node);
}
private void applyRules(LayoutAnalysis analysis, Node node) {
LayoutNode layoutNode = new LayoutNode(node);
for (Rule rule : mRules) {
rule.run(analysis, layoutNode, node);
}
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (C) 2009 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.layoutopt.uix;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Attr;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import com.android.layoutopt.uix.xml.XmlDocumentBuilder;
/**
* Wrapper class for W3C Node objects. Provides extra utilities specific
* to Android XML layouts.
*/
public class LayoutNode {
private static final String ANDROID_LAYOUT_WIDTH = "android:layout_width";
private static final String ANDROID_LAYOUT_HEIGHT = "android:layout_height";
private static final String VALUE_FILL_PARENT = "fill_parent";
private static final String VALUE_WRAP_CONTENT = "wrap_content";
private Map<String, String> mAttributes;
private final Element mNode;
private LayoutNode[] mChildren;
LayoutNode(Node node) {
if (node == null) throw new IllegalArgumentException("The node cannot be null");
if (node.getNodeType() != Node.ELEMENT_NODE) {
throw new IllegalArgumentException("The node must be an element type");
}
mNode = (Element) node;
}
/**
* Returns the start line of this node.
*
* @return The start line or -1 if the line is unknown.
*/
public int getStartLine() {
final Object data = mNode.getUserData(XmlDocumentBuilder.NODE_START_LINE);
return data == null ? -1 : (Integer) data;
}
/**
* Returns the end line of this node.
*
* @return The end line or -1 if the line is unknown.
*/
public int getEndLine() {
final Object data = mNode.getUserData(XmlDocumentBuilder.NODE_END_LINE);
return data == null ? -1 : (Integer) data;
}
/**
* Returns the wrapped W3C XML node object.
*
* @return An XML node.
*/
public Node getNode() {
return mNode;
}
/**
* Indicates whether the node is of the specified type.
*
* @param name The name of the node.
*
* @return True if this node has the same name as tagName, false otherwise.
*/
public boolean is(String name) {
return mNode.getNodeName().equals(name);
}
/**
* Indicates whether the node has declared the specified attribute.
*
* @param attribute The name of the attribute to check.
*
* @return True if the attribute is specified, false otherwise.
*/
public boolean has(String attribute) {
return mNode.hasAttribute(attribute);
}
/**
* Returns whether this node is the document root.
*
* @return True if the wrapped node is the root of the document,
* false otherwise.
*/
public boolean isRoot() {
return mNode == mNode.getOwnerDocument().getDocumentElement();
}
/**
* Returns whether this node's width is fill_parent.
*/
public boolean isWidthFillParent() {
return mNode.getAttribute(ANDROID_LAYOUT_WIDTH).equals(VALUE_FILL_PARENT);
}
/**
* Returns whether this node's width is wrap_content.
*/
public boolean isWidthWrapContent() {
return mNode.getAttribute(ANDROID_LAYOUT_WIDTH).equals(VALUE_WRAP_CONTENT);
}
/**
* Returns whether this node's height is fill_parent.
*/
public boolean isHeightFillParent() {
return mNode.getAttribute(ANDROID_LAYOUT_HEIGHT).equals(VALUE_FILL_PARENT);
}
/**
* Returns whether this node's height is wrap_content.
*/
public boolean isHeightWrapContent() {
return mNode.getAttribute(ANDROID_LAYOUT_HEIGHT).equals(VALUE_WRAP_CONTENT);
}
/**
* Returns a map of all the attributes declared for this node.
*
* The name of the attributes contains the namespace.
*
* @return A map of [name, value] describing the attributes of this node.
*/
public Map<String, String> getAttributes() {
if (mAttributes == null) {
NamedNodeMap attributes = mNode.getAttributes();
int count = attributes.getLength();
mAttributes = new HashMap<String, String>(count);
for (int i = 0; i < count; i++) {
Node node = attributes.item(i);
Attr attribute = (Attr) node;
mAttributes.put(attribute.getName(), attribute.getValue());
}
}
return mAttributes;
}
/**
* Returns all the children of this node.
*/
public LayoutNode[] getChildren() {
if (mChildren == null) {
NodeList list = mNode.getChildNodes();
int count = list.getLength();
List<LayoutNode> children = new ArrayList<LayoutNode>(count);
for (int i = 0; i < count; i++) {
Node child = list.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
children.add(new LayoutNode(child));
}
}
mChildren = children.toArray(new LayoutNode[children.size()]);
}
return mChildren;
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2009 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.layoutopt.uix.groovy;
import com.android.layoutopt.uix.LayoutAnalysis;
import com.android.layoutopt.uix.LayoutNode;
import java.util.Map;
import groovy.lang.GString;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Support class for Groovy rules. This class adds new Groovy capabilities
* to {@link com.android.layoutopt.uix.LayoutAnalysis} and {@link org.w3c.dom.Node}.
*/
public class LayoutAnalysisCategory {
/**
* xmlNode.isRoot()
*/
public static boolean isRoot(Node node) {
return node.getOwnerDocument().getDocumentElement() == node;
}
/**
* xmlNode.is("tagName")
*/
public static boolean is(Node node, String name) {
return node.getNodeName().equals(name);
}
/**
* xmlNode.depth()
*/
public static int depth(Node node) {
int maxDepth = 0;
NodeList list = node.getChildNodes();
int count = list.getLength();
for (int i = 0; i < count; i++) {
maxDepth = Math.max(maxDepth, depth(list.item(i)));
}
return maxDepth + 1;
}
/**
* analysis << "The issue"
*/
public static LayoutAnalysis leftShift(LayoutAnalysis analysis, GString description) {
analysis.addIssue(description.toString());
return analysis;
}
/**
* analysis << [node: node, description: "The issue"]
*/
public static LayoutAnalysis leftShift(LayoutAnalysis analysis, Map issue) {
analysis.addIssue((LayoutNode) issue.get("node"), issue.get("description").toString());
return analysis;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2009 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.layoutopt.uix.rules;
import groovy.lang.Script;
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.xml.dom.DOMCategory;
import com.android.layoutopt.uix.LayoutAnalysis;
import com.android.layoutopt.uix.LayoutNode;
import com.android.layoutopt.uix.groovy.LayoutAnalysisCategory;
import org.w3c.dom.Node;
import org.codehaus.groovy.runtime.GroovyCategorySupport;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
/**
* Implementation of a rule using a Groovy script.
*/
public class GroovyRule implements Rule {
private final String mName;
private final Script mScript;
private final Binding mBinding;
private final Closure mClosure;
private final List<Class> mCategories;
public GroovyRule(String name, Script script) {
mName = name;
mScript = script;
mBinding = new Binding();
mScript.setBinding(mBinding);
mClosure = new Closure(this) {
@Override
public Object call() {
return mScript.run();
}
};
mCategories = new ArrayList<Class>();
Collections.addAll(mCategories, DOMCategory.class, LayoutAnalysisCategory.class);
}
public String getName() {
return mName;
}
public void run(LayoutAnalysis analysis, LayoutNode node, Node xml) {
mBinding.setVariable("analysis", analysis);
mBinding.setVariable("node", node);
mBinding.setVariable("xml", xml);
GroovyCategorySupport.use(mCategories, mClosure);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2009 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.layoutopt.uix.rules;
import com.android.layoutopt.uix.LayoutAnalysis;
import com.android.layoutopt.uix.LayoutNode;
import org.w3c.dom.Node;
/**
* Interface that define an analysis rule.
*/
public interface Rule {
/**
* Returns the name of the rule.
*
* @return A non-null String.
*/
String getName();
/**
* Runs the rule for the specified node. The rule must add any detected
* issue to the analysis.
*
* @param analysis The resulting analysis.
* @param node The layout node to analyse.
* @param xml The original XML node.
*/
void run(LayoutAnalysis analysis, LayoutNode node, Node xml);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2009 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.layoutopt.uix.util;
import java.io.Closeable;
import java.io.IOException;
/**
* Various utilities related to I/O operations.
*/
public class IOUtilities {
private IOUtilities() {
}
/**
* Safely close a Closeable object, like an InputStream.
*
* @param stream The object to close.
*
* @return True if the object is null or was closed properly,
* false otherwise.
*/
public static boolean close(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright (C) 2009 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.layoutopt.uix.xml;
import com.sun.org.apache.xerces.internal.parsers.DOMParser;
import com.sun.org.apache.xerces.internal.xni.XMLLocator;
import com.sun.org.apache.xerces.internal.xni.NamespaceContext;
import com.sun.org.apache.xerces.internal.xni.Augmentations;
import com.sun.org.apache.xerces.internal.xni.XNIException;
import com.sun.org.apache.xerces.internal.xni.QName;
import com.sun.org.apache.xerces.internal.xni.XMLAttributes;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Node;
import org.w3c.dom.Document;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXException;
import org.xml.sax.InputSource;
import java.io.InputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.util.LinkedList;
/**
* Parses XML documents. This class tries to add meta-data in the resulting DOM
* trees to indicate the start and end line numbers of each node.
*/
public class XmlDocumentBuilder {
/**
* Name of the node user data containing the start line number of the node.
*
* @see Node#getUserData(String)
*/
public static final String NODE_START_LINE = "startLine";
/**
* Name of the node user data containing the end line number of the node.
*
* @see Node#getUserData(String)
*/
public static final String NODE_END_LINE = "endLine";
private final DocumentBuilder mBuilder;
private boolean mHasLineNumbersSupport;
/**
* Creates a new XML document builder.
*/
public XmlDocumentBuilder() {
try {
Class.forName("com.sun.org.apache.xerces.internal.parsers.DOMParser");
mHasLineNumbersSupport = true;
} catch (ClassNotFoundException e) {
// Ignore
}
if (!mHasLineNumbersSupport) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
mBuilder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new IllegalStateException("Could not initialize the XML parser");
}
} else {
mBuilder = null;
}
}
/**
* Indicates whether the XML documents created by this class are annotated
* with line numbers.
*
* @return True if the parsed documents contain line numbers meta-data,
* false otherwise.
*
* @see #NODE_START_LINE
* @see #NODE_END_LINE
*/
public boolean isHasLineNumbersSupport() {
return mHasLineNumbersSupport;
}
public Document parse(InputStream inputStream) throws SAXException, IOException {
if (!mHasLineNumbersSupport) {
return mBuilder.parse(inputStream);
} else {
DOMParser parser = new LineNumberDOMParser();
parser.parse(new InputSource(inputStream));
return parser.getDocument();
}
}
public Document parse(String content) throws SAXException, IOException {
if (!mHasLineNumbersSupport) {
return mBuilder.parse(content);
} else {
DOMParser parser = new LineNumberDOMParser();
parser.parse(content);
return parser.getDocument();
}
}
public Document parse(File file) throws SAXException, IOException {
return parse(new FileInputStream(file));
}
private static class LineNumberDOMParser extends DOMParser {
private static final String FEATURE_NODE_EXPANSION =
"http://apache.org/xml/features/dom/defer-node-expansion";
private static final String CURRENT_NODE =
"http://apache.org/xml/properties/dom/current-element-node";
private XMLLocator mLocator;
private LinkedList<Node> mStack = new LinkedList<Node>();
private LineNumberDOMParser() {
try {
setFeature(FEATURE_NODE_EXPANSION, false);
} catch (SAXNotRecognizedException e) {
e.printStackTrace();
} catch (SAXNotSupportedException e) {
e.printStackTrace();
}
}
@Override
public void startDocument(XMLLocator xmlLocator, String s,
NamespaceContext namespaceContext, Augmentations augmentations)
throws XNIException {
super.startDocument(xmlLocator, s, namespaceContext, augmentations);
mLocator = xmlLocator;
mStack.add(setNodeLineNumber(NODE_START_LINE));
}
private Node setNodeLineNumber(String tag) {
Node node = null;
try {
node = (Node) getProperty(CURRENT_NODE);
} catch (SAXNotRecognizedException e) {
e.printStackTrace();
} catch (SAXNotSupportedException e) {
e.printStackTrace();
}
if (node != null) {
node.setUserData(tag, mLocator.getLineNumber(), null);
}
return node;
}
@Override
public void startElement(QName qName, XMLAttributes xmlAttributes,
Augmentations augmentations) throws XNIException {
super.startElement(qName, xmlAttributes, augmentations);
mStack.add(setNodeLineNumber(NODE_START_LINE));
}
@Override
public void endElement(QName qName, Augmentations augmentations) throws XNIException {
super.endElement(qName, augmentations);
Node node = mStack.removeLast();
if (node != null) {
node.setUserData(NODE_END_LINE, mLocator.getLineNumber(), null);
}
}
}
}

View File

@@ -0,0 +1,16 @@
// Rule: MergeRootFrameLayout
//
// Description: Checks whether the root node of the XML document can be
// replaced with a <merge /> tag.
//
// Conditions:
// - The node is the root of the document
// - The node is a FrameLayout
// - The node is fill_parent in both orientation *or* it has no layout_gravity
// - The node does not have a background nor a foreground
if (xml.isRoot() && xml.is("FrameLayout") && !xml.'@android:background' &&
!xml.'@android:foreground' && ((node.isWidthFillParent() &&
node.isHeightFillParent()) || !xml.'@android:layout_gravity')) {
analysis << [node: node, description: "The root-level <FrameLayout/> can be replaced with <merge/>"]
}

View File

@@ -0,0 +1,10 @@
// Rule: TooManyLevels
//
// Description: Checks whether the layout has too many nested groups.
//
// Conditions:
// - The depth of the layout is > 10
if (xml.isRoot() && (depth = xml.depth()) > 10) {
analysis << "This layout has too many nested layouts: ${depth} levels!"
}

View File

@@ -0,0 +1,10 @@
// Rule: TooManyViews
//
// Description: Checks whether the layout has too many views.
//
// Conditions:
// - The document contains more than 80 views
if (xml.isRoot && (size = xml.'**'.size()) > 80) {
analysis << "This layout has too many views: ${size} views!"
}