Add AndroidManifest class to handle parsing and writing dummy manifests.
RELNOTES: None
PiperOrigin-RevId: 210002035
diff --git a/src/test/java/com/google/devtools/build/android/AndroidManifestTest.java b/src/test/java/com/google/devtools/build/android/AndroidManifestTest.java
new file mode 100644
index 0000000..175bb65
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/AndroidManifestTest.java
@@ -0,0 +1,131 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.android;
+
+import static java.nio.file.StandardOpenOption.CREATE_NEW;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.jimfs.Jimfs;
+import com.google.common.truth.Truth;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the AndroidManifest class */
+@RunWith(JUnit4.class)
+public class AndroidManifestTest {
+
+ private FileSystem fileSystem;
+
+ @Before
+ public void setUp() throws Exception {
+ fileSystem = Jimfs.newFileSystem();
+ }
+
+ @Test
+ public void parseMinSdkAndPackageName() throws Exception {
+ final String packageName = "com.google.wooga";
+ final String minSdk = "26";
+ final AndroidManifest androidManifest =
+ AndroidManifest.parseFrom(manifest(packageName, minSdk).write(fileSystem.getPath("tmp")));
+ Truth.assertThat(androidManifest).isEqualTo(AndroidManifest.of(packageName, minSdk));
+ }
+
+ @Test
+ public void parseMissingMinSdk() throws Exception {
+ final String packageName = "com.google.wooga";
+ final AndroidManifest androidManifest =
+ AndroidManifest.parseFrom(manifest(packageName).write(fileSystem.getPath("tmp")));
+
+ Truth.assertThat(androidManifest).isEqualTo(AndroidManifest.of(packageName, "1"));
+ }
+
+ @Test
+ public void writeDummyManifestWithoutPlaceholdersNoMinSdk() throws Exception {
+ final String packageName = "${applicationId}.wooga";
+ final String packageForR = "com.google.android.wooga";
+ final AndroidManifest androidManifest =
+ AndroidManifest.parseFrom(manifest(packageName).write(fileSystem.getPath("tmp")));
+
+ Truth.assertThat(
+ Files.readAllLines(
+ androidManifest.writeDummyManifestForAapt(
+ fileSystem.getPath("dummy-manifest"), packageForR),
+ StandardCharsets.UTF_8))
+ .containsExactly(
+ "<?xml version='1.0' encoding='utf-8'?>",
+ "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
+ "package='com.google.android.wooga'>",
+ "<application/>",
+ "<uses-sdk android:minSdkVersion='1'/>",
+ "</manifest>")
+ .inOrder();
+ }
+
+ @Test
+ public void writeDummyManifestWithoutPlaceholdersAndMinSdk() throws Exception {
+ final String packageName = "${applicationId}.wooga";
+ final String packageForR = "com.google.android.wooga";
+ final AndroidManifest androidManifest =
+ AndroidManifest.parseFrom(manifest(packageName, "26").write(fileSystem.getPath("tmp")));
+
+ Truth.assertThat(
+ Files.readAllLines(
+ androidManifest.writeDummyManifestForAapt(
+ fileSystem.getPath("dummy-manifest"), packageForR),
+ StandardCharsets.UTF_8))
+ .containsExactly(
+ "<?xml version='1.0' encoding='utf-8'?>",
+ "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
+ "package='com.google.android.wooga'>",
+ "<application/>",
+ "<uses-sdk android:minSdkVersion='26'/>",
+ "</manifest>")
+ .inOrder();
+ }
+
+ private static Manifest manifest(String pkg) {
+ return manifest(pkg, "");
+ }
+
+ private static Manifest manifest(String pkg, String minSdk, String... lines) {
+ return parent ->
+ Files.write(
+ Files.createDirectories(parent).resolve("AndroidManifest.xml"),
+ ImmutableList.<String>builder()
+ .add(
+ "<?xml version='1.0' encoding='utf-8'?>",
+ "<manifest",
+ " xmlns:android='http://schemas.android.com/apk/res/android'",
+ String.format(" package='%s'>", pkg),
+ "<application>",
+ (minSdk == null || minSdk.isEmpty())
+ ? ""
+ : String.format("<uses-sdk android:minSdkVersion='%s'/>", minSdk))
+ .add(lines)
+ .add("</application>", "</manifest>")
+ .build(),
+ CREATE_NEW);
+ }
+
+ private interface Manifest {
+ Path write(Path parent) throws IOException;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/android/BUILD b/src/test/java/com/google/devtools/build/android/BUILD
index a4ace24..e8f3b73 100644
--- a/src/test/java/com/google/devtools/build/android/BUILD
+++ b/src/test/java/com/google/devtools/build/android/BUILD
@@ -41,6 +41,20 @@
)
java_test(
+ name = "AndroidManifestTest",
+ size = "small",
+ srcs = ["AndroidManifestTest.java"],
+ deps = [
+ ":test_utils",
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:guava",
+ "//third_party:jimfs",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
name = "RClassGeneratorActionTest",
size = "small",
srcs = ["RClassGeneratorActionTest.java"],
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidManifest.java b/src/tools/android/java/com/google/devtools/build/android/AndroidManifest.java
new file mode 100644
index 0000000..3998acc
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidManifest.java
@@ -0,0 +1,148 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.android;
+
+import static com.google.common.base.Predicates.not;
+
+import com.android.SdkConstants;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.logging.Logger;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.Attribute;
+import javax.xml.stream.events.StartElement;
+import javax.xml.stream.events.XMLEvent;
+
+/** Represents an AndroidManifest in the context of resource link and compilation. */
+public class AndroidManifest {
+
+ private static final Logger logger = Logger.getLogger(AndroidManifest.class.getName());
+
+ private static final QName PACKAGE_NAME = QName.valueOf("package");
+ private static final QName MANIFEST = QName.valueOf("manifest");
+ private static final QName USES_SDK = QName.valueOf("uses-sdk");
+ private static final QName MIN_SDK =
+ new QName(SdkConstants.ANDROID_URI, "minSdkVersion", "android");
+ private final String packageName;
+ private final String minSdk;
+
+ private AndroidManifest(String packageName, String minSdk) {
+ this.packageName = packageName;
+ this.minSdk = minSdk;
+ }
+
+ /** Parses the manifest from the path. */
+ public static AndroidManifest parseFrom(Path manifest) {
+ try (InputStream input = Files.newInputStream(manifest)) {
+ final XMLEventReader xmlEventReader =
+ XmlResourceValues.getXmlInputFactory().createXMLEventReader(input);
+
+ String packageName = "";
+ String minSdk = "";
+ while (xmlEventReader.hasNext()) {
+ final XMLEvent xmlEvent = xmlEventReader.nextEvent();
+ if (xmlEvent.isStartElement()) {
+ final StartElement element = xmlEvent.asStartElement();
+ if (MANIFEST.equals(element.getName())) {
+ packageName = getAttributeValue(xmlEvent, PACKAGE_NAME).orElse(packageName);
+ }
+ if (USES_SDK.equals(element.getName())) {
+ minSdk = getAttributeValue(xmlEvent, MIN_SDK).orElse(minSdk);
+ }
+ }
+ }
+
+ if (minSdk.isEmpty()) {
+ logger.warning(
+ String.format(
+ "\n\u001B[31mCONFIGURATION:\u001B[0m" + " %s has no minSdkVersion. Using 1.",
+ manifest));
+ }
+
+ return new AndroidManifest(packageName, minSdk.isEmpty() ? "1" : minSdk);
+ } catch (IOException | XMLStreamException e) {
+ throw new ManifestProcessingException(e);
+ }
+ }
+
+ private static Optional<String> getAttributeValue(XMLEvent xmlEvent, QName attributeName) {
+ return Optional.ofNullable(xmlEvent.asStartElement().getAttributeByName(attributeName))
+ .map(Attribute::getValue)
+ .filter(not(String::isEmpty));
+ }
+
+ public static AndroidManifest asEmpty() {
+ return new AndroidManifest("", "1");
+ }
+
+ public static AndroidManifest of(String packageName, String minSdk) {
+ return new AndroidManifest(packageName, minSdk);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AndroidManifest)) {
+ return false;
+ }
+ AndroidManifest that = (AndroidManifest) o;
+ return Objects.equals(packageName, that.packageName) && Objects.equals(minSdk, that.minSdk);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("packageName", packageName)
+ .add("minSdk", minSdk)
+ .toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, minSdk);
+ }
+
+ /** Creates a dummy manifest for processing without any manifest template variables. */
+ public Path writeDummyManifestForAapt(Path workingDirectory, String packageForR) {
+ try {
+ return Files.write(
+ Files.createDirectories(workingDirectory).resolve("AndroidManifest.xml"),
+ ImmutableList.of(
+ "<?xml version='1.0' encoding='utf-8'?>",
+ "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
+ String.format(
+ "package='%s'>", Strings.isNullOrEmpty(packageForR) ? packageName : packageForR),
+ "<application/>",
+ Strings.isNullOrEmpty(minSdk)
+ ? ""
+ : String.format("<uses-sdk android:minSdkVersion='%s'/>", minSdk),
+ "</manifest>"),
+ StandardOpenOption.CREATE_NEW);
+ } catch (IOException e) {
+ throw new AndroidManifestProcessor.ManifestProcessingException(e);
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java b/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java
index 1530959..893710b 100644
--- a/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java
+++ b/src/tools/android/java/com/google/devtools/build/android/ManifestProcessingException.java
@@ -14,7 +14,7 @@
package com.google.devtools.build.android;
/** Indicates an error parsing or modifying an AndroidManifest.xml. */
-public class ManifestProcessingException extends Exception {
+public class ManifestProcessingException extends RuntimeException {
public ManifestProcessingException() {
super();
@@ -23,4 +23,8 @@
public ManifestProcessingException(String message) {
super(message);
}
+
+ public ManifestProcessingException(Exception e) {
+ super(e);
+ }
}