Implement AAPT Bundle format

AAPT will scan XML files looking for the <aapt:attr> XML tag.

<!-- @layout/bundle.xml -->
<ImageView xmlns:aapt="http://schemas.android.com/aapt">
  <aapt:attr name="android:src">
    <vector android:pathData="..." ...>
    </vector>
  </aapt:attr>
</ImageView>

The SINGLE child element of the <aapt:attr> tag is extracted into its own top
level resource. It is given a generated name.

The parent element of <aapt:attr> is then given the resource attribute that was assigned
to the `name' attribute. The value is set to a reference to the generated resource.

<!-- @layout/bundle.xml -->
<ImageView android:src="@drawable/bundle_1.xml">
</ImageView>

<!-- @layout/bundle_1.xml -->
<vector android:pathData="..." ...>
</vector>

Bug:22627686
Change-Id: I8575fc4f739011402662fbf6b3db96df0012f598
diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp
index e22e76d..fb0fe38 100644
--- a/tools/aapt/Resource.cpp
+++ b/tools/aapt/Resource.cpp
@@ -1537,12 +1537,20 @@
     std::queue<CompileResourceWorkItem>& workQueue = table.getWorkQueue();
     while (!workQueue.empty()) {
         CompileResourceWorkItem& workItem = workQueue.front();
-        err = compileXmlFile(bundle, assets, workItem.resourceName, workItem.file, &table, xmlFlags);
+        int xmlCompilationFlags = xmlFlags | XML_COMPILE_PARSE_VALUES
+                | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS;
+        if (!workItem.needsCompiling) {
+            xmlCompilationFlags &= ~XML_COMPILE_ASSIGN_ATTRIBUTE_IDS;
+            xmlCompilationFlags &= ~XML_COMPILE_PARSE_VALUES;
+        }
+        err = compileXmlFile(bundle, assets, workItem.resourceName, workItem.xmlRoot,
+                             workItem.file, &table, xmlCompilationFlags);
+
         if (err == NO_ERROR) {
             assets->addResource(workItem.resPath.getPathLeaf(),
-                    workItem.resPath,
-                    workItem.file,
-                    workItem.file->getResourceType());
+                                workItem.resPath,
+                                workItem.file,
+                                workItem.file->getResourceType());
         } else {
             hasErrors = true;
         }
@@ -1737,9 +1745,7 @@
             manifestFile->getGroupEntry(),
             manifestFile->getResourceType());
     err = compileXmlFile(bundle, assets, String16(), manifestFile,
-            outManifestFile, &table,
-            XML_COMPILE_ASSIGN_ATTRIBUTE_IDS
-            | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES);
+            outManifestFile, &table, XML_COMPILE_STANDARD_RESOURCE & ~XML_COMPILE_STRIP_COMMENTS);
     if (err < NO_ERROR) {
         return err;
     }
diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp
index d5a09d8..0e470d9 100644
--- a/tools/aapt/ResourceTable.cpp
+++ b/tools/aapt/ResourceTable.cpp
@@ -88,8 +88,11 @@
         root->setUTF8(true);
     }
 
-    bool hasErrors = false;
+    if (table->processBundleFormat(bundle, resourceName, target, root) != NO_ERROR) {
+        return UNKNOWN_ERROR;
+    }
     
+    bool hasErrors = false;
     if ((options&XML_COMPILE_ASSIGN_ATTRIBUTE_IDS) != 0) {
         status_t err = root->assignResourceIds(assets, table);
         if (err != NO_ERROR) {
@@ -97,9 +100,11 @@
         }
     }
 
-    status_t err = root->parseValues(assets, table);
-    if (err != NO_ERROR) {
-        hasErrors = true;
+    if ((options&XML_COMPILE_PARSE_VALUES) != 0) {
+        status_t err = root->parseValues(assets, table);
+        if (err != NO_ERROR) {
+            hasErrors = true;
+        }
     }
 
     if (hasErrors) {
@@ -114,7 +119,7 @@
         printf("Input XML Resource:\n");
         root->print();
     }
-    err = root->flatten(target,
+    status_t err = root->flatten(target,
             (options&XML_COMPILE_STRIP_COMMENTS) != 0,
             (options&XML_COMPILE_STRIP_RAW_VALUES) != 0);
     if (err != NO_ERROR) {
@@ -4755,9 +4760,9 @@
         newConfig.sdkVersion = sdkVersionToGenerate;
         sp<AaptFile> newFile = new AaptFile(target->getSourceFile(),
                 AaptGroupEntry(newConfig), target->getResourceType());
-        String8 resPath = String8::format("res/%s/%s",
+        String8 resPath = String8::format("res/%s/%s.xml",
                 newFile->getGroupEntry().toDirName(target->getResourceType()).string(),
-                target->getSourceFile().getPathLeaf().string());
+                String8(resourceName).string());
         resPath.convertToResPath();
 
         // Add a resource table entry.
@@ -4784,9 +4789,11 @@
         item.resourceName = resourceName;
         item.resPath = resPath;
         item.file = newFile;
+        item.xmlRoot = newRoot;
+        item.needsCompiling = false;    // This step occurs after we parse/assign, so we don't need
+                                        // to do it again.
         mWorkQueue.push(item);
     }
-
     return NO_ERROR;
 }
 
@@ -4825,3 +4832,226 @@
         }
     }
 }
+
+static String16 buildNamespace(const String16& package) {
+    return String16("http://schemas.android.com/apk/res/") + package;
+}
+
+static sp<XMLNode> findOnlyChildElement(const sp<XMLNode>& parent) {
+    const Vector<sp<XMLNode> >& children = parent->getChildren();
+    sp<XMLNode> onlyChild;
+    for (size_t i = 0; i < children.size(); i++) {
+        if (children[i]->getType() != XMLNode::TYPE_CDATA) {
+            if (onlyChild != NULL) {
+                return NULL;
+            }
+            onlyChild = children[i];
+        }
+    }
+    return onlyChild;
+}
+
+/**
+ * Detects use of the `bundle' format and extracts nested resources into their own top level
+ * resources. The bundle format looks like this:
+ *
+ * <!-- res/drawable/bundle.xml -->
+ * <animated-vector xmlns:aapt="http://schemas.android.com/aapt">
+ *   <aapt:attr name="android:drawable">
+ *     <vector android:width="60dp"
+ *             android:height="60dp">
+ *       <path android:name="v"
+ *             android:fillColor="#000000"
+ *             android:pathData="M300,70 l 0,-70 70,..." />
+ *     </vector>
+ *   </aapt:attr>
+ * </animated-vector>
+ *
+ * When AAPT sees the <aapt:attr> tag, it will extract its single element and its children
+ * into a new high-level resource, assigning it a name and ID. Then value of the `name`
+ * attribute must be a resource attribute. That resource attribute is inserted into the parent
+ * with the reference to the extracted resource as the value.
+ *
+ * <!-- res/drawable/bundle.xml -->
+ * <animated-vector android:drawable="@drawable/bundle_1.xml">
+ * </animated-vector>
+ *
+ * <!-- res/drawable/bundle_1.xml -->
+ * <vector android:width="60dp"
+ *         android:height="60dp">
+ *   <path android:name="v"
+ *         android:fillColor="#000000"
+ *         android:pathData="M300,70 l 0,-70 70,..." />
+ * </vector>
+ */
+status_t ResourceTable::processBundleFormat(const Bundle* bundle,
+                                            const String16& resourceName,
+                                            const sp<AaptFile>& target,
+                                            const sp<XMLNode>& root) {
+    Vector<sp<XMLNode> > namespaces;
+    if (root->getType() == XMLNode::TYPE_NAMESPACE) {
+        namespaces.push(root);
+    }
+    return processBundleFormatImpl(bundle, resourceName, target, root, &namespaces);
+}
+
+status_t ResourceTable::processBundleFormatImpl(const Bundle* bundle,
+                                                const String16& resourceName,
+                                                const sp<AaptFile>& target,
+                                                const sp<XMLNode>& parent,
+                                                Vector<sp<XMLNode> >* namespaces) {
+    const String16 kAaptNamespaceUri16("http://schemas.android.com/aapt");
+    const String16 kName16("name");
+    const String16 kAttr16("attr");
+    const String16 kAssetPackage16(mAssets->getPackage());
+
+    Vector<sp<XMLNode> >& children = parent->getChildren();
+    for (size_t i = 0; i < children.size(); i++) {
+        const sp<XMLNode>& child = children[i];
+
+        if (child->getType() == XMLNode::TYPE_CDATA) {
+            continue;
+        } else if (child->getType() == XMLNode::TYPE_NAMESPACE) {
+            namespaces->push(child);
+        }
+
+        if (child->getElementNamespace() != kAaptNamespaceUri16 ||
+                child->getElementName() != kAttr16) {
+            status_t result = processBundleFormatImpl(bundle, resourceName, target, child,
+                                                      namespaces);
+            if (result != NO_ERROR) {
+                return result;
+            }
+
+            if (child->getType() == XMLNode::TYPE_NAMESPACE) {
+                namespaces->pop();
+            }
+            continue;
+        }
+
+        // This is the <aapt:attr> tag. Look for the 'name' attribute.
+        SourcePos source(child->getFilename(), child->getStartLineNumber());
+
+        sp<XMLNode> nestedRoot = findOnlyChildElement(child);
+        if (nestedRoot == NULL) {
+            source.error("<%s:%s> must have exactly one child element",
+                         String8(child->getElementNamespace()).string(),
+                         String8(child->getElementName()).string());
+            return UNKNOWN_ERROR;
+        }
+
+        // Find the special attribute 'parent-attr'. This attribute's value contains
+        // the resource attribute for which this element should be assigned in the parent.
+        const XMLNode::attribute_entry* attr = child->getAttribute(String16(), kName16);
+        if (attr == NULL) {
+            source.error("inline resource definition must specify an attribute via 'name'");
+            return UNKNOWN_ERROR;
+        }
+
+        // Parse the attribute name.
+        const char* errorMsg = NULL;
+        String16 attrPackage, attrType, attrName;
+        bool result = ResTable::expandResourceRef(attr->string.string(),
+                                                  attr->string.size(),
+                                                  &attrPackage, &attrType, &attrName,
+                                                  &kAttr16, &kAssetPackage16,
+                                                  &errorMsg, NULL);
+        if (!result) {
+            source.error("invalid attribute name for 'name': %s", errorMsg);
+            return UNKNOWN_ERROR;
+        }
+
+        if (attrType != kAttr16) {
+            // The value of the 'name' attribute must be an attribute reference.
+            source.error("value of 'name' must be an attribute reference.");
+            return UNKNOWN_ERROR;
+        }
+
+        // Generate a name for this nested resource and try to add it to the table.
+        // We do this in a loop because the name may be taken, in which case we will
+        // increment a suffix until we succeed.
+        String8 nestedResourceName;
+        String8 nestedResourcePath;
+        int suffix = 1;
+        while (true) {
+            // This child element will be extracted into its own resource file.
+            // Generate a name and path for it from its parent.
+            nestedResourceName = String8::format("%s_%d",
+                        String8(resourceName).string(), suffix++);
+            nestedResourcePath = String8::format("res/%s/%s.xml",
+                        target->getGroupEntry().toDirName(target->getResourceType())
+                                               .string(),
+                        nestedResourceName.string());
+
+            // Lookup or create the entry for this name.
+            sp<Entry> entry = getEntry(kAssetPackage16,
+                                       String16(target->getResourceType()),
+                                       String16(nestedResourceName),
+                                       source,
+                                       false,
+                                       &target->getGroupEntry().toParams(),
+                                       true);
+            if (entry == NULL) {
+                return UNKNOWN_ERROR;
+            }
+
+            if (entry->getType() == Entry::TYPE_UNKNOWN) {
+                // The value for this resource has never been set,
+                // meaning we're good!
+                entry->setItem(source, String16(nestedResourcePath));
+                break;
+            }
+
+            // We failed (name already exists), so try with a different name
+            // (increment the suffix).
+        }
+
+        if (bundle->getVerbose()) {
+            source.printf("generating nested resource %s:%s/%s",
+                    mAssets->getPackage().string(), target->getResourceType().string(),
+                    nestedResourceName.string());
+        }
+
+        // Build the attribute reference and assign it to the parent.
+        String16 nestedResourceRef = String16(String8::format("@%s:%s/%s",
+                    mAssets->getPackage().string(), target->getResourceType().string(),
+                    nestedResourceName.string()));
+
+        String16 attrNs = buildNamespace(attrPackage);
+        if (parent->getAttribute(attrNs, attrName) != NULL) {
+            SourcePos(parent->getFilename(), parent->getStartLineNumber())
+                    .error("parent of nested resource already defines attribute '%s:%s'",
+                           String8(attrPackage).string(), String8(attrName).string());
+            return UNKNOWN_ERROR;
+        }
+
+        // Add the reference to the inline resource.
+        parent->addAttribute(attrNs, attrName, nestedResourceRef);
+
+        // Remove the <aapt:attr> child element from here.
+        children.removeAt(i);
+        i--;
+
+        // Append all namespace declarations that we've seen on this branch in the XML tree
+        // to this resource.
+        // We do this because the order of namespace declarations and prefix usage is determined
+        // by the developer and we do not want to override any decisions. Be conservative.
+        for (size_t nsIndex = namespaces->size(); nsIndex > 0; nsIndex--) {
+            const sp<XMLNode>& ns = namespaces->itemAt(nsIndex - 1);
+            sp<XMLNode> newNs = XMLNode::newNamespace(ns->getFilename(), ns->getNamespacePrefix(),
+                                                      ns->getNamespaceUri());
+            newNs->addChild(nestedRoot);
+            nestedRoot = newNs;
+        }
+
+        // Schedule compilation of the nested resource.
+        CompileResourceWorkItem workItem;
+        workItem.resPath = nestedResourcePath;
+        workItem.resourceName = String16(nestedResourceName);
+        workItem.xmlRoot = nestedRoot;
+        workItem.file = new AaptFile(target->getSourceFile(), target->getGroupEntry(),
+                                     target->getResourceType());
+        mWorkQueue.push(workItem);
+    }
+    return NO_ERROR;
+}
diff --git a/tools/aapt/ResourceTable.h b/tools/aapt/ResourceTable.h
index c4bdf09..4b7b3cd 100644
--- a/tools/aapt/ResourceTable.h
+++ b/tools/aapt/ResourceTable.h
@@ -23,13 +23,14 @@
 enum {
     XML_COMPILE_STRIP_COMMENTS = 1<<0,
     XML_COMPILE_ASSIGN_ATTRIBUTE_IDS = 1<<1,
-    XML_COMPILE_COMPACT_WHITESPACE = 1<<2,
-    XML_COMPILE_STRIP_WHITESPACE = 1<<3,
-    XML_COMPILE_STRIP_RAW_VALUES = 1<<4,
-    XML_COMPILE_UTF8 = 1<<5,
+    XML_COMPILE_PARSE_VALUES = 1 << 2,
+    XML_COMPILE_COMPACT_WHITESPACE = 1<<3,
+    XML_COMPILE_STRIP_WHITESPACE = 1<<4,
+    XML_COMPILE_STRIP_RAW_VALUES = 1<<5,
+    XML_COMPILE_UTF8 = 1<<6,
 
     XML_COMPILE_STANDARD_RESOURCE =
-            XML_COMPILE_STRIP_COMMENTS | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS
+            XML_COMPILE_STRIP_COMMENTS | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS | XML_COMPILE_PARSE_VALUES
             | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES
 };
 
@@ -83,6 +84,8 @@
     String16 resourceName;
     String8 resPath;
     sp<AaptFile> file;
+    sp<XMLNode> xmlRoot;
+    bool needsCompiling = true;
 };
 
 class ResourceTable : public ResTable::Accessor
@@ -206,6 +209,12 @@
                              const sp<AaptFile>& file,
                              const sp<XMLNode>& root);
 
+    status_t processBundleFormat(const Bundle* bundle,
+                                 const String16& resourceName,
+                                 const sp<AaptFile>& file,
+                                 const sp<XMLNode>& parent);
+
+
     sp<AaptFile> flatten(Bundle* bundle, const sp<const ResourceFilter>& filter,
             const bool isBase);
 
@@ -586,6 +595,11 @@
                       Res_value* outValue);
     int getPublicAttributeSdkLevel(uint32_t attrId) const;
 
+    status_t processBundleFormatImpl(const Bundle* bundle,
+                                     const String16& resourceName,
+                                     const sp<AaptFile>& file,
+                                     const sp<XMLNode>& parent,
+                                     Vector<sp<XMLNode> >* namespaces);
 
     String16 mAssetsPackage;
     PackageType mPackageType;
diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp
index dc08eb8..5b215da 100644
--- a/tools/aapt/XMLNode.cpp
+++ b/tools/aapt/XMLNode.cpp
@@ -693,6 +693,12 @@
     return mChildren;
 }
 
+
+Vector<sp<XMLNode> >& XMLNode::getChildren()
+{
+    return mChildren;
+}
+
 const String8& XMLNode::getFilename() const
 {
     return mFilename;
@@ -717,6 +723,18 @@
     return NULL;
 }
 
+bool XMLNode::removeAttribute(const String16& ns, const String16& name)
+{
+    for (size_t i = 0; i < mAttributes.size(); i++) {
+        const attribute_entry& ae(mAttributes.itemAt(i));
+        if (ae.ns == ns && ae.name == name) {
+            removeAttribute(i);
+            return true;
+        }
+    }
+    return false;
+}
+
 XMLNode::attribute_entry* XMLNode::editAttribute(const String16& ns,
         const String16& name)
 {
diff --git a/tools/aapt/XMLNode.h b/tools/aapt/XMLNode.h
index b9e5cd5..749bf9f 100644
--- a/tools/aapt/XMLNode.h
+++ b/tools/aapt/XMLNode.h
@@ -55,7 +55,7 @@
     sp<XMLNode> newCData(const String8& filename) {
         return new XMLNode(filename);
     }
-    
+
     enum type {
         TYPE_NAMESPACE,
         TYPE_ELEMENT,
@@ -70,6 +70,7 @@
     const String16& getElementNamespace() const;
     const String16& getElementName() const;
     const Vector<sp<XMLNode> >& getChildren() const;
+    Vector<sp<XMLNode> >& getChildren();
 
     const String8& getFilename() const;
     
@@ -97,6 +98,7 @@
     const Vector<attribute_entry>& getAttributes() const;
 
     const attribute_entry* getAttribute(const String16& ns, const String16& name) const;
+    bool removeAttribute(const String16& ns, const String16& name);
     
     attribute_entry* editAttribute(const String16& ns, const String16& name);