diff options
author | Romain Guy <romainguy@android.com> | 2009-10-05 02:21:30 -0700 |
---|---|---|
committer | Romain Guy <romainguy@android.com> | 2009-10-05 03:48:33 -0700 |
commit | c49990616a2c36e5a8d221712ae5ef94a06be793 (patch) | |
tree | 7fb6332667c7e69d5da25ca8c56444768f88889f /layoutopt | |
parent | 029f98d69605e322c8f8592ab59d2f89814872c8 (diff) | |
download | sdk-c49990616a2c36e5a8d221712ae5ef94a06be793.zip sdk-c49990616a2c36e5a8d221712ae5ef94a06be793.tar.gz sdk-c49990616a2c36e5a8d221712ae5ef94a06be793.tar.bz2 |
New layout optimization tool. Run layoutopt on the command line.
Change-Id: I8e4697e19ca8a203dc8a41b464f7cb46d52184b0
Diffstat (limited to 'layoutopt')
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> |