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