New layout optimization tool. Run layoutopt on the command line.
Change-Id: I8e4697e19ca8a203dc8a41b464f7cb46d52184b0
This commit is contained in:
4
tools/layoutopt/libs/uix/Android.mk
Normal file
4
tools/layoutopt/libs/uix/Android.mk
Normal 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
|
||||
13
tools/layoutopt/libs/uix/src/Android.mk
Normal file
13
tools/layoutopt/libs/uix/src/Android.mk
Normal 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)
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/>"]
|
||||
}
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -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!"
|
||||
}
|
||||
Reference in New Issue
Block a user