/* * Copyright (C) 2012 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.motorolamobility.preflighting.core.internal.utils; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.eclipse.core.runtime.Path; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.motorolamobility.preflighting.core.exception.PreflightingToolException; import com.motorolamobility.preflighting.core.i18n.PreflightingCoreNLS; import com.motorolamobility.preflighting.core.logging.PreflightingLogger; public final class AaptUtils { private static final String ANDROID_SMALL_SCREENS = "android:smallScreens"; public static final String APP_VALIDATOR_TEMP_DIR = "MotodevAppValidator"; private static final String JAVA_TEMP_DIR_PROPERTY = "java.io.tmpdir"; private static final String TEMP_DIR_PATH = System.getProperty(JAVA_TEMP_DIR_PROPERTY); // Temp folder used for APK extracting public static final File tmpAppValidatorFolder = new File(TEMP_DIR_PATH, APP_VALIDATOR_TEMP_DIR); private static final String CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ private static final String RESOURCES_ARSC = "resources.arsc"; //$NON-NLS-1$ private static final String XML_FILE = "xml"; //$NON-NLS-1$ private static final String ELEMENT_NODE = "E:"; //$NON-NLS-1$ private static final String ATTRIBUTE_NODE = "A:"; //$NON-NLS-1$ private static final String NAMESPACE_XMLNS = "N:"; //$NON-NLS-1$ private static HashMap<String, HashMap<String, String>> resourceValues = new HashMap<String, HashMap<String, String>>(); private static Map<String, String> navigationMap; private static Map<String, String> nightMap; private static Map<String, String> keyboardMap; private static Map<String, String> touchMap; private static Map<String, String> densityMap; private static Map<String, String> sizeMap; private static Map<String, String> orientationMap; private static Map<String, String> longMap; private static Map<String, String> navHiddenMap; private static Map<String, String> keyHiddenMap; private static Map<String, String> typeMap; public static final String APK_EXTENSION = ".apk"; public static final String ZIP_EXTENSION = ".zip"; private static Map<Pattern, Map<String, String>> localizationAttributesMap2 = new HashMap<Pattern, Map<String, String>>(); private static Map<Pattern, String> localizationAttributesMap1 = new HashMap<Pattern, String>(); private static Pattern[] patternArray = new Pattern[18]; static { // Initialize specific maps navigationMap = new HashMap<String, String>(); navigationMap.put("1", "nonav"); navigationMap.put("2", "dpad"); navigationMap.put("3", "trackball"); navigationMap.put("4", "wheel"); nightMap = new HashMap<String, String>(); nightMap.put("16", "notnight"); nightMap.put("32", "night"); keyboardMap = new HashMap<String, String>(); keyboardMap.put("1", "nokeys"); keyboardMap.put("2", "qwerty"); keyboardMap.put("3", "12key"); touchMap = new HashMap<String, String>(); touchMap.put("1", "notouch"); touchMap.put("2", "stylus"); touchMap.put("3", "finger"); densityMap = new HashMap<String, String>(); densityMap.put("no", "nodpi"); densityMap.put("120", "ldpi"); densityMap.put("160", "mdpi"); densityMap.put("240", "hdpi"); sizeMap = new HashMap<String, String>(); sizeMap.put("1", "small"); sizeMap.put("2", "normal"); sizeMap.put("3", "large"); orientationMap = new HashMap<String, String>(); orientationMap.put("1", "port"); orientationMap.put("2", "land"); orientationMap.put("3", "square"); longMap = new HashMap<String, String>(); longMap.put("16", "notlong"); longMap.put("32", "long"); navHiddenMap = new HashMap<String, String>(); navHiddenMap.put("8", "navhidden"); navHiddenMap.put("4", "navexposed"); keyHiddenMap = new HashMap<String, String>(); keyHiddenMap.put("1", "keyexposed"); keyHiddenMap.put("2", "keyhidden"); typeMap = new HashMap<String, String>(); typeMap.put("3", "car"); // initialize localization folder attributes // the order of patternArray elements are extremely important, do not // modify it localizationAttributesMap1.put(patternArray[0] = Pattern.compile("mcc=[0-9]+"), "mcc"); localizationAttributesMap1.put(patternArray[1] = Pattern.compile("mnc=[0-9]+"), "mnc"); localizationAttributesMap1.put(patternArray[2] = Pattern.compile("lang=[a-z]+"), ""); localizationAttributesMap1.put(patternArray[3] = Pattern.compile("cnt=[A-Z]+"), "r"); localizationAttributesMap2.put(patternArray[4] = Pattern.compile("sz=[0-9]"), sizeMap); localizationAttributesMap2.put(patternArray[5] = Pattern.compile("lng=[0-9]+"), longMap); localizationAttributesMap2.put(patternArray[6] = Pattern.compile("orient=[0-9]"), orientationMap); localizationAttributesMap2.put(patternArray[7] = Pattern.compile("type=[0-9]"), typeMap); localizationAttributesMap2.put(patternArray[8] = Pattern.compile("night=[0-9]+"), nightMap); localizationAttributesMap2.put(patternArray[9] = Pattern.compile("density=[0-9]+"), densityMap); localizationAttributesMap2.put(patternArray[10] = Pattern.compile("touch=[0-9]"), touchMap); localizationAttributesMap2.put(patternArray[11] = Pattern.compile("keyhid=[0-9]"), keyHiddenMap); localizationAttributesMap2 .put(patternArray[12] = Pattern.compile("kbd=[0-9]"), keyboardMap); localizationAttributesMap2.put(patternArray[13] = Pattern.compile("navhid=[0-9]"), navHiddenMap); localizationAttributesMap2.put(patternArray[14] = Pattern.compile("nav=[0-9]"), navigationMap); localizationAttributesMap1.put(patternArray[15] = Pattern.compile("\\sw=[0-9]+"), ""); localizationAttributesMap1.put(patternArray[16] = Pattern.compile("\\sh=[0-9]+"), "x"); localizationAttributesMap1.put(patternArray[17] = Pattern.compile("sdk=[0-9]+"), "v"); } /** * Cleans resources maps among executions for applications */ public static void cleanApplicationResourceValues() { resourceValues.clear(); } public static void extractFilesFromAPK(File apkFile, String sdkPath, File tmpProjectFile) throws PreflightingToolException { if ((tmpProjectFile != null) && tmpProjectFile.exists() && tmpProjectFile.canWrite()) { ZipInputStream apkInputStream = null; FileOutputStream apkOutputStream = null; try { // create the buffer and the the zip stream byte[] buf = new byte[1024]; apkInputStream = new ZipInputStream(new FileInputStream(apkFile.getAbsolutePath())); ZipEntry apkZipEntry = null; try { apkZipEntry = apkInputStream.getNextEntry(); } catch (Exception e) { PreflightingLogger.error(ApkUtils.class, "It was not possible to read the android package.", e); //$NON-NLS-1$ } if (apkZipEntry == null) { throw new IOException("Invalid APK file."); } String folders = null; File fileToCreate = null; // create res folder fileToCreate = new File(tmpProjectFile, "res"); if (!fileToCreate.exists()) { fileToCreate.mkdirs(); } // create the resources file Map<File, Document> languageMap = retrieveLocalizationStringsMapFromAPK(sdkPath, apkFile.getAbsolutePath(), "ProjectResourcesValues.xml"); createLocalizationFilesFromMap(languageMap, fileToCreate); // iterates through each entry to be extracted of the android // package while (apkZipEntry != null) { try { String apkEntryName = apkZipEntry.getName(); if (apkEntryName.indexOf(Path.SEPARATOR) != -1) { // creates the directory structure folders = apkEntryName.substring(0, apkEntryName.lastIndexOf(Path.SEPARATOR)); fileToCreate = new File(tmpProjectFile, folders); if (!fileToCreate.exists()) { fileToCreate.mkdirs(); } } if (apkEntryName.endsWith(XML_FILE)) { // Gets XML from the parser fileToCreate = new File(tmpProjectFile, apkEntryName); createXMLFile(sdkPath, apkFile.getAbsolutePath(), apkEntryName, fileToCreate); } // filter files which is desired to create else if (!apkEntryName.endsWith(RESOURCES_ARSC) && !apkEntryName.endsWith(CLASSES_DEX)) { // write the file try { apkOutputStream = new FileOutputStream(tmpProjectFile.getAbsolutePath() + Path.SEPARATOR + apkEntryName); int length = 0; while ((length = apkInputStream.read(buf, 0, 1024)) > -1) { apkOutputStream.write(buf, 0, length); } } finally { if (apkOutputStream != null) { try { apkOutputStream.close(); } catch (IOException e) { //Do Nothing. } } } } } catch (ZipException zipException) { // throw exception because the apk is probably corrupt PreflightingLogger .error(ApkUtils.class, "It was not possible to read the android package; it is probably corrupt.", zipException); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_ImpossibleExtractAndroidPackageMessage, zipException); } catch (IOException ioException) { // log the error but do not thrown an exception because // it will be attempted to create all files PreflightingLogger.error(ApkUtils.class, "It was not possible to extract the android package.", ioException); //$NON-NLS-1$ } finally { apkInputStream.closeEntry(); apkZipEntry = apkInputStream.getNextEntry(); } } } catch (IOException ioException) { PreflightingLogger.error(ApkUtils.class, "It was not possible to read the android package.", ioException); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_ImpossibleExtractAndroidPackageMessage, ioException); } finally { try { if (apkInputStream != null) { apkInputStream.close(); } if (apkOutputStream != null) { apkOutputStream.close(); } } catch (IOException ioException) { // Do Nothing. } } } else { PreflightingLogger.error(ApkUtils.class, "It was not possible to read the android package."); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_ImpossibleExtractAndroidPackageMessage); } } /** * Given an APK file, all folders and DOMs for creating the directory * structure with the localization files are returned in a {@link Map}. <br> * The {@link Map} returned holds the following info: [{@link File}, * {@link Document}] in which the {@link File} represents the folder path in * which the {@link Document} is to be created. * * @param aaptPath * AAP tool path. * @param apkPath * APK file which the strings of translation are retrieved. * @param xmlFileName * XML file name generated by the AAP tool. * * @return The {@link Map} structure holding the {@link File}s and * {@link Document}s necessary to create the directory tree for * translation. * * @throws PreflightingToolException * Exception thrown in case anything goes wrong extracting data. */ public static Map<File, Document> retrieveLocalizationStringsMapFromAPK(String aaptPath, String apkPath, String xmlFileName) throws PreflightingToolException { BufferedReader bReader = null; InputStreamReader reader = null; Map<File, Document> map = new HashMap<File, Document>(); try { Process aapt = runAAPTCommandForExtractingResourcesAndValues(aaptPath, apkPath, xmlFileName); // read output and store it in a buffer reader = new InputStreamReader(aapt.getInputStream()); bReader = new BufferedReader(reader); // patterns used to retrieve lines for language, key and values of // string translations Pattern languagePattern = Pattern.compile("config\\s[0-9]+"); Pattern stringKeyPattern = Pattern.compile("[\\s]{2,}resource.+:string/[a-zA-Z0-9\\._$]+:"); Pattern stringArrayKeyPattern = Pattern.compile("[\\s]{2,}resource.+:array/[a-zA-Z0-9\\._$]+:"); Pattern stringArrayCountPattern = Pattern.compile("Count=[0-9]+"); Pattern stringValuePattern = Pattern.compile("\".*\""); Matcher matcher = null; Document document = null; Element resourceElement = null; Element stringElement = null; File languageDirectory = null; int stringArraySize = 0; String folderName = null; String stringArraySizeText = null; String key = null; String value = null; String[] arrayValue = null; String infoLine = ""; while ((infoLine = bReader.readLine()) != null) { // try to match with language matcher = languagePattern.matcher(infoLine); if (matcher.find()) { // in case there are a document and file, add it to the map if ((document != null) && (languageDirectory != null) && (resourceElement != null) && (resourceElement.getChildNodes() != null) && (resourceElement.getChildNodes().getLength() > 0)) { map.put(languageDirectory, document); // reset them languageDirectory = null; document = null; } // get the folder name based on the language folderName = createResourcesSubfolders(infoLine, "values"); languageDirectory = new File(folderName); // try to find an existent directory document = findDocumentByLanguageDirectory(map, languageDirectory); // the DOM was not found - initialize variables if (document == null) { document = createNewDocument(); resourceElement = document.createElement("resources"); document.appendChild(resourceElement); } // the DOM was found - get the resources element (root // element) else { resourceElement = document.getDocumentElement(); } } // try to match with single string keys matcher = stringKeyPattern.matcher(infoLine); if (matcher.find()) { key = matcher.group(); key = key.split(":string/")[1].split(":")[0]; infoLine = ""; do { // go the the next line in order to read the value infoLine += bReader.readLine(); } // do not delete the bReader.ready() statement because this avoids infinitive loops in case the regular expression fails while (!infoLine.matches(".*\".*\".*") && bReader.ready()); matcher = stringValuePattern.matcher(infoLine); if (matcher.find()) { value = matcher.group(); value = value.substring(1, value.length() - 1); // create element to be appended to the resource element appendNewElementToNode("string", "name", key, value, resourceElement, document); } } // try to match with array string keys matcher = stringArrayKeyPattern.matcher(infoLine); if (matcher.find()) { key = matcher.group(); key = key.split(":array/")[1].split(":")[0]; // go the the next line in order to get the number of // elements in the array infoLine = bReader.readLine(); matcher = stringArrayCountPattern.matcher(infoLine); if (matcher.find()) { stringArraySizeText = matcher.group(); stringArraySize = Integer.parseInt(stringArraySizeText.split("=")[1]); // get each string of the array arrayValue = new String[stringArraySize]; for (int arrayStringIndex = 0; arrayStringIndex < stringArraySize; arrayStringIndex++) { try { // go the the next line in order to read the // value infoLine = bReader.readLine(); matcher = stringValuePattern.matcher(infoLine); matcher.find(); value = matcher.group(); value = value.substring(1, value.length() - 1); } catch (Exception e) { // TODO fix this (for now, just keep going, but // this value may be necessary in the future) value = "(reference)"; } arrayValue[arrayStringIndex] = value; } // append array-string element stringElement = appendNewElementToNode("string-array", "name", key, null, resourceElement, document); // create and append the array of strings for (int arrayStringIndex = 0; arrayStringIndex < stringArraySize; arrayStringIndex++) { value = arrayValue[arrayStringIndex]; appendNewElementToNode("item", null, null, value, stringElement, document); } } } } // in case there are a document and file, add it to the map if ((document != null) && (languageDirectory != null) && (resourceElement != null) && (resourceElement.getChildNodes() != null) && (resourceElement.getChildNodes().getLength() > 0)) { map.put(languageDirectory, document); // reset them languageDirectory = null; document = null; } } catch (IOException ioException) { PreflightingLogger.error(ApkUtils.class, ioException.getMessage()); throw new PreflightingToolException(ioException.getMessage(), ioException); } finally { // close resources if (reader != null) { try { reader.close(); } catch (IOException e) { //Do Nothing. } } if (bReader != null) { try { bReader.close(); } catch (IOException e) { //Do Nothing. } } } return map; } /** * Execute the AAPT command: aapt d --values resources [ApkFile].apk > * [XMLFileName].xml. * * @param aaptPath * AAPT path. * @param apkPath * Target APK File path. * @param xmlFileName * XML file name which will be generated. * * @return The {@link Process} created the the execution of the AAPT * command. * * @throws IOException * Exception thrown in case the command execution fails. */ private static Process runAAPTCommandForExtractingResourcesAndValues(String aaptPath, String apkPath, String xmlFileName) throws IOException { // execute command: aapt.exe d --values resources <name>.apk <name>.xml String[] aaptCommand = new String[] { aaptPath, "d", "--values", "resources", apkPath, xmlFileName //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ }; return Runtime.getRuntime().exec(aaptCommand); } /** * Put data from {@link Map}, created in method * {@link #retrieveLocalizationStringsMapFromAPK(String, String, String)} * into the /res directory using the given parameter {@link File}. * * @param map * {@link Map} which data will be extracted. * @param resFile * {@link File} structure which will hold the tree model holding * directories and translation files. * @throws PreflightingToolException */ private static void createLocalizationFilesFromMap(Map<File, Document> map, File resFile) throws PreflightingToolException { Set<File> fileSet = map.keySet(); File stringFolder = null; // iterate through all directories for (File key : fileSet) { // create temporary directories stringFolder = new File(resFile, key.getPath()); if (!stringFolder.exists()) { stringFolder.mkdirs(); } // create XML file createXmlFromDom(map.get(key), new File(resFile.getAbsolutePath() + Path.SEPARATOR + key.getPath() + File.separator + "strings.xml")); } } /** * Create a XML File based on an AAPT output from a APK embedded file. * * @param aaptPath * AAPT path. * @param apkPath * APK path. * @param xmlFileName * the XML file name which is embedded in the APT and is to be * created as an XML file. * @param fileToCreate * XML file to be created. * * @throws PreflightingToolException * Exception thrown when there are problems creating the XML * file. The exception message describe in details the problem. */ public static void createXMLFile(String aaptPath, String apkPath, String xmlFileName, File fileToCreate) throws PreflightingToolException { // command for AAPT tool which gets the XML-to-be file to be worked on String[] aaptCommand = new String[] { aaptPath, "dump", "xmltree", apkPath, xmlFileName //$NON-NLS-1$ //$NON-NLS-2$ }; // execute AAPT command Process aapt = null; try { aapt = Runtime.getRuntime().exec(aaptCommand); } catch (IOException ioException) { PreflightingLogger.error(ApkUtils.class, "Problems executing AAPT command.", //$NON-NLS-1$ ioException); throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_AaptExecutionProblemMessage, ioException); } Map<String, String> namespaceMap = new HashMap<String, String>(); Map<Integer, LineElement> map = readToMap(aapt, aaptPath, apkPath, fileToCreate.getAbsolutePath(), namespaceMap); if (!map.isEmpty()) { Integer outerRow = map.keySet().size(); Integer innerRow; while (outerRow > 0) { LineElement childElement = map.get(outerRow); innerRow = outerRow - 1; while (innerRow > 0) { LineElement parentElement = map.get(innerRow); if (parentElement.getDepth() < childElement.getDepth()) { parentElement.addChildLine(outerRow); break; } innerRow--; } outerRow--; } // create new DOM Document document = createNewDocument(); // populate it addNodes(document, map, map.get(1), null); // add schema for (String namespace : namespaceMap.keySet()) { document.getDocumentElement().setAttribute("xmlns:" + namespace, namespaceMap.get(namespace)); } // create XML file createXmlFromDom(document, fileToCreate); } } /** * generate folder names according to configurations * * @param lineRead * line read from aapt output * @param folderPrefix * The first name of all folders. * @return the directory name * @throws PreflightingToolException * Exception thrown when the entered line has a bad format. */ private static String createResourcesSubfolders(String lineRead, String folderPrefix) throws PreflightingToolException { Pattern configPattern = Pattern.compile("config\\s[0-9]"); Matcher matcher = null; StringBuffer strBuf = new StringBuffer(lineRead); // try to match with type matcher = configPattern.matcher(strBuf); if (matcher.find()) { for (int i = 0; i < 18; i++) { matcher = patternArray[i].matcher(strBuf); if (matcher.find()) { String result = matcher.group(); String value = result.split("=")[1]; // special treatment if (localizationAttributesMap2.containsKey(patternArray[i])) { String nameSegment = localizationAttributesMap2.get(patternArray[i]).get(value); if (nameSegment != null) { folderPrefix += "-" + nameSegment; } } else { String nameSegment = localizationAttributesMap1.get(patternArray[i]) + value; // treat the specific case of height, whose value is // preceded by x egg. 1024x864 if (i != 16) { folderPrefix += "-" + nameSegment; } else { folderPrefix += nameSegment; } } } } } else { PreflightingLogger.error("The entered line has a bad format."); throw new PreflightingToolException("The entered line has a bad format."); } return folderPrefix; } /** * Create a XML file from a {@link Document}. * * @param document * Document to be turned into a XML File. * @param xmlFile * XML file which will receive the {@link Document} stream. * * @throws PreflightingToolException * Exception thrown when there are problems creating the XML * file. */ private static void createXmlFromDom(Document document, File xmlFile) throws PreflightingToolException { StreamResult result = null; DOMSource source = null; Transformer transformer = null; FileOutputStream fo = null; StringWriter sw = null; try { // get factory TransformerFactory transformerFactory = TransformerFactory.newInstance(); // get transformer and configure it transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); //$NON-NLS-1$ //$NON-NLS-2$ // create the XML file source = new DOMSource(document); sw = new StringWriter(); result = new StreamResult(sw); transformer.transform(source, result); fo = new FileOutputStream(xmlFile); fo.write(sw.toString().getBytes("utf-8")); } catch (Exception ex) { //log error, but try to continue validation without the XML file with problem PreflightingLogger.error(ApkUtils.class, "Problems creating the XML file.", ex); //$NON-NLS-1$ } finally { try { // Close streams and stuff if (fo != null) { fo.close(); } if (result.getWriter() != null) { result.getWriter().close(); } if (sw != null) { sw.close(); } } catch (IOException e) { // do nothing } } } /** * Create a new {@link Document}. * * @return A newly-created {@link Document} object. * * @throws PreflightingToolException * Exception thrown when there are problems creating a new * {@link Document}. */ private static Document createNewDocument() throws PreflightingToolException { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = null; try { documentBuilder = documentBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException pcException) { PreflightingLogger .error(ApkUtils.class, "Problems creating DOM isntance.", pcException); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_DomInstanceProblemMessage, pcException); } // create DOM Document document = documentBuilder.newDocument(); return document; } /** * Create a Map holding the AAPT XML output info. * * @param aaptProccess * AAPT execution process - its data will be processed here * @param aaptPath * APPT path * @param apkPath * APK path * @param xmlFileName * XML file name * * @return The Map holding APPT XML output info. * * @throws PreflightingToolException * Exception thrown in case there are problems reading the APPT * XML output info from the process. */ public static Map<Integer, LineElement> readToMap(Process aaptProccess, String aaptPath, String apkPath, String xmlFileName, Map<String, String> namespaceMap) throws PreflightingToolException { InputStreamReader reader = new InputStreamReader(aaptProccess.getInputStream()); BufferedReader bReader = new BufferedReader(reader); // list for the map List<LineElement> lineList = new ArrayList<LineElement>(); LineElement lineElement; String infoLine; try { while ((infoLine = bReader.readLine()) != null) { if (infoLine.length() > 0) { lineElement = new LineElement(); if (infoLine.contains(ELEMENT_NODE) || infoLine.contains(ELEMENT_NODE)) { lineElement.setType(LineElement.LineType.ELEMENT); lineElement.setDepth(infoLine.split(ELEMENT_NODE)[0].length()); lineElement.setName(getElementLineName(infoLine)); lineList.add(lineElement); } else if (infoLine.contains(ATTRIBUTE_NODE)) { lineElement.setType(LineElement.LineType.ATTRIBUTE); lineElement.setDepth(infoLine.split(ATTRIBUTE_NODE)[0].length()); lineElement.setName(getElementLineName(infoLine)); lineElement.setValue(getElementLineValue(aaptPath, apkPath, xmlFileName, infoLine)); lineList.add(lineElement); } else if (infoLine.contains(NAMESPACE_XMLNS)) { String namespace = infoLine.split(NAMESPACE_XMLNS)[1].trim(); if (namespace.indexOf("=") != -1) { String id = namespace.substring(0, namespace.indexOf("=")); String url = namespace.substring(namespace.indexOf("=") + 1); namespaceMap.put(id, url); } } } } } catch (IOException ioException) { PreflightingLogger.error(ApkUtils.class, "Problems reading AAPT command execution result.", ioException); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_AaptResultReadProblemMessage, ioException); } finally { // close resources try { if (bReader != null) { bReader.close(); } if (reader != null) { reader.close(); } } catch (IOException ioException) { PreflightingLogger.error(ApkUtils.class, "Problems reading AAPT command execution result.", ioException); //$NON-NLS-1$ throw new PreflightingToolException( PreflightingCoreNLS.ApkUtils_AaptResultReadProblemMessage, ioException); } } Map<Integer, LineElement> map = new HashMap<Integer, LineElement>(); Integer counter = 0; for (LineElement elem : lineList) { ++counter; map.put(counter, elem); } return map; } /** * Get the Element Line?s Name from a text line. It could either be an * Element or an Attribute. * * @param lineText * Text Line where the Name will be retrieved. * * @return Returns the Name. */ private static String getElementLineName(String lineText) { Matcher matcher = null; Pattern pattern = null; String matchText = null; String name = null; // try to match the element pattern pattern = Pattern.compile("(E: .+ ){1}"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); // in case there is a match, populate the Line Element object if (matcher.find()) { matchText = matcher.group(); name = matchText.split(ELEMENT_NODE)[1].trim(); } else { // try to match the attribute pattern pattern = Pattern.compile("(A:){1}"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); if (matcher.find()) { // since there is an element pattern, get its name pattern = Pattern.compile("^ *A:\\s*[\\w:\\w]*"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); if (matcher.find()) { matchText = matcher.group(); // get the name name = matchText.split(ATTRIBUTE_NODE)[1]; // adjust it name = name.trim(); } } } return name; } /** * Try to find the {@link Document} associated with a certain language * directory path. In case nothing is found, null is returned. * * @param map * {@link Map} in which the search will be made. * @param languageDirectory * Directory holding the path to be compared, in order to find * the {@link Document} in the given {@link Map}. This Object is * updated with a reference to the object in the {@link Map}. * * @return {@link Document} element associated, in case a match is * successful. */ private static Document findDocumentByLanguageDirectory(Map<File, Document> map, File languageDirectory) { Document document = null; Set<File> languageFolders = map.keySet(); if (languageFolders != null) { for (File languageFolder : languageFolders) { if (languageFolder.getPath().equals(languageDirectory.getPath())) { document = map.get(languageFolder); languageDirectory = languageFolder; break; } } } return document; } /** * Given a certain parent {@link Element} and {@link Document}, append a * child {@link Element} with Tag Name (which cannot be null), Node * Attribute Name, Node Attribute Value (which both are null at the same * time or none is null at all), Node Value (which can be null). * * @param nodeTagName * Node Tag Name. * @param nodeAttributeName * Node Attribute Name. * @param nodeAttributeValue * Node Attribute Value. * @param nodeValue * Node Value. * @param elementToBeApppendedTo * Parent {@link Element} which the new created {@link Element} * will be appended to. * @param document * {@link Document} which everything belongs to. * * @return Returns the created {@link Element}. */ private static Element appendNewElementToNode(String nodeTagName, String nodeAttributeName, String nodeAttributeValue, String nodeValue, Element elementToBeApppendedTo, Document document) { // create element and append it Element element = document.createElement(nodeTagName); if ((nodeAttributeName != null) && (nodeAttributeValue != null)) { element.setAttribute(nodeAttributeName, nodeAttributeValue); } if (nodeValue != null) { element.setTextContent(nodeValue); } elementToBeApppendedTo.appendChild(element); return element; } /** * Add all attributes and children in a {@link Document}, given a * {@link LineElement}. * * @param document * DOM where elements are added. * @param map * Map holding all {@link LineElement}s. * @param elem * Element to be added to the DOM. * @param rootElement * Root element. */ private static void addNodes(Document document, Map<Integer, LineElement> map, LineElement elem, Element rootElement) { if (elem.getType() == LineElement.LineType.ELEMENT) { // add element or root Element element = document.createElement(elem.getName()); if (rootElement == null) { document.appendChild(element); } else { rootElement.appendChild(element); } // add children for (Integer childElementMapIndex : elem.getChildLines()) { LineElement childElement = map.get(childElementMapIndex); // add all attributes from this node if (childElement.getType() == LineElement.LineType.ATTRIBUTE) { element.setAttribute(childElement.getName(), childElement.getValue()); } // add a child element else { addNodes(document, map, childElement, element); } } } } /** * Retrieve the Value of an Text line. * * @param lineText * Text line where the value will be retrieved from. * * @return Value retrieved. */ private static String getElementLineValue(String aaptPath, String apkPath, String xmlFileName, String lineText) { Matcher matcher = null; Pattern pattern = null; String matchText = null; String name = null; // Get the values, depending on their pattern // start with Raw values pattern = Pattern.compile("(\\(Raw: \".*\"\\)){1}"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); if (matcher.find()) { matchText = matcher.group(); // get the element within "" pattern = Pattern.compile("(\".*\"){1}"); //$NON-NLS-1$ matcher = pattern.matcher(matchText); if (matcher.find()) { matchText = matcher.group(); name = matchText.replaceAll("\"", "").trim(); //$NON-NLS-1$ //$NON-NLS-2$ } } else { // get values after @ pattern = Pattern.compile("(\\)=@.*){1}"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); if (matcher.find()) { matchText = matcher.group(); name = matchText.replaceAll("\\)=@", "").trim(); //$NON-NLS-1$ //$NON-NLS-2$ name = getResourceMatch(aaptPath, apkPath, xmlFileName, name); } else { // get values with type pattern = Pattern.compile("(\\(type .*\\).*){1}"); //$NON-NLS-1$ matcher = pattern.matcher(lineText); if (matcher.find()) { matchText = matcher.group(); name = matchText.replaceAll("(\\(type .*\\))", ""); //$NON-NLS-1$ //$NON-NLS-2$ name = name.replace("0x", ""); //$NON-NLS-1$ //$NON-NLS-2$ try { long longValue = Long.parseLong(name, 16); name = Long.toHexString(longValue).trim(); // TODO: correctly handle types instead of doing this kind of verification if (lineText.contains(ANDROID_SMALL_SCREENS) || lineText.contains("android:normalScreens") || lineText.contains("android:largeScreens") || lineText.contains("android:xlargeScreens") || lineText.contains("android:anyDensity") || lineText.contains("android:resizeable")) { name = longValue == 0 ? "false" : "true"; } } catch (NumberFormatException ex) { /* * Do nothing because the number could not be converted * to an integer. Leave it as it is to put in the XML * file. */ } } } } return name; } /** * Get the Resource reference from a @x value in the AAPT XML output. * * @param aaptPath * AAPT Path. * @param apkPath * APK Path. * @param xmlFileName * XML file Name * @param resourceId * Resource Id which the value will be retrieved. * * @return Value referenced by a resource Id. */ private static String getResourceMatch(String aaptPath, String apkPath, String xmlFileName, String resourceId) { xmlFileName = xmlFileName.substring(xmlFileName.indexOf(".tmp") + 5); // we parse a xml file only once, so we check if its values are already // stored if (resourceValues.get(xmlFileName) == null) { HashMap<String, String> currentMap = new HashMap<String, String>(); BufferedReader bReader = null; InputStreamReader reader = null; try { Process aapt = runAAPTCommandForExtractingResourcesAndValues(aaptPath, apkPath, xmlFileName); // read output and store it in a buffer reader = new InputStreamReader(aapt.getInputStream()); bReader = new BufferedReader(reader); String infoLine = ""; //$NON-NLS-1$ StringBuffer strBuf = new StringBuffer(); while ((infoLine = bReader.readLine()) != null) { strBuf.append(infoLine); strBuf.append("\n"); //$NON-NLS-1$ } // apply pattern to retrieve resource id and its value Pattern pattern = Pattern.compile("resource\\s[0-9a-fxA-FX]+\\s[a-zA-Z_0-9.]+:[a-z0-9./_]+:"); //$NON-NLS-1$ Matcher matcher = pattern.matcher(strBuf); Pattern keyPattern = Pattern.compile("\\s[0-9a-fxA-FX]+\\s"); //$NON-NLS-1$ Pattern valuePattern = Pattern.compile(":[a-z0-9./_]+:"); //$NON-NLS-1$ while (matcher.find()) { String match = matcher.group(); // key matcher Matcher keyMatcher = keyPattern.matcher(match); keyMatcher.find(); String key = keyMatcher.group(); key = key.trim(); // aapt output has a resource reference for each // configuration // e.g. a drawable resource can present three densities: // hpdi, mpdi, lpdi if (!currentMap.containsKey(key)) { // value matcher Matcher valueMatcher = valuePattern.matcher(match); valueMatcher.find(); String value = valueMatcher.group(); value = "@" + value.substring(1, value.length() - 1); //$NON-NLS-1$ currentMap.put(key, value); } } // store in global variable resourceValues.put(xmlFileName, currentMap); } catch (Exception e) { PreflightingLogger.error(ApkUtils.class, e.getMessage()); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { //Do nothing. } } if (bReader != null) { try { bReader.close(); } catch (IOException e) { //Do nothing. } } } } return resourceValues.get(xmlFileName).get(resourceId); } } /** * Class which holds each line information from the AAPT XML output. */ class LineElement { /** * Enumerator which determines which type of the emement it is to be put in * the XML file. */ public enum LineType { ELEMENT, ATTRIBUTE } private final List<Integer> childLines = new ArrayList<Integer>(); /** * Get the list of children indexes. * * @return List of children indexes. */ public List<Integer> getChildLines() { Collections.sort(childLines); return childLines; } /** * Add a child index representation. * * @param index * Child inex representation. */ public void addChildLine(Integer index) { childLines.add(index); } private LineType type; /** * Get the {@link LineType}. * * @return The {@link LineType}. */ public LineType getType() { return type; } /** * Set the {@link LineType}. * * @param type * The {@link LineType}. */ public void setType(LineType type) { this.type = type; } /** * Get the Name. * * @return The name. */ public String getName() { return name; } /** * Set the Name. * * @param name * The name. */ public void setName(String name) { this.name = name; } /** * Get the node depth. * * @return The node depth. */ public int getDepth() { return depth; } /** * Set the node depth. * * @param depth * The node depth. */ public void setDepth(int depth) { this.depth = depth; } private String name; private String value; /** * Get the Node value. * * @return The node value. */ public String getValue() { return value; } /** * Set the node value. * * @param value * The node value. */ public void setValue(String value) { this.value = value; } private int depth; }