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);
+  }
 }