blob: 257cdfef713ca4a56c38655513224b48122b2a23 [file] [log] [blame]
/*
* Copyright (C) 2007 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.android.ide.common.xml;
import com.android.SdkConstants;
import com.android.ide.common.xml.ManifestData.Activity;
import com.android.ide.common.xml.ManifestData.Instrumentation;
import com.android.ide.common.xml.ManifestData.SupportsScreens;
import com.android.ide.common.xml.ManifestData.UsesConfiguration;
import com.android.ide.common.xml.ManifestData.UsesFeature;
import com.android.ide.common.xml.ManifestData.UsesLibrary;
import com.android.io.IAbstractFile;
import com.android.io.IAbstractFolder;
import com.android.io.StreamException;
import com.android.resources.Keyboard;
import com.android.resources.Navigation;
import com.android.resources.TouchScreen;
import com.android.xml.AndroidManifest;
import com.google.common.io.Closeables;
import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
public class AndroidManifestParser {
private static final int LEVEL_TOP = 0;
private static final int LEVEL_INSIDE_MANIFEST = 1;
private static final int LEVEL_INSIDE_APPLICATION = 2;
private static final int LEVEL_INSIDE_APP_COMPONENT = 3;
private static final int LEVEL_INSIDE_INTENT_FILTER = 4;
private static final String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$
private static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$
public interface ManifestErrorHandler extends ErrorHandler {
/**
* Handles a parsing error and an optional line number.
*/
void handleError(Exception exception, int lineNumber);
/**
* Checks that a class is valid and can be used in the Android Manifest.
* <p/>
* Errors are put as {@code org.eclipse.core.resources.IMarker} on the manifest file.
*
* @param locator
* @param className the fully qualified name of the class to test.
* @param superClassName the fully qualified name of the class it is supposed to extend.
* @param testVisibility if <code>true</code>, the method will check the visibility of
* the class or of its constructors.
*/
void checkClass(Locator locator, String className, String superClassName,
boolean testVisibility);
}
/**
* XML error & data handler used when parsing the AndroidManifest.xml file.
* <p/>
* During parsing this will fill up the {@link ManifestData} object given to the constructor
* and call out errors to the given {@link ManifestErrorHandler}.
*/
private static class ManifestHandler extends DefaultHandler {
//--- temporary data/flags used during parsing
private final ManifestData mManifestData;
private final ManifestErrorHandler mErrorHandler;
private int mCurrentLevel = 0;
private int mValidLevel = 0;
private Activity mCurrentActivity = null;
private Locator mLocator;
/**
* Creates a new {@link ManifestHandler}.
*
* @param manifestFile The manifest file being parsed. Can be null.
* @param manifestData Class containing the manifest info obtained during the parsing.
* @param errorHandler An optional error handler.
*/
ManifestHandler(IAbstractFile manifestFile, ManifestData manifestData,
ManifestErrorHandler errorHandler) {
super();
mManifestData = manifestData;
mErrorHandler = errorHandler;
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator)
*/
@Override
public void setDocumentLocator(Locator locator) {
mLocator = locator;
super.setDocumentLocator(locator);
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
* java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
try {
if (mManifestData == null) {
return;
}
// if we're at a valid level
if (mValidLevel == mCurrentLevel) {
String value;
switch (mValidLevel) {
case LEVEL_TOP:
if (AndroidManifest.NODE_MANIFEST.equals(localName)) {
// lets get the package name.
mManifestData.mPackage = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_PACKAGE,
false /* hasNamespace */);
// and the versionCode
String tmp = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_VERSIONCODE, true);
if (tmp != null) {
try {
mManifestData.mVersionCode = Integer.valueOf(tmp);
} catch (NumberFormatException e) {
// keep null in the field.
}
}
mValidLevel++;
}
break;
case LEVEL_INSIDE_MANIFEST:
if (AndroidManifest.NODE_APPLICATION.equals(localName)) {
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_PROCESS,
true /* hasNamespace */);
if (value != null) {
mManifestData.addProcessName(value);
}
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_DEBUGGABLE,
true /* hasNamespace*/);
if (value != null) {
mManifestData.mDebuggable = Boolean.parseBoolean(value);
}
mValidLevel++;
} else if (AndroidManifest.NODE_USES_SDK.equals(localName)) {
mManifestData.setMinSdkVersionString(getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
true /* hasNamespace */));
mManifestData.setTargetSdkVersionString(getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION,
true /* hasNamespace */));
} else if (AndroidManifest.NODE_INSTRUMENTATION.equals(localName)) {
processInstrumentationNode(attributes);
} else if (AndroidManifest.NODE_SUPPORTS_SCREENS.equals(localName)) {
processSupportsScreensNode(attributes);
} else if (AndroidManifest.NODE_USES_CONFIGURATION.equals(localName)) {
processUsesConfiguration(attributes);
} else if (AndroidManifest.NODE_USES_FEATURE.equals(localName)) {
UsesFeature feature = new UsesFeature();
// get the name
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (value != null) {
feature.mName = value;
}
// read the required attribute
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_REQUIRED,
true /*hasNamespace*/);
if (value != null) {
Boolean b = Boolean.valueOf(value);
if (b != null) {
feature.mRequired = b;
}
}
// read the gl es attribute
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_GLESVERSION,
true /*hasNamespace*/);
if (value != null) {
try {
int version = Integer.decode(value);
feature.mGlEsVersion = version;
} catch (NumberFormatException e) {
// ignore
}
}
mManifestData.mFeatures.add(feature);
}
break;
case LEVEL_INSIDE_APPLICATION:
if (AndroidManifest.NODE_ACTIVITY.equals(localName)
|| AndroidManifest.NODE_ACTIVITY_ALIAS.equals(localName)) {
processActivityNode(attributes);
mValidLevel++;
} else if (AndroidManifest.NODE_SERVICE.equals(localName)) {
processNode(attributes, SdkConstants.CLASS_SERVICE);
mValidLevel++;
} else if (AndroidManifest.NODE_RECEIVER.equals(localName)) {
processNode(attributes, SdkConstants.CLASS_BROADCASTRECEIVER);
mValidLevel++;
} else if (AndroidManifest.NODE_PROVIDER.equals(localName)) {
processNode(attributes, SdkConstants.CLASS_CONTENTPROVIDER);
mValidLevel++;
} else if (AndroidManifest.NODE_USES_LIBRARY.equals(localName)) {
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (value != null) {
UsesLibrary library = new UsesLibrary();
library.mName = value;
// read the required attribute
value = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_REQUIRED,
true /*hasNamespace*/);
if (value != null) {
Boolean b = Boolean.valueOf(value);
if (b != null) {
library.mRequired = b;
}
}
mManifestData.mLibraries.add(library);
}
}
break;
case LEVEL_INSIDE_APP_COMPONENT:
// only process this level if we are in an activity
if (mCurrentActivity != null &&
AndroidManifest.NODE_INTENT.equals(localName)) {
mCurrentActivity.resetIntentFilter();
mValidLevel++;
}
break;
case LEVEL_INSIDE_INTENT_FILTER:
if (mCurrentActivity != null) {
if (AndroidManifest.NODE_ACTION.equals(localName)) {
// get the name attribute
String action = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (action != null) {
mCurrentActivity.setHasAction(true);
mCurrentActivity.setHasMainAction(
ACTION_MAIN.equals(action));
}
} else if (AndroidManifest.NODE_CATEGORY.equals(localName)) {
String category = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (CATEGORY_LAUNCHER.equals(category)) {
mCurrentActivity.setHasLauncherCategory(true);
}
}
// no need to increase mValidLevel as we don't process anything
// below this level.
}
break;
}
}
mCurrentLevel++;
} finally {
super.startElement(uri, localName, name, attributes);
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
* java.lang.String)
*/
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
try {
if (mManifestData == null) {
return;
}
// decrement the levels.
if (mValidLevel == mCurrentLevel) {
mValidLevel--;
}
mCurrentLevel--;
// if we're at a valid level
// process the end of the element
if (mValidLevel == mCurrentLevel) {
switch (mValidLevel) {
case LEVEL_INSIDE_APPLICATION:
mCurrentActivity = null;
break;
case LEVEL_INSIDE_APP_COMPONENT:
// if we found both a main action and a launcher category, this is our
// launcher activity!
if (mManifestData.mLauncherActivity == null &&
mCurrentActivity != null &&
mCurrentActivity.isHomeActivity() &&
mCurrentActivity.isExported()) {
mManifestData.mLauncherActivity = mCurrentActivity;
}
break;
default:
break;
}
}
} finally {
super.endElement(uri, localName, name);
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
*/
@Override
public void error(SAXParseException e) {
if (mErrorHandler != null) {
mErrorHandler.handleError(e, e.getLineNumber());
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException)
*/
@Override
public void fatalError(SAXParseException e) {
if (mErrorHandler != null) {
mErrorHandler.handleError(e, e.getLineNumber());
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException)
*/
@Override
public void warning(SAXParseException e) throws SAXException {
if (mErrorHandler != null) {
mErrorHandler.warning(e);
}
}
/**
* Processes the activity node.
* @param attributes the attributes for the activity node.
*/
private void processActivityNode(Attributes attributes) {
// lets get the activity name, and add it to the list
String activityName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (activityName != null) {
activityName = AndroidManifest.combinePackageAndClassName(mManifestData.mPackage,
activityName);
// get the exported flag.
String exportedStr = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_EXPORTED, true);
boolean exported = exportedStr == null ||
exportedStr.toLowerCase(Locale.US).equals("true"); //$NON-NLS-1$
mCurrentActivity = new Activity(activityName, exported);
mManifestData.mActivities.add(mCurrentActivity);
if (mErrorHandler != null) {
mErrorHandler.checkClass(mLocator, activityName, SdkConstants.CLASS_ACTIVITY,
true /* testVisibility */);
}
} else {
// no activity found! Aapt will output an error,
// so we don't have to do anything
mCurrentActivity = null;
}
String processName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_PROCESS,
true /* hasNamespace */);
if (processName != null) {
mManifestData.addProcessName(processName);
}
}
/**
* Processes the service/receiver/provider nodes.
* @param attributes the attributes for the activity node.
* @param superClassName the fully qualified name of the super class that this
* node is representing
*/
private void processNode(Attributes attributes, String superClassName) {
// lets get the class name, and check it if required.
String serviceName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (serviceName != null) {
serviceName = AndroidManifest.combinePackageAndClassName(mManifestData.mPackage,
serviceName);
if (mErrorHandler != null) {
mErrorHandler.checkClass(mLocator, serviceName, superClassName,
false /* testVisibility */);
}
}
String processName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_PROCESS,
true /* hasNamespace */);
if (processName != null) {
mManifestData.addProcessName(processName);
}
}
/**
* Processes the instrumentation node.
* @param attributes the attributes for the instrumentation node.
*/
private void processInstrumentationNode(Attributes attributes) {
// lets get the class name, and check it if required.
String instrumentationName = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_NAME,
true /* hasNamespace */);
if (instrumentationName != null) {
String instrClassName = AndroidManifest.combinePackageAndClassName(
mManifestData.mPackage, instrumentationName);
String targetPackage = getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_TARGET_PACKAGE,
true /* hasNamespace */);
mManifestData.mInstrumentations.add(
new Instrumentation(instrClassName, targetPackage));
if (mErrorHandler != null) {
mErrorHandler.checkClass(mLocator, instrClassName,
SdkConstants.CLASS_INSTRUMENTATION, true /* testVisibility */);
}
}
}
/**
* Processes the supports-screens node.
* @param attributes the attributes for the supports-screens node.
*/
private void processSupportsScreensNode(Attributes attributes) {
mManifestData.mSupportsScreensFromManifest = new SupportsScreens();
mManifestData.mSupportsScreensFromManifest.setResizeable(getAttributeBooleanValue(
attributes, AndroidManifest.ATTRIBUTE_RESIZEABLE, true /*hasNamespace*/));
mManifestData.mSupportsScreensFromManifest.setAnyDensity(getAttributeBooleanValue(
attributes, AndroidManifest.ATTRIBUTE_ANYDENSITY, true /*hasNamespace*/));
mManifestData.mSupportsScreensFromManifest.setSmallScreens(getAttributeBooleanValue(
attributes, AndroidManifest.ATTRIBUTE_SMALLSCREENS, true /*hasNamespace*/));
mManifestData.mSupportsScreensFromManifest.setNormalScreens(getAttributeBooleanValue(
attributes, AndroidManifest.ATTRIBUTE_NORMALSCREENS, true /*hasNamespace*/));
mManifestData.mSupportsScreensFromManifest.setLargeScreens(getAttributeBooleanValue(
attributes, AndroidManifest.ATTRIBUTE_LARGESCREENS, true /*hasNamespace*/));
}
/**
* Processes the supports-screens node.
* @param attributes the attributes for the supports-screens node.
*/
private void processUsesConfiguration(Attributes attributes) {
mManifestData.mUsesConfiguration = new UsesConfiguration();
mManifestData.mUsesConfiguration.mReqFiveWayNav = getAttributeBooleanValue(
attributes,
AndroidManifest.ATTRIBUTE_REQ_5WAYNAV, true /*hasNamespace*/);
mManifestData.mUsesConfiguration.mReqNavigation = Navigation.getEnum(
getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_REQ_NAVIGATION, true /*hasNamespace*/));
mManifestData.mUsesConfiguration.mReqHardKeyboard = getAttributeBooleanValue(
attributes,
AndroidManifest.ATTRIBUTE_REQ_HARDKEYBOARD, true /*hasNamespace*/);
mManifestData.mUsesConfiguration.mReqKeyboardType = Keyboard.getEnum(
getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_REQ_KEYBOARDTYPE, true /*hasNamespace*/));
mManifestData.mUsesConfiguration.mReqTouchScreen = TouchScreen.getEnum(
getAttributeValue(attributes,
AndroidManifest.ATTRIBUTE_REQ_TOUCHSCREEN, true /*hasNamespace*/));
}
/**
* Searches through the attributes list for a particular one and returns its value.
* @param attributes the attribute list to search through
* @param attributeName the name of the attribute to look for.
* @param hasNamespace Indicates whether the attribute has an android namespace.
* @return a String with the value or null if the attribute was not found.
* @see SdkConstants#NS_RESOURCES
*/
private String getAttributeValue(Attributes attributes, String attributeName,
boolean hasNamespace) {
int count = attributes.getLength();
for (int i = 0 ; i < count ; i++) {
if (attributeName.equals(attributes.getLocalName(i)) &&
((hasNamespace &&
SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) ||
(hasNamespace == false && attributes.getURI(i).isEmpty()))) {
return attributes.getValue(i);
}
}
return null;
}
/**
* Searches through the attributes list for a particular one and returns its value as a
* Boolean. If the attribute is not present, this will return null.
* @param attributes the attribute list to search through
* @param attributeName the name of the attribute to look for.
* @param hasNamespace Indicates whether the attribute has an android namespace.
* @return a String with the value or null if the attribute was not found.
* @see SdkConstants#NS_RESOURCES
*/
private Boolean getAttributeBooleanValue(Attributes attributes, String attributeName,
boolean hasNamespace) {
int count = attributes.getLength();
for (int i = 0 ; i < count ; i++) {
if (attributeName.equals(attributes.getLocalName(i)) &&
((hasNamespace &&
SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) ||
(hasNamespace == false && attributes.getURI(i).isEmpty()))) {
String attr = attributes.getValue(i);
if (attr != null) {
return Boolean.valueOf(attr);
} else {
return null;
}
}
}
return null;
}
}
private static final SAXParserFactory sParserFactory;
static {
sParserFactory = SAXParserFactory.newInstance();
sParserFactory.setNamespaceAware(true);
}
/**
* Parses the Android Manifest, and returns a {@link ManifestData} object containing the
* result of the parsing.
*
* @param manifestFile the {@link IAbstractFile} representing the manifest file.
* @param gatherData indicates whether the parsing will extract data from the manifest. If false
* the method will always return null.
* @param errorHandler an optional errorHandler.
* @return A class containing the manifest info obtained during the parsing, or null on error.
*
* @throws StreamException
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
*/
public static ManifestData parse(
IAbstractFile manifestFile,
boolean gatherData,
ManifestErrorHandler errorHandler)
throws SAXException, IOException, StreamException, ParserConfigurationException {
if (manifestFile != null) {
SAXParser parser = sParserFactory.newSAXParser();
ManifestData data = null;
if (gatherData) {
data = new ManifestData();
}
ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
data, errorHandler);
InputStream is = manifestFile.getContents();
try {
parser.parse(new InputSource(is), manifestHandler);
} finally {
try {
Closeables.close(is, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
}
return data;
}
return null;
}
/**
* Parses the Android Manifest, and returns an object containing the result of the parsing.
*
* <p/>
* This is the equivalent of calling <pre>parse(manifestFile, true, null)</pre>
*
* @param manifestFile the manifest file to parse.
*
* @throws ParserConfigurationException
* @throws StreamException
* @throws IOException
* @throws SAXException
*/
public static ManifestData parse(IAbstractFile manifestFile)
throws SAXException, IOException, StreamException, ParserConfigurationException {
return parse(manifestFile, true, null);
}
public static ManifestData parse(IAbstractFolder projectFolder)
throws SAXException, IOException, StreamException, ParserConfigurationException {
IAbstractFile manifestFile = AndroidManifest.getManifest(projectFolder);
if (manifestFile == null) {
throw new FileNotFoundException();
}
return parse(manifestFile, true, null);
}
/**
* Parses the Android Manifest from an {@link InputStream}, and returns a {@link ManifestData}
* object containing the result of the parsing.
*
* @param manifestFileStream the {@link InputStream} representing the manifest file.
* @return A class containing the manifest info obtained during the parsing or null on error.
*
* @throws StreamException
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
*/
public static ManifestData parse(InputStream manifestFileStream)
throws SAXException, IOException, StreamException, ParserConfigurationException {
if (manifestFileStream != null) {
SAXParser parser = sParserFactory.newSAXParser();
ManifestData data = new ManifestData();
ManifestHandler manifestHandler = new ManifestHandler(null, data, null);
parser.parse(new InputSource(manifestFileStream), manifestHandler);
return data;
}
return null;
}
}