OSDN Git Service

New layout optimization tool. Run layoutopt on the command line.
authorRomain Guy <romainguy@android.com>
Mon, 5 Oct 2009 09:21:30 +0000 (02:21 -0700)
committerRomain Guy <romainguy@android.com>
Mon, 5 Oct 2009 10:48:33 +0000 (03:48 -0700)
Change-Id: I8e4697e19ca8a203dc8a41b464f7cb46d52184b0

27 files changed:
tools/layoutopt/Android.mk [new file with mode: 0644]
tools/layoutopt/MODULE_LICENSE_APACHE2 [new file with mode: 0644]
tools/layoutopt/app/Android.mk [new file with mode: 0644]
tools/layoutopt/app/README [new file with mode: 0644]
tools/layoutopt/app/etc/Android.mk [new file with mode: 0644]
tools/layoutopt/app/etc/layoutopt [new file with mode: 0755]
tools/layoutopt/app/etc/layoutopt.bat [new file with mode: 0755]
tools/layoutopt/app/etc/manifest.txt [new file with mode: 0644]
tools/layoutopt/app/src/Android.mk [new file with mode: 0644]
tools/layoutopt/app/src/com/android/layoutopt/cli/Main.java [new file with mode: 0644]
tools/layoutopt/libs/Android.mk [new file with mode: 0644]
tools/layoutopt/libs/uix/Android.mk [new file with mode: 0644]
tools/layoutopt/libs/uix/src/Android.mk [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java [new file with mode: 0644]
tools/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule [new file with mode: 0644]
tools/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule [new file with mode: 0644]
tools/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule [new file with mode: 0644]
tools/layoutopt/samples/simple.xml [new file with mode: 0644]
tools/layoutopt/samples/too_deep.xml [new file with mode: 0644]
tools/layoutopt/samples/too_many.xml [new file with mode: 0644]

diff --git a/tools/layoutopt/Android.mk b/tools/layoutopt/Android.mk
new file mode 100644 (file)
index 0000000..43b2dcf
--- /dev/null
@@ -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/tools/layoutopt/MODULE_LICENSE_APACHE2 b/tools/layoutopt/MODULE_LICENSE_APACHE2
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tools/layoutopt/app/Android.mk b/tools/layoutopt/app/Android.mk
new file mode 100644 (file)
index 0000000..3fae340
--- /dev/null
@@ -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/tools/layoutopt/app/README b/tools/layoutopt/app/README
new file mode 100644 (file)
index 0000000..c118022
--- /dev/null
@@ -0,0 +1,3 @@
+Layout optimizer.
+
+Simple command line front end for the uix library.
\ No newline at end of file
diff --git a/tools/layoutopt/app/etc/Android.mk b/tools/layoutopt/app/etc/Android.mk
new file mode 100644 (file)
index 0000000..ae08f9d
--- /dev/null
@@ -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/tools/layoutopt/app/etc/layoutopt b/tools/layoutopt/app/etc/layoutopt
new file mode 100755 (executable)
index 0000000..22ebb67
--- /dev/null
@@ -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/tools/layoutopt/app/etc/layoutopt.bat b/tools/layoutopt/app/etc/layoutopt.bat
new file mode 100755 (executable)
index 0000000..e1eb7b3
--- /dev/null
@@ -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/tools/layoutopt/app/etc/manifest.txt b/tools/layoutopt/app/etc/manifest.txt
new file mode 100644 (file)
index 0000000..5d0afdf
--- /dev/null
@@ -0,0 +1,2 @@
+Main-Class: com.android.layoutopt.cli.Main
+Class-Path: groovy-all-1.6.5.jar
diff --git a/tools/layoutopt/app/src/Android.mk b/tools/layoutopt/app/src/Android.mk
new file mode 100644 (file)
index 0000000..131addd
--- /dev/null
@@ -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/tools/layoutopt/app/src/com/android/layoutopt/cli/Main.java b/tools/layoutopt/app/src/com/android/layoutopt/cli/Main.java
new file mode 100644 (file)
index 0000000..9b3b9aa
--- /dev/null
@@ -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/tools/layoutopt/libs/Android.mk b/tools/layoutopt/libs/Android.mk
new file mode 100644 (file)
index 0000000..6af13a8
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/Android.mk b/tools/layoutopt/libs/uix/Android.mk
new file mode 100644 (file)
index 0000000..8344e57
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/Android.mk b/tools/layoutopt/libs/uix/src/Android.mk
new file mode 100644 (file)
index 0000000..65abcbc
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java
new file mode 100644 (file)
index 0000000..852fd60
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java
new file mode 100644 (file)
index 0000000..015bccc
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutNode.java
new file mode 100644 (file)
index 0000000..6f0a078
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java
new file mode 100644 (file)
index 0000000..6f84d5c
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java
new file mode 100644 (file)
index 0000000..85d60ef
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java
new file mode 100644 (file)
index 0000000..3112499
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java
new file mode 100644 (file)
index 0000000..69ac30f
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java b/tools/layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java
new file mode 100644 (file)
index 0000000..f5779ff
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule b/tools/layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule
new file mode 100644 (file)
index 0000000..c7b2f1a
--- /dev/null
@@ -0,0 +1,16 @@
+// Rule: MergeRootFrameLayout
+//
+// Description: Checks whether the root node of the XML document can be
+//              replaced with a <merge /> tag.
+//
+// Conditions:
+// - The node is the root of the document
+// - The node is a FrameLayout
+// - The node is fill_parent in both orientation *or* it has no layout_gravity
+// - The node does not have a background nor a foreground
+
+if (xml.isRoot() && xml.is("FrameLayout") && !xml.'@android:background' &&
+        !xml.'@android:foreground' && ((node.isWidthFillParent() &&
+                node.isHeightFillParent()) || !xml.'@android:layout_gravity')) {
+    analysis << [node: node, description: "The root-level <FrameLayout/> can be replaced with <merge/>"]
+}
diff --git a/tools/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule b/tools/layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule
new file mode 100644 (file)
index 0000000..bd8d558
--- /dev/null
@@ -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/tools/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule b/tools/layoutopt/libs/uix/src/resources/rules/TooManyViews.rule
new file mode 100644 (file)
index 0000000..466382e
--- /dev/null
@@ -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/tools/layoutopt/samples/simple.xml b/tools/layoutopt/samples/simple.xml
new file mode 100644 (file)
index 0000000..1fd36e2
--- /dev/null
@@ -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/tools/layoutopt/samples/too_deep.xml b/tools/layoutopt/samples/too_deep.xml
new file mode 100644 (file)
index 0000000..7317362
--- /dev/null
@@ -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/tools/layoutopt/samples/too_many.xml b/tools/layoutopt/samples/too_many.xml
new file mode 100644 (file)
index 0000000..41c18ff
--- /dev/null
@@ -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>