AAPT2: Add support to strip namespaces from XML

The --no-xml-namespaces flag will strip namespace information from
compiled binary XML files in res/* (excluding res/raw/*) and
AndroidManifest.xml. It will also strip URI information from compiled
binary XML files in res/* (excluding res/raw/* and AndroidManifest.xml).

AndroidManifest.xml URI information is retained due to PackageParser, which
requires the Android URI for intent filters.

Bug: 29115919
Change-Id: I90cad6ed39ce02a69776f55314c1d4f38ad1aabe
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 4f38e94..b52c530 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -40,6 +40,7 @@
 	link/ReferenceLinker.cpp \
 	link/TableMerger.cpp \
 	link/VersionCollapser.cpp \
+	link/XmlNamespaceRemover.cpp \
 	link/XmlReferenceLinker.cpp \
 	process/SymbolTable.cpp \
 	proto/ProtoHelpers.cpp \
@@ -89,6 +90,7 @@
 	link/ReferenceLinker_test.cpp \
 	link/TableMerger_test.cpp \
 	link/VersionCollapser_test.cpp \
+	link/XmlNamespaceRemover_test.cpp \
 	link/XmlReferenceLinker_test.cpp \
 	process/SymbolTable_test.cpp \
 	proto/TableProtoSerializer_test.cpp \
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 5a2bb6a..ea95dd1 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -74,6 +74,7 @@
     bool generateNonFinalIds = false;
     std::vector<std::string> javadocAnnotations;
     bool outputToDirectory = false;
+    bool noXmlNamespaces = false;
     bool autoAddOverlay = false;
     bool doNotCompressAnything = false;
     std::unordered_set<std::string> extensionsToNotCompress;
@@ -293,6 +294,7 @@
 struct ResourceFileFlattenerOptions {
     bool noAutoVersion = false;
     bool noVersionVectors = false;
+    bool noXmlNamespaces = false;
     bool keepRawValues = false;
     bool doNotCompressAnything = false;
     bool updateProguardSpec = false;
@@ -382,6 +384,13 @@
         return false;
     }
 
+    if (mOptions.noXmlNamespaces) {
+        XmlNamespaceRemover namespaceRemover;
+        if (!namespaceRemover.consume(mContext, outFileOp->xmlToFlatten.get())) {
+            return false;
+        }
+    }
+
     if (!mOptions.noAutoVersion) {
         if (mOptions.noVersionVectors) {
             // Skip this if it is a vector or animated-vector.
@@ -1296,6 +1305,7 @@
         fileFlattenerOptions.extensionsToNotCompress = mOptions.extensionsToNotCompress;
         fileFlattenerOptions.noAutoVersion = mOptions.noAutoVersion;
         fileFlattenerOptions.noVersionVectors = mOptions.noVersionVectors;
+        fileFlattenerOptions.noXmlNamespaces = mOptions.noXmlNamespaces;
         fileFlattenerOptions.updateProguardSpec =
                 static_cast<bool>(mOptions.generateProguardRulesPath);
 
@@ -1594,6 +1604,14 @@
                         error = true;
                     }
                 }
+
+                if (mOptions.noXmlNamespaces) {
+                    // PackageParser will fail if URIs are removed from AndroidManifest.xml.
+                    XmlNamespaceRemover namespaceRemover(true /* keepUris */);
+                    if (!namespaceRemover.consume(mContext, manifestXml.get())) {
+                        error = true;
+                    }
+                }
             } else {
                 error = true;
             }
@@ -1732,6 +1750,9 @@
             .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified "
                             "by -o",
                             &options.outputToDirectory)
+            .optionalSwitch("--no-xml-namespaces", "Removes XML namespace prefix and URI "
+                            "information from AndroidManifest.xml\nand XML binaries in res/*.",
+                            &options.noXmlNamespaces)
             .optionalFlag("--min-sdk-version", "Default minimum SDK version to use for "
                           "AndroidManifest.xml",
                           &options.manifestFixerOptions.minSdkVersionDefault)
diff --git a/tools/aapt2/link/Linkers.h b/tools/aapt2/link/Linkers.h
index 43b8fb4..82e2868 100644
--- a/tools/aapt2/link/Linkers.h
+++ b/tools/aapt2/link/Linkers.h
@@ -86,6 +86,23 @@
 };
 
 /**
+ * Removes namespace nodes and URI information from the XmlResource.
+ *
+ * Once an XmlResource is processed by this consumer, it is no longer able to have its attributes
+ * parsed. As such, this XmlResource must have already been processed by XmlReferenceLinker.
+ */
+class XmlNamespaceRemover : public IXmlResourceConsumer {
+private:
+    bool mKeepUris;
+
+public:
+    XmlNamespaceRemover(bool keepUris = false) : mKeepUris(keepUris) {
+    };
+
+    bool consume(IAaptContext* context, xml::XmlResource* resource) override;
+};
+
+/**
  * Resolves attributes in the XmlResource and compiles string values to resource values.
  * Once an XmlResource is processed by this linker, it is ready to be flattened.
  */
diff --git a/tools/aapt2/link/XmlNamespaceRemover.cpp b/tools/aapt2/link/XmlNamespaceRemover.cpp
new file mode 100644
index 0000000..9f95177
--- /dev/null
+++ b/tools/aapt2/link/XmlNamespaceRemover.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include "ResourceTable.h"
+#include "link/Linkers.h"
+
+#include <algorithm>
+
+namespace aapt {
+
+namespace {
+
+/**
+ * Visits each xml Node, removing URI references and nested namespaces.
+ */
+class XmlVisitor : public xml::Visitor {
+public:
+    XmlVisitor(bool keepUris) : mKeepUris(keepUris) {
+    }
+
+    void visit(xml::Element* el) override {
+        // Strip namespaces
+        for (auto& child : el->children) {
+            while (child && xml::nodeCast<xml::Namespace>(child.get())) {
+                if (child->children.empty()) {
+                    child = {};
+                } else {
+                    child = std::move(child->children.front());
+                    child->parent = el;
+                }
+            }
+        }
+        el->children.erase(std::remove_if(el->children.begin(), el->children.end(),
+                [](const std::unique_ptr<xml::Node>& child) -> bool {
+            return child == nullptr;
+        }), el->children.end());
+
+        if (!mKeepUris) {
+            for (xml::Attribute& attr : el->attributes) {
+                attr.namespaceUri = std::string();
+            }
+            el->namespaceUri = std::string();
+        }
+        xml::Visitor::visit(el);
+    }
+
+private:
+    bool mKeepUris;
+};
+
+} // namespace
+
+bool XmlNamespaceRemover::consume(IAaptContext* context, xml::XmlResource* resource) {
+    if (!resource->root) {
+        return false;
+    }
+    // Replace any root namespaces until the root is a non-namespace node
+    while (xml::nodeCast<xml::Namespace>(resource->root.get())) {
+        if (resource->root->children.empty()) {
+            break;
+        }
+        resource->root = std::move(resource->root->children.front());
+        resource->root->parent = nullptr;
+    }
+    XmlVisitor visitor(mKeepUris);
+    resource->root->accept(&visitor);
+    return true;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/link/XmlNamespaceRemover_test.cpp b/tools/aapt2/link/XmlNamespaceRemover_test.cpp
new file mode 100644
index 0000000..e72ea439
--- /dev/null
+++ b/tools/aapt2/link/XmlNamespaceRemover_test.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include "link/Linkers.h"
+#include "test/Test.h"
+
+namespace aapt {
+
+class XmlUriTestVisitor : public xml::Visitor {
+public:
+    void visit(xml::Element* el) override {
+        for (const auto& attr : el->attributes) {
+            EXPECT_EQ(std::string(), attr.namespaceUri);
+        }
+        EXPECT_EQ(std::string(), el->namespaceUri);
+        xml::Visitor::visit(el);
+    }
+
+    void visit(xml::Namespace* ns) override {
+        EXPECT_EQ(std::string(), ns->namespaceUri);
+        xml::Visitor::visit(ns);
+    }
+};
+
+class XmlNamespaceTestVisitor : public xml::Visitor {
+public:
+    void visit(xml::Namespace* ns) override {
+        ADD_FAILURE() << "Detected namespace: "
+                << ns->namespacePrefix << "=\"" << ns->namespaceUri << "\"";
+        xml::Visitor::visit(ns);
+    }
+};
+
+class XmlNamespaceRemoverTest : public ::testing::Test {
+public:
+    void SetUp() override {
+        mContext = test::ContextBuilder()
+                .setCompilationPackage("com.app.test")
+                .build();
+    }
+
+protected:
+    std::unique_ptr<IAaptContext> mContext;
+};
+
+TEST_F(XmlNamespaceRemoverTest, RemoveUris) {
+    std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF(
+            <View xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:text="hello" />)EOF");
+
+    XmlNamespaceRemover remover;
+    ASSERT_TRUE(remover.consume(mContext.get(), doc.get()));
+
+    xml::Node* root = doc.get()->root.get();
+    ASSERT_NE(root, nullptr);
+
+    XmlUriTestVisitor visitor;
+    root->accept(&visitor);
+}
+
+TEST_F(XmlNamespaceRemoverTest, RemoveNamespaces) {
+    std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF(
+            <View xmlns:android="http://schemas.android.com/apk/res/android"
+                  xmlns:foo="http://schemas.android.com/apk/res/foo"
+                  foo:bar="foobar"
+                  android:text="hello" />)EOF");
+
+    XmlNamespaceRemover remover;
+    ASSERT_TRUE(remover.consume(mContext.get(), doc.get()));
+
+    xml::Node* root = doc.get()->root.get();
+    ASSERT_NE(root, nullptr);
+
+    XmlNamespaceTestVisitor visitor;
+    root->accept(&visitor);
+}
+
+TEST_F(XmlNamespaceRemoverTest, RemoveNestedNamespaces) {
+    std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF(
+            <View xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:text="hello">
+              <View xmlns:foo="http://schemas.example.com/foo"
+                    android:text="foo"/>
+            </View>)EOF");
+
+    XmlNamespaceRemover remover;
+    ASSERT_TRUE(remover.consume(mContext.get(), doc.get()));
+
+    xml::Node* root = doc.get()->root.get();
+    ASSERT_NE(root, nullptr);
+
+    XmlNamespaceTestVisitor visitor;
+    root->accept(&visitor);
+}
+
+} // namespace aapt