blob: 1ddfc68384c94d279a6cedd1db2ad83217ab3894 [file] [log] [blame]
/*
* 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;
}