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