aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndroid (Google) Code Review <android-gerrit@google.com>2009-10-05 07:13:21 -0400
committerAndroid (Google) Code Review <android-gerrit@google.com>2009-10-05 07:13:21 -0400
commit0cbd47a86156fbfc411c5e0990695ffd6568f47f (patch)
treee794ecdddfab4ebf7f56cca3c82a423e93a54cd7
parent9753171cffd39fbc46299c23aac94e963652f892 (diff)
parentc49990616a2c36e5a8d221712ae5ef94a06be793 (diff)
downloadsdk-0cbd47a86156fbfc411c5e0990695ffd6568f47f.zip
sdk-0cbd47a86156fbfc411c5e0990695ffd6568f47f.tar.gz
sdk-0cbd47a86156fbfc411c5e0990695ffd6568f47f.tar.bz2
Merge change I8e4697e1 into eclair
* changes: New layout optimization tool. Run layoutopt on the command line.
-rw-r--r--layoutopt/Android.mk5
-rw-r--r--layoutopt/MODULE_LICENSE_APACHE20
-rw-r--r--layoutopt/app/Android.mk5
-rw-r--r--layoutopt/app/README3
-rw-r--r--layoutopt/app/etc/Android.mk8
-rwxr-xr-xlayoutopt/app/etc/layoutopt63
-rwxr-xr-xlayoutopt/app/etc/layoutopt.bat48
-rw-r--r--layoutopt/app/etc/manifest.txt2
-rw-r--r--layoutopt/app/src/Android.mk15
-rw-r--r--layoutopt/app/src/com/android/layoutopt/cli/Main.java123
-rw-r--r--layoutopt/libs/Android.mk5
-rw-r--r--layoutopt/libs/uix/Android.mk4
-rw-r--r--layoutopt/libs/uix/src/Android.mk13
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java163
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java250
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java184
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java77
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java69
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java43
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java47
-rw-r--r--layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java189
-rw-r--r--layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule16
-rw-r--r--layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule10
-rw-r--r--layoutopt/libs/uix/src/resources/rules/TooManyViews.rule10
-rw-r--r--layoutopt/samples/simple.xml7
-rw-r--r--layoutopt/samples/too_deep.xml85
-rw-r--r--layoutopt/samples/too_many.xml413
27 files changed, 1857 insertions, 0 deletions
diff --git a/layoutopt/Android.mk b/layoutopt/Android.mk
new file mode 100644
index 0000000..43b2dcf
--- /dev/null
+++ b/layoutopt/Android.mk
@@ -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
diff --git a/layoutopt/MODULE_LICENSE_APACHE2 b/layoutopt/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/layoutopt/MODULE_LICENSE_APACHE2
diff --git a/layoutopt/app/Android.mk b/layoutopt/app/Android.mk
new file mode 100644
index 0000000..3fae340
--- /dev/null
+++ b/layoutopt/app/Android.mk
@@ -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
diff --git a/layoutopt/app/README b/layoutopt/app/README
new file mode 100644
index 0000000..c118022
--- /dev/null
+++ b/layoutopt/app/README
@@ -0,0 +1,3 @@
+Layout optimizer.
+
+Simple command line front end for the uix library. \ No newline at end of file
diff --git a/layoutopt/app/etc/Android.mk b/layoutopt/app/etc/Android.mk
new file mode 100644
index 0000000..ae08f9d
--- /dev/null
+++ b/layoutopt/app/etc/Android.mk
@@ -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)
+
diff --git a/layoutopt/app/etc/layoutopt b/layoutopt/app/etc/layoutopt
new file mode 100755
index 0000000..22ebb67
--- /dev/null
+++ b/layoutopt/app/etc/layoutopt
@@ -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" "$@"
diff --git a/layoutopt/app/etc/layoutopt.bat b/layoutopt/app/etc/layoutopt.bat
new file mode 100755
index 0000000..e1eb7b3
--- /dev/null
+++ b/layoutopt/app/etc/layoutopt.bat
@@ -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% %*
+
diff --git a/layoutopt/app/etc/manifest.txt b/layoutopt/app/etc/manifest.txt
new file mode 100644
index 0000000..5d0afdf
--- /dev/null
+++ b/layoutopt/app/etc/manifest.txt
@@ -0,0 +1,2 @@
+Main-Class: com.android.layoutopt.cli.Main
+Class-Path: groovy-all-1.6.5.jar
diff --git a/layoutopt/app/src/Android.mk b/layoutopt/app/src/Android.mk
new file mode 100644
index 0000000..131addd
--- /dev/null
+++ b/layoutopt/app/src/Android.mk
@@ -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)
+
diff --git a/layoutopt/app/src/com/android/layoutopt/cli/Main.java b/layoutopt/app/src/com/android/layoutopt/cli/Main.java
new file mode 100644
index 0000000..9b3b9aa
--- /dev/null
+++ b/layoutopt/app/src/com/android/layoutopt/cli/Main.java
@@ -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;
+ }
+}
diff --git a/layoutopt/libs/Android.mk b/layoutopt/libs/Android.mk
new file mode 100644
index 0000000..6af13a8
--- /dev/null
+++ b/layoutopt/libs/Android.mk
@@ -0,0 +1,5 @@
+# Copyright 2009 The Android Open Source Project
+#
+DDMSLIBS_LOCAL_DIR := $(call my-dir)
+include $(DDMSLIBS_LOCAL_DIR)/uix/Android.mk
+
diff --git a/layoutopt/libs/uix/Android.mk b/layoutopt/libs/uix/Android.mk
new file mode 100644
index 0000000..8344e57
--- /dev/null
+++ b/layoutopt/libs/uix/Android.mk
@@ -0,0 +1,4 @@
+# Copyright 2009 The Android Open Source Project
+#
+UIX_LOCAL_DIR := $(call my-dir)
+include $(UIX_LOCAL_DIR)/src/Android.mk
diff --git a/layoutopt/libs/uix/src/Android.mk b/layoutopt/libs/uix/src/Android.mk
new file mode 100644
index 0000000..65abcbc
--- /dev/null
+++ b/layoutopt/libs/uix/src/Android.mk
@@ -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)
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java
new file mode 100644
index 0000000..852fd60
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java
@@ -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();
+ }
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java
new file mode 100644
index 0000000..015bccc
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java
@@ -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);
+ }
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java
new file mode 100644
index 0000000..6f0a078
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java
@@ -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;
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java
new file mode 100644
index 0000000..6f84d5c
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java
@@ -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;
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java
new file mode 100644
index 0000000..85d60ef
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java
@@ -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);
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java
new file mode 100644
index 0000000..3112499
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java
@@ -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);
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java
new file mode 100644
index 0000000..69ac30f
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java
@@ -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;
+ }
+}
diff --git a/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java b/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java
new file mode 100644
index 0000000..f5779ff
--- /dev/null
+++ b/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule b/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule
new file mode 100644
index 0000000..c7b2f1a
--- /dev/null
+++ b/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule
@@ -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/>"]
+}
diff --git a/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule b/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule
new file mode 100644
index 0000000..bd8d558
--- /dev/null
+++ b/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule
@@ -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!"
+}
diff --git a/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule b/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule
new file mode 100644
index 0000000..466382e
--- /dev/null
+++ b/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule
@@ -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!"
+}
diff --git a/layoutopt/samples/simple.xml b/layoutopt/samples/simple.xml
new file mode 100644
index 0000000..1fd36e2
--- /dev/null
+++ b/layoutopt/samples/simple.xml
@@ -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" />
diff --git a/layoutopt/samples/too_deep.xml b/layoutopt/samples/too_deep.xml
new file mode 100644
index 0000000..7317362
--- /dev/null
+++ b/layoutopt/samples/too_deep.xml
@@ -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>
diff --git a/layoutopt/samples/too_many.xml b/layoutopt/samples/too_many.xml
new file mode 100644
index 0000000..41c18ff
--- /dev/null
+++ b/layoutopt/samples/too_many.xml
@@ -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>