/* * Copyright 2012-present Facebook, Inc. * * 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.facebook.buck.android; import com.facebook.buck.util.XmlDomParser; import com.google.common.base.Preconditions; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class DefaultAndroidManifestReader implements AndroidManifestReader { /** * XPath expression to retrieve the names of activities with an intent-filter that gets them to * show up in the launcher. */ private static final String XPATH_LAUNCHER_ACTIVITIES = "/manifest/application/*" + " [self::activity[not(@android:enabled) or @android:enabled='true'] or " + " self::activity-alias[not(@android:enabled) or @android:enabled='true']]" + " [intent-filter[action/@android:name='android.intent.action.MAIN' and " + " category/@android:name='android.intent.category.LAUNCHER']]" + " /@android:name"; /** * XPath expression to get the package. For a manifest as {@code <manifest * package="com.facebook.katana" />}, this results in {@code com.facebook.katana}. */ private static final String XPATH_PACKAGE = "/manifest/@package"; /** * XPath expression to get the version code. For a manifest as {@code <manifest * android:versionCode="1" />}, this results in {@code 1}. */ private static final String XPATH_VERSION_CODE = "/manifest/@android:versionCode"; /** XPath expression to get the instrumentation test runner. */ private static final String XPATH_INSTRUMENTATION_TEST_RUNNER = "/manifest/instrumentation/@android:name"; private final XPathExpression packageExpression; private final XPathExpression versionCodeExpression; private final XPathExpression instrumentationTestRunnerExpression; private final XPathExpression launchableActivitiesExpression; private final Document doc; private DefaultAndroidManifestReader(InputSource src) throws IOException { try { // Parse the XML. doc = XmlDomParser.parse(src, true); // Compile the XPath expressions. XPath xPath = XPathFactory.newInstance().newXPath(); xPath.setNamespaceContext(androidNamespaceContext); launchableActivitiesExpression = xPath.compile(XPATH_LAUNCHER_ACTIVITIES); packageExpression = xPath.compile(XPATH_PACKAGE); versionCodeExpression = xPath.compile(XPATH_VERSION_CODE); instrumentationTestRunnerExpression = xPath.compile(XPATH_INSTRUMENTATION_TEST_RUNNER); } catch (XPathExpressionException | SAXException e) { throw new RuntimeException(e); } } @Override public List<String> getLauncherActivities() { try { NodeList nodes; nodes = (NodeList) launchableActivitiesExpression.evaluate(doc, XPathConstants.NODESET); List<String> activities = new ArrayList<>(); for (int i = 0; i < nodes.getLength(); i++) { activities.add(nodes.item(i).getTextContent()); } return activities; } catch (XPathExpressionException e) { throw new RuntimeException(e); } } @Override public String getPackage() { try { return (String) packageExpression.evaluate(doc, XPathConstants.STRING); } catch (XPathExpressionException e) { throw new RuntimeException(e); } } @Override public String getVersionCode() { try { return (String) versionCodeExpression.evaluate(doc, XPathConstants.STRING); } catch (XPathExpressionException e) { throw new RuntimeException(e); } } @Override public String getInstrumentationTestRunner() { try { return (String) instrumentationTestRunnerExpression.evaluate(doc, XPathConstants.STRING); } catch (XPathExpressionException e) { throw new RuntimeException(e); } } /** This allows querying the AndroidManifest for e.g. attributes like android:name using XPath */ private static NamespaceContext androidNamespaceContext = new NamespaceContext() { @Override public Iterator<String> getPrefixes(String namespaceURI) { throw new UnsupportedOperationException(); } @Override public String getPrefix(String namespaceURI) { throw new UnsupportedOperationException(); } @Override public String getNamespaceURI(String prefix) { if (prefix.equals("android")) { return "http://schemas.android.com/apk/res/android"; } else { throw new IllegalArgumentException(); } } }; /** * Parses an XML given via its path and returns an {@link AndroidManifestReader} for it. * * @param absolutePath absolute path to an AndroidManifest.xml file * @return an {@code AndroidManifestReader} for {@code path} * @throws IOException */ public static AndroidManifestReader forPath(Path absolutePath) throws IOException { Preconditions.checkArgument( absolutePath.isAbsolute(), "Must be passed absolute path, got %s", absolutePath); try (Reader reader = Files.newBufferedReader(absolutePath)) { return forReader(reader); } } /** * Parses an XML given as a string and returns an {@link AndroidManifestReader} for it. * * @param xmlString a string representation of an XML document * @return an {@code AndroidManifestReader} for the XML document * @throws IOException */ public static AndroidManifestReader forString(String xmlString) throws IOException { return forReader(new StringReader(xmlString)); } private static AndroidManifestReader forReader(Reader reader) throws IOException { return new DefaultAndroidManifestReader(new InputSource(reader)); } }