diff options
Diffstat (limited to 'src/plugins/preflighting.core/src/com/motorolamobility/preflighting/core/internal/utils/AaptUtils.java')
-rw-r--r-- | src/plugins/preflighting.core/src/com/motorolamobility/preflighting/core/internal/utils/AaptUtils.java | 1528 |
1 files changed, 1528 insertions, 0 deletions
diff --git a/src/plugins/preflighting.core/src/com/motorolamobility/preflighting/core/internal/utils/AaptUtils.java b/src/plugins/preflighting.core/src/com/motorolamobility/preflighting/core/internal/utils/AaptUtils.java new file mode 100644 index 0000000..1ddfc68 --- /dev/null +++ b/src/plugins/preflighting.core/src/com/motorolamobility/preflighting/core/internal/utils/AaptUtils.java @@ -0,0 +1,1528 @@ +/* + * 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; + +} |