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,5 @@
# Copyright 2009 The Android Open Source Project
#
LAYOUTOPT_LOCAL_DIR := $(call my-dir)
include $(LAYOUTOPT_LOCAL_DIR)/libs/Android.mk
include $(LAYOUTOPT_LOCAL_DIR)/app/Android.mk

View File

View File

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

View File

@@ -0,0 +1,3 @@
Layout optimizer.
Simple command line front end for the uix library.

View File

@@ -0,0 +1,8 @@
# Copyright 2009 The Android Open Source Project
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PREBUILT_EXECUTABLES := layoutopt
include $(BUILD_HOST_PREBUILT)

View File

@@ -0,0 +1,63 @@
#!/bin/sh
# Copyright 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.
# Set up prog to be the path of this script, including following symlinks,
# and set up progdir to be the fully-qualified pathname of its directory.
prog="$0"
while [ -h "${prog}" ]; do
newProg=`/bin/ls -ld "${prog}"`
newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
if expr "x${newProg}" : 'x/' >/dev/null; then
prog="${newProg}"
else
progdir=`dirname "${prog}"`
prog="${progdir}/${newProg}"
fi
done
oldwd=`pwd`
progdir=`dirname "${prog}"`
cd "${progdir}"
progdir=`pwd`
prog="${progdir}"/`basename "${prog}"`
cd "${oldwd}"
jarfile=layoutopt.jar
frameworkdir="$progdir"
if [ ! -r "$frameworkdir/$jarfile" ]
then
frameworkdir=`dirname "$progdir"`/tools/lib
libdir=`dirname "$progdir"`/tools/lib
fi
if [ ! -r "$frameworkdir/$jarfile" ]
then
frameworkdir=`dirname "$progdir"`/framework
libdir=`dirname "$progdir"`/lib
fi
if [ ! -r "$frameworkdir/$jarfile" ]
then
echo `basename "$prog"`": can't find $jarfile"
exit 1
fi
if [ "$OSTYPE" = "cygwin" ] ; then
jarpath=`cygpath -w "$frameworkdir/$jarfile"`
progdir=`cygpath -w "$progdir"`
else
jarpath="$frameworkdir/$jarfile"
fi
# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
# might need more memory, e.g. -Xmx128M
exec java -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@"

View File

@@ -0,0 +1,48 @@
@echo off
rem Copyright (C) 2009 The Android Open Source Project
rem
rem Licensed under the Apache License, Version 2.0 (the "License");
rem you may not use this file except in compliance with the License.
rem You may obtain a copy of the License at
rem
rem http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.
rem don't modify the caller's environment
setlocal
rem Set up prog to be the path of this script, including following symlinks,
rem and set up progdir to be the fully-qualified pathname of its directory.
set prog=%~f0
rem Change current directory and drive to where the script is, to avoid
rem issues with directories containing whitespaces.
cd /d %~dp0
set jarfile=layoutopt.jar
set frameworkdir=
if exist %frameworkdir%%jarfile% goto JarFileOk
set frameworkdir=lib\
if exist %frameworkdir%%jarfile% goto JarFileOk
set frameworkdir=..\framework\
:JarFileOk
if debug NEQ "%1" goto NoDebug
set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
shift 1
:NoDebug
set jarpath=%frameworkdir%%jarfile%
set javaextdirs=%swt_path%;%frameworkdir%
call java %java_debug% -Djava.ext.dirs=%javaextdirs% %jarpath% %*

View File

@@ -0,0 +1,2 @@
Main-Class: com.android.layoutopt.cli.Main
Class-Path: groovy-all-1.6.5.jar

View File

@@ -0,0 +1,15 @@
# 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_JAR_MANIFEST := ../etc/manifest.txt
LOCAL_JAVA_LIBRARIES := \
uix
LOCAL_MODULE := layoutopt
include $(BUILD_HOST_JAVA_LIBRARY)

View File

@@ -0,0 +1,123 @@
/*
* 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.cli;
import com.android.layoutopt.uix.LayoutAnalyzer;
import com.android.layoutopt.uix.LayoutAnalysis;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
/**
* Command line utility for the uix library.
*
* This is a simple CLI front-end for the uix library, used to
* analyze and optimize Android layout files.
*/
public class Main {
/**
* Main entry point of the application.
*
* @param args One mandatory parameter, a path (absolute or relative)
* to an Android XML layout file
*/
public static void main(String[] args) {
Parameters p = checkParameters(args);
if (!p.valid) {
displayHelpMessage();
exit();
}
analyzeFiles(p.files);
}
private static void analyzeFiles(File[] files) {
LayoutAnalyzer analyzer = new LayoutAnalyzer();
for (File file : files) {
if (file.isFile()) {
analyze(analyzer, file);
} else {
analyzeFiles(file.listFiles());
}
}
}
private static void analyze(LayoutAnalyzer analyzer, File file) {
LayoutAnalysis analysis = analyzer.analyze(file);
System.out.println(analysis.getName());
for (LayoutAnalysis.Issue issue : analysis.getIssues()) {
System.out.print(String.format("\t%d:%d ", issue.getStartLine(), issue.getEndLine()));
System.out.println(issue.getDescription());
}
}
/**
* Exits the tool.
*/
private static void exit() {
System.exit(0);
}
/**
* Displays this tool's help message on the standard output.
*/
private static void displayHelpMessage() {
System.out.println("usage: layoutopt <directories/files to analyze>");
}
/**
* Builds a valid Parameters object. Parses the paramters if necessary
* and checks for errors.
*
* @param args The parameters passed from the CLI.
*/
private static Parameters checkParameters(String[] args) {
Parameters p = new Parameters();
if (args.length < 1) {
p.valid = false;
} else {
List<File> files = new ArrayList<File>();
for (String path : args) {
File file = new File(path);
if (file.exists()) {
files.add(file);
}
}
p.files = files.toArray(new File[files.size()]);
p.valid = true;
}
return p;
}
/**
* Parameters parsed from the CLI.
*/
private static class Parameters {
/**
* True if this list of parameters is valid, false otherwise.
*/
boolean valid;
/**
* Paths (absolute or relative) to the files to be analyzed.
*/
File[] files;
}
}

View File

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

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!"
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,413 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ok" />
</LinearLayout>
</LinearLayout>
</FrameLayout>