AAPT2: Add support to specify stable IDs

The --stable-ids flag allows the user to specify a file containing
a list of resource name and resource ID pairs in the form of:

package:type/name = 0xPPTTEEEE

This assigns the given resource the specified ID. It helps ensure
that when adding or removing resources, IDs are assigned in a stable
fashion.

If a package, type, or name is not found, no error or warning is
raised.

Change-Id: Ibc2f4e05cc924be255fedd862d835cb5b18d7584
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 8093e6a..ded661e 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -44,10 +44,12 @@
 #include "util/StringPiece.h"
 #include "xml/XmlDom.h"
 
+#include <android-base/file.h>
 #include <google/protobuf/io/coded_stream.h>
 
 #include <fstream>
 #include <sys/stat.h>
+#include <unordered_map>
 #include <vector>
 
 namespace aapt {
@@ -76,6 +78,8 @@
     ManifestFixerOptions manifestFixerOptions;
     std::unordered_set<std::string> products;
     TableSplitterOptions tableSplitterOptions;
+    std::unordered_map<ResourceName, ResourceId> stableIdMap;
+    Maybe<std::string> resourceIdMapPath;
 };
 
 class LinkContext : public IAaptContext {
@@ -517,6 +521,77 @@
     return !error;
 }
 
+static bool writeStableIdMapToPath(IDiagnostics* diag,
+                                   const std::unordered_map<ResourceName, ResourceId>& idMap,
+                                   const std::string idMapPath) {
+    std::ofstream fout(idMapPath, std::ofstream::binary);
+    if (!fout) {
+        diag->error(DiagMessage(idMapPath) << strerror(errno));
+        return false;
+    }
+
+    for (const auto& entry : idMap) {
+        const ResourceName& name = entry.first;
+        const ResourceId& id = entry.second;
+        fout << name << " = " << id << "\n";
+    }
+
+    if (!fout) {
+        diag->error(DiagMessage(idMapPath) << "failed writing to file: " << strerror(errno));
+        return false;
+    }
+
+    return true;
+}
+
+static bool loadStableIdMap(IDiagnostics* diag, const std::string& path,
+                            std::unordered_map<ResourceName, ResourceId>* outIdMap) {
+    std::string content;
+    if (!android::base::ReadFileToString(path, &content)) {
+        diag->error(DiagMessage(path) << "failed reading stable ID file");
+        return false;
+    }
+
+    outIdMap->clear();
+    size_t lineNo = 0;
+    for (StringPiece line : util::tokenize(content, '\n')) {
+        lineNo++;
+        line = util::trimWhitespace(line);
+        if (line.empty()) {
+            continue;
+        }
+
+        auto iter = std::find(line.begin(), line.end(), '=');
+        if (iter == line.end()) {
+            diag->error(DiagMessage(Source(path, lineNo)) << "missing '='");
+            return false;
+        }
+
+        ResourceNameRef name;
+        StringPiece resNameStr = util::trimWhitespace(
+                line.substr(0, std::distance(line.begin(), iter)));
+        if (!ResourceUtils::parseResourceName(resNameStr, &name)) {
+            diag->error(DiagMessage(Source(path, lineNo))
+                        << "invalid resource name '" << resNameStr << "'");
+            return false;
+        }
+
+        const size_t resIdStartIdx = std::distance(line.begin(), iter) + 1;
+        const size_t resIdStrLen = line.size() - resIdStartIdx;
+        StringPiece resIdStr = util::trimWhitespace(line.substr(resIdStartIdx, resIdStrLen));
+
+        Maybe<ResourceId> maybeId = ResourceUtils::tryParseResourceId(resIdStr);
+        if (!maybeId) {
+            diag->error(DiagMessage(Source(path, lineNo)) << "invalid resource ID '"
+                        << resIdStr << "'");
+            return false;
+        }
+
+        (*outIdMap)[name.toResourceName()] = maybeId.value();
+    }
+    return true;
+}
+
 class LinkCommand {
 public:
     LinkCommand(LinkContext* context, const LinkOptions& options) :
@@ -1176,11 +1251,32 @@
 
         if (!mOptions.staticLib) {
             // Assign IDs if we are building a regular app.
-            IdAssigner idAssigner;
+            IdAssigner idAssigner(&mOptions.stableIdMap);
             if (!idAssigner.consume(mContext, &mFinalTable)) {
                 mContext->getDiagnostics()->error(DiagMessage() << "failed assigning IDs");
                 return 1;
             }
+
+            // Now grab each ID and emit it as a file.
+            if (mOptions.resourceIdMapPath) {
+                for (auto& package : mFinalTable.packages) {
+                    for (auto& type : package->types) {
+                        for (auto& entry : type->entries) {
+                            ResourceName name(package->name, type->type, entry->name);
+                            // The IDs are guaranteed to exist.
+                            mOptions.stableIdMap[std::move(name)] = ResourceId(package->id.value(),
+                                                                               type->id.value(),
+                                                                               entry->id.value());
+                        }
+                    }
+                }
+
+                if (!writeStableIdMapToPath(mContext->getDiagnostics(),
+                                            mOptions.stableIdMap,
+                                            mOptions.resourceIdMapPath.value())) {
+                    return 1;
+                }
+            }
         } else {
             // Static libs are merged with other apps, and ID collisions are bad, so verify that
             // no IDs have been set.
@@ -1437,6 +1533,7 @@
     bool legacyXFlag = false;
     bool requireLocalization = false;
     bool verbose = false;
+    Maybe<std::string> stableIdFilePath;
     Flags flags = Flags()
             .requiredFlag("-o", "Output path", &options.outputPath)
             .requiredFlag("--manifest", "Path to the Android manifest to build",
@@ -1493,6 +1590,11 @@
             .optionalSwitch("--non-final-ids", "Generates R.java without the final modifier.\n"
                             "This is implied when --static-lib is specified.",
                             &options.generateNonFinalIds)
+            .optionalFlag("--stable-ids", "File containing a list of name to ID mapping.",
+                          &stableIdFilePath)
+            .optionalFlag("--emit-ids", "Emit a file at the given path with a list of name to ID\n"
+                          "mappings, suitable for use with --stable-ids.",
+                          &options.resourceIdMapPath)
             .optionalFlag("--private-symbols", "Package name to use when generating R.java for "
                           "private symbols.\n"
                           "If not specified, public and private symbols will use the application's "
@@ -1619,6 +1721,13 @@
         options.tableSplitterOptions.preferredDensity = preferredDensityConfig.density;
     }
 
+    if (!options.staticLib && stableIdFilePath) {
+        if (!loadStableIdMap(context.getDiagnostics(), stableIdFilePath.value(),
+                             &options.stableIdMap)) {
+            return 1;
+        }
+    }
+
     // Turn off auto versioning for static-libs.
     if (options.staticLib) {
         options.noAutoVersion = true;