| /* |
| * 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; |
| } |
| } |
| } |