/*
|
* Copyright (C) 2011 The Android Open Source Project
|
*
|
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
|
*
|
* 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.
|
*/
|
|
import org.w3c.dom.Document;
|
import org.w3c.dom.Element;
|
import org.w3c.dom.NamedNodeMap;
|
import org.w3c.dom.Node;
|
import org.w3c.dom.NodeList;
|
import org.xml.sax.InputSource;
|
import org.xml.sax.SAXException;
|
|
import java.io.BufferedReader;
|
import java.io.BufferedWriter;
|
import java.io.File;
|
import java.io.FileNotFoundException;
|
import java.io.FileReader;
|
import java.io.FileWriter;
|
import java.io.IOException;
|
import java.io.Reader;
|
import java.io.StringReader;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Map.Entry;
|
import java.util.Set;
|
|
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.ParserConfigurationException;
|
|
/**
|
* Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
|
* attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
|
* listed in the top of the context menu) is determined by running this script on a body
|
* of sample layout code.
|
* <p>
|
* This program takes one or more directory paths, and then it searches all of them recursively
|
* for layout files that are not in folders containing the string "test", and computes and
|
* prints frequency statistics.
|
*/
|
public class Analyzer {
|
/** Number of attributes to print for each view */
|
public static final int ATTRIBUTE_COUNT = 6;
|
/** Separate out any attributes that constitute less than N percent of the total */
|
public static final int THRESHOLD = 10; // percent
|
|
private List<File> mDirectories;
|
private File mCurrentFile;
|
private boolean mListAdvanced;
|
|
/** Map from view id to map from attribute to frequency count */
|
private Map<String, Map<String, Usage>> mFrequencies =
|
new HashMap<String, Map<String, Usage>>(100);
|
|
private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
|
new HashMap<String, Map<String, Usage>>(100);
|
|
private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
|
private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);
|
|
private int mFileVisitCount;
|
private int mLayoutFileCount;
|
private File mXmlMetadataFile;
|
|
private Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced) {
|
mDirectories = directories;
|
mXmlMetadataFile = xmlMetadataFile;
|
mListAdvanced = listAdvanced;
|
}
|
|
public static void main(String[] args) {
|
if (args.length < 1) {
|
System.err.println("Usage: " + Analyzer.class.getSimpleName()
|
+ " <directory1> [directory2 [directory3 ...]]\n");
|
System.err.println("Recursively scans for layouts in the given directory and");
|
System.err.println("computes statistics about attribute frequencies.");
|
System.exit(-1);
|
}
|
|
File metadataFile = null;
|
List<File> directories = new ArrayList<File>();
|
boolean listAdvanced = false;
|
for (int i = 0, n = args.length; i < n; i++) {
|
String arg = args[i];
|
|
if (arg.equals("--list")) {
|
// List ALL encountered attributes
|
listAdvanced = true;
|
continue;
|
}
|
|
// The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
|
// and attempts to insert topAttrs attributes into it (and saves it as same
|
// file +.mod as an extension). This isn't listed on the usage flag because
|
// it's pretty brittle and requires some manual fixups to the file afterwards.
|
if (arg.equals("--metadata")) {
|
i++;
|
File file = new File(args[i]);
|
if (!file.exists()) {
|
System.err.println(file.getName() + " does not exist");
|
System.exit(-5);
|
}
|
if (!file.isFile() || !file.getName().endsWith(".xml")) {
|
System.err.println(file.getName() + " must be an XML file");
|
System.exit(-4);
|
}
|
metadataFile = file;
|
continue;
|
}
|
File directory = new File(arg);
|
if (!directory.exists()) {
|
System.err.println(directory.getName() + " does not exist");
|
System.exit(-2);
|
}
|
|
if (!directory.isDirectory()) {
|
System.err.println(directory.getName() + " is not a directory");
|
System.exit(-3);
|
}
|
|
directories.add(directory);
|
}
|
|
new Analyzer(directories, metadataFile, listAdvanced).analyze();
|
}
|
|
private void analyze() {
|
for (File directory : mDirectories) {
|
scanDirectory(directory);
|
}
|
|
if (mListAdvanced) {
|
listAdvanced();
|
}
|
|
printStatistics();
|
|
if (mXmlMetadataFile != null) {
|
printMergedMetadata();
|
}
|
}
|
|
private void scanDirectory(File directory) {
|
File[] files = directory.listFiles();
|
if (files == null) {
|
return;
|
}
|
|
for (File file : files) {
|
mFileVisitCount++;
|
if (mFileVisitCount % 50000 == 0) {
|
System.out.println("Analyzed " + mFileVisitCount + " files...");
|
}
|
|
if (file.isFile()) {
|
scanFile(file);
|
} else if (file.isDirectory()) {
|
// Skip stuff related to tests
|
if (file.getName().contains("test")) {
|
continue;
|
}
|
|
// Recurse over subdirectories
|
scanDirectory(file);
|
}
|
}
|
}
|
|
private void scanFile(File file) {
|
if (file.getName().endsWith(".xml")) {
|
File parent = file.getParentFile();
|
if (parent.getName().startsWith("layout")) {
|
analyzeLayout(file);
|
}
|
}
|
|
}
|
|
private void analyzeLayout(File file) {
|
mCurrentFile = file;
|
mLayoutFileCount++;
|
Document document = null;
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
InputSource is = new InputSource(new StringReader(readFile(file)));
|
try {
|
factory.setNamespaceAware(true);
|
factory.setValidating(false);
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
document = builder.parse(is);
|
|
analyzeDocument(document);
|
|
} catch (ParserConfigurationException e) {
|
// pass -- ignore files we can't parse
|
} catch (SAXException e) {
|
// pass -- ignore files we can't parse
|
} catch (IOException e) {
|
// pass -- ignore files we can't parse
|
}
|
}
|
|
|
private void analyzeDocument(Document document) {
|
analyzeElement(document.getDocumentElement());
|
}
|
|
private void analyzeElement(Element element) {
|
if (element.getTagName().equals("item")) {
|
// Resource files shouldn't be in the layout/ folder but I came across
|
// some cases
|
System.out.println("Warning: found <item> tag in a layout file in "
|
+ mCurrentFile.getPath());
|
return;
|
}
|
|
countAttributes(element);
|
countLayoutAttributes(element);
|
|
// Recurse over children
|
NodeList childNodes = element.getChildNodes();
|
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
|
Node child = childNodes.item(i);
|
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
analyzeElement((Element) child);
|
}
|
}
|
}
|
|
private void countAttributes(Element element) {
|
String tag = element.getTagName();
|
Map<String, Usage> attributeMap = mFrequencies.get(tag);
|
if (attributeMap == null) {
|
attributeMap = new HashMap<String, Usage>(70);
|
mFrequencies.put(tag, attributeMap);
|
}
|
|
NamedNodeMap attributes = element.getAttributes();
|
for (int i = 0, n = attributes.getLength(); i < n; i++) {
|
Node attribute = attributes.item(i);
|
String name = attribute.getNodeName();
|
|
if (name.startsWith("android:layout_")) {
|
// Skip layout attributes; they are a function of the parent layout that this
|
// view is embedded within, not the view itself.
|
// TODO: Consider whether we should incorporate this info or make statistics
|
// about that as well?
|
continue;
|
}
|
|
if (name.equals("android:id")) {
|
// Skip ids: they are (mostly) unrelated to the view type and the tool
|
// already offers id editing prominently
|
continue;
|
}
|
|
if (name.startsWith("xmlns:")) {
|
// Unrelated to frequency counts
|
continue;
|
}
|
|
Usage usage = attributeMap.get(name);
|
if (usage == null) {
|
usage = new Usage(name);
|
} else {
|
usage.incrementCount();
|
}
|
attributeMap.put(name, usage);
|
}
|
}
|
|
private void countLayoutAttributes(Element element) {
|
String parentTag = element.getParentNode().getNodeName();
|
Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
|
if (attributeMap == null) {
|
attributeMap = new HashMap<String, Usage>(70);
|
mLayoutAttributeFrequencies.put(parentTag, attributeMap);
|
}
|
|
NamedNodeMap attributes = element.getAttributes();
|
for (int i = 0, n = attributes.getLength(); i < n; i++) {
|
Node attribute = attributes.item(i);
|
String name = attribute.getNodeName();
|
|
if (!name.startsWith("android:layout_")) {
|
continue;
|
}
|
|
// Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
|
// very interesting
|
if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
|
continue;
|
}
|
|
Usage usage = attributeMap.get(name);
|
if (usage == null) {
|
usage = new Usage(name);
|
} else {
|
usage.incrementCount();
|
}
|
attributeMap.put(name, usage);
|
}
|
}
|
|
// Copied from AdtUtils
|
private static String readFile(File file) {
|
try {
|
return readFile(new FileReader(file));
|
} catch (FileNotFoundException e) {
|
e.printStackTrace();
|
}
|
|
return null;
|
}
|
|
private static String readFile(Reader inputStream) {
|
BufferedReader reader = null;
|
try {
|
reader = new BufferedReader(inputStream);
|
StringBuilder sb = new StringBuilder(2000);
|
while (true) {
|
int c = reader.read();
|
if (c == -1) {
|
return sb.toString();
|
} else {
|
sb.append((char)c);
|
}
|
}
|
} catch (IOException e) {
|
// pass -- ignore files we can't read
|
} finally {
|
try {
|
if (reader != null) {
|
reader.close();
|
}
|
} catch (IOException e) {
|
e.printStackTrace();
|
}
|
}
|
|
return null;
|
}
|
|
private void printStatistics() {
|
System.out.println("Analyzed " + mLayoutFileCount
|
+ " layouts (in a directory trees containing " + mFileVisitCount + " files)");
|
System.out.println("Top " + ATTRIBUTE_COUNT
|
+ " for each view (excluding layout_ attributes) :");
|
System.out.println("\n");
|
System.out.println(" Rank Count Share Attribute");
|
System.out.println("=========================================================");
|
List<String> views = new ArrayList<String>(mFrequencies.keySet());
|
Collections.sort(views);
|
for (String view : views) {
|
String top = processUageMap(view, mFrequencies.get(view));
|
if (top != null) {
|
mTopAttributes.put(view, top);
|
}
|
}
|
|
System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
|
+ "mandatory layout_width and layout_height):");
|
System.out.println("\n");
|
System.out.println(" Rank Count Share Attribute");
|
System.out.println("=========================================================");
|
views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
|
Collections.sort(views);
|
for (String view : views) {
|
String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
|
if (top != null) {
|
mTopLayoutAttributes.put(view, top);
|
}
|
}
|
}
|
|
private static String processUageMap(String view, Map<String, Usage> map) {
|
if (map == null) {
|
return null;
|
}
|
|
if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
|
// Skip custom views
|
return null;
|
}
|
|
List<Usage> values = new ArrayList<Usage>(map.values());
|
if (values.size() == 0) {
|
return null;
|
}
|
|
Collections.sort(values);
|
int totalCount = 0;
|
for (Usage usage : values) {
|
totalCount += usage.count;
|
}
|
|
System.out.println("\n<" + view + ">:");
|
if (view.equals("#document")) {
|
System.out.println("(Set on root tag, probably intended for included context)");
|
}
|
|
int place = 1;
|
int count = 0;
|
int prevCount = -1;
|
float prevPercentage = 0f;
|
StringBuilder sb = new StringBuilder();
|
for (Usage usage : values) {
|
if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
|
break;
|
}
|
|
float percentage = 100 * usage.count/(float)totalCount;
|
if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
|
System.out.println(" -----Less than 10%-------------------------------------");
|
}
|
System.out.printf(" %1d. %5d %5.1f%% %s\n", place, usage.count,
|
percentage, usage.attribute);
|
|
prevPercentage = percentage;
|
if (prevCount != usage.count) {
|
prevCount = usage.count;
|
place++;
|
}
|
|
if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
|
if (sb.length() > 0) {
|
sb.append(',');
|
}
|
String name = usage.attribute;
|
if (name.startsWith("android:")) {
|
name = name.substring("android:".length());
|
}
|
sb.append(name);
|
}
|
}
|
|
return sb.length() > 0 ? sb.toString() : null;
|
}
|
|
private void printMergedMetadata() {
|
assert mXmlMetadataFile != null;
|
String metadata = readFile(mXmlMetadataFile);
|
if (metadata == null || metadata.length() == 0) {
|
System.err.println("Invalid metadata file");
|
System.exit(-6);
|
}
|
|
System.err.flush();
|
System.out.println("\n\nUpdating layout metadata file...");
|
System.out.flush();
|
|
StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
|
String[] lines = metadata.split("\n");
|
for (int i = 0; i < lines.length; i++) {
|
String line = lines[i];
|
sb.append(line).append('\n');
|
int classIndex = line.indexOf("class=\"");
|
if (classIndex != -1) {
|
int start = classIndex + "class=\"".length();
|
int end = line.indexOf('"', start + 1);
|
if (end != -1) {
|
String view = line.substring(start, end);
|
if (view.startsWith("android.widget.")) {
|
view = view.substring("android.widget.".length());
|
} else if (view.startsWith("android.view.")) {
|
view = view.substring("android.view.".length());
|
} else if (view.startsWith("android.webkit.")) {
|
view = view.substring("android.webkit.".length());
|
}
|
String top = mTopAttributes.get(view);
|
if (top == null) {
|
System.err.println("Warning: No frequency data for view " + view);
|
} else {
|
sb.append(line.substring(0, classIndex)); // Indentation
|
|
sb.append("topAttrs=\"");
|
sb.append(top);
|
sb.append("\"\n");
|
}
|
|
top = mTopLayoutAttributes.get(view);
|
if (top != null) {
|
// It's a layout attribute
|
sb.append(line.substring(0, classIndex)); // Indentation
|
|
sb.append("topLayoutAttrs=\"");
|
sb.append(top);
|
sb.append("\"\n");
|
}
|
}
|
}
|
}
|
|
System.out.println("\nTop attributes:");
|
System.out.println("--------------------------");
|
List<String> views = new ArrayList<String>(mTopAttributes.keySet());
|
Collections.sort(views);
|
for (String view : views) {
|
String top = mTopAttributes.get(view);
|
System.out.println(view + ": " + top);
|
}
|
|
System.out.println("\nTop layout attributes:");
|
System.out.println("--------------------------");
|
views = new ArrayList<String>(mTopLayoutAttributes.keySet());
|
Collections.sort(views);
|
for (String view : views) {
|
String top = mTopLayoutAttributes.get(view);
|
System.out.println(view + ": " + top);
|
}
|
|
System.out.println("\nModified XML metadata file:\n");
|
String newContent = sb.toString();
|
File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
|
if (output.exists()) {
|
output.delete();
|
}
|
try {
|
BufferedWriter writer = new BufferedWriter(new FileWriter(output));
|
writer.write(newContent);
|
writer.close();
|
} catch (IOException e) {
|
e.printStackTrace();
|
}
|
System.out.println("Done - wrote " + output.getPath());
|
}
|
|
//private File mPublicFile = new File(location, "data/res/values/public.xml");
|
private File mPublicFile = new File("/Volumes/AndroidWork/git/frameworks/base/core/res/res/values/public.xml");
|
|
private void listAdvanced() {
|
Set<String> keys = new HashSet<String>(1000);
|
|
// Merged usages across view types
|
Map<String, Usage> mergedUsages = new HashMap<String, Usage>(100);
|
|
for (Entry<String,Map<String,Usage>> entry : mFrequencies.entrySet()) {
|
String view = entry.getKey();
|
if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
|
// Skip custom views etc
|
continue;
|
}
|
Map<String, Usage> map = entry.getValue();
|
for (Usage usage : map.values()) {
|
// if (usage.count == 1) {
|
// System.out.println("Only found *one* usage of " + usage.attribute);
|
// }
|
// if (usage.count < 4) {
|
// System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
|
// }
|
|
String attribute = usage.attribute;
|
int index = attribute.indexOf(':');
|
if (index == -1 || attribute.startsWith("android:")) {
|
Usage merged = mergedUsages.get(attribute);
|
if (merged == null) {
|
merged = new Usage(attribute);
|
merged.count = usage.count;
|
mergedUsages.put(attribute, merged);
|
} else {
|
merged.count += usage.count;
|
}
|
}
|
}
|
}
|
|
for (Usage usage : mergedUsages.values()) {
|
String attribute = usage.attribute;
|
if (usage.count < 4) {
|
System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
|
continue;
|
}
|
int index = attribute.indexOf(':');
|
if (index != -1) {
|
attribute = attribute.substring(index + 1); // +1: skip ':'
|
}
|
keys.add(attribute);
|
}
|
|
List<String> sorted = new ArrayList<String>(keys);
|
Collections.sort(sorted);
|
System.out.println("\nEncountered Attributes");
|
System.out.println("-----------------------------");
|
for (String attribute : sorted) {
|
System.out.println(attribute);
|
}
|
|
System.out.println();
|
}
|
|
private static class Usage implements Comparable<Usage> {
|
public String attribute;
|
public int count;
|
|
|
public Usage(String attribute) {
|
super();
|
this.attribute = attribute;
|
|
count = 1;
|
}
|
|
public void incrementCount() {
|
count++;
|
}
|
|
@Override
|
public int compareTo(Usage o) {
|
// Sort by decreasing frequency, then sort alphabetically
|
int frequencyDelta = o.count - count;
|
if (frequencyDelta != 0) {
|
return frequencyDelta;
|
} else {
|
return attribute.compareTo(o.attribute);
|
}
|
}
|
|
@Override
|
public String toString() {
|
return attribute + ": " + count;
|
}
|
|
@Override
|
public int hashCode() {
|
final int prime = 31;
|
int result = 1;
|
result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
|
return result;
|
}
|
|
@Override
|
public boolean equals(Object obj) {
|
if (this == obj)
|
return true;
|
if (obj == null)
|
return false;
|
if (getClass() != obj.getClass())
|
return false;
|
Usage other = (Usage) obj;
|
if (attribute == null) {
|
if (other.attribute != null)
|
return false;
|
} else if (!attribute.equals(other.attribute))
|
return false;
|
return true;
|
}
|
}
|
}
|