Merge "AAPT2: Add diff command" into nyc-mr1-dev am: 1a1461058e
am: 894f96a117

* commit '894f96a117a9ba942b9486317f6852c7472443a6':
  AAPT2: Add diff command

Change-Id: Ic53915ac3c04d151f55c159a170d46da7a1f6c17
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 3a1e2bb..4b5ea65 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -115,6 +115,7 @@
 
 toolSources := \
 	compile/Compile.cpp \
+	diff/Diff.cpp \
 	dump/Dump.cpp \
 	link/Link.cpp
 
diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp
index a2fadd9..00d8aae 100644
--- a/tools/aapt2/Main.cpp
+++ b/tools/aapt2/Main.cpp
@@ -24,6 +24,7 @@
 extern int compile(const std::vector<StringPiece>& args);
 extern int link(const std::vector<StringPiece>& args);
 extern int dump(const std::vector<StringPiece>& args);
+extern int diff(const std::vector<StringPiece>& args);
 
 } // namespace aapt
 
@@ -44,12 +45,14 @@
             return aapt::link(args);
         } else if (command == "dump" || command == "d") {
             return aapt::dump(args);
+        } else if (command == "diff") {
+            return aapt::diff(args);
         }
         std::cerr << "unknown command '" << command << "'\n";
     } else {
         std::cerr << "no command specified\n";
     }
 
-    std::cerr << "\nusage: aapt2 [compile|link|dump] ..." << std::endl;
+    std::cerr << "\nusage: aapt2 [compile|link|dump|diff] ..." << std::endl;
     return 1;
 }
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 9704d970..a84c306 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -152,7 +152,7 @@
                 break;
             }
 
-            spanStack.back().lastChar = builder.str().size();
+            spanStack.back().lastChar = builder.str().size() - 1;
             outStyleString->spans.push_back(spanStack.back());
             spanStack.pop_back();
 
@@ -1058,6 +1058,16 @@
 
     std::unique_ptr<Array> array = util::make_unique<Array>();
 
+    bool translateable = mOptions.translatable;
+    if (Maybe<StringPiece16> translateableAttr = xml::findAttribute(parser, u"translatable")) {
+        if (!ResourceUtils::tryParseBool(translateableAttr.value(), &translateable)) {
+            mDiag->error(DiagMessage(outResource->source)
+                         << "invalid value for 'translatable'. Must be a boolean");
+            return false;
+        }
+    }
+    array->setTranslateable(translateable);
+
     bool error = false;
     const size_t depth = parser->getDepth();
     while (xml::XmlPullParser::nextChildNode(parser, depth)) {
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 8d734f3..e700ed9 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -189,6 +189,17 @@
     return results;
 }
 
+std::vector<ResourceConfigValue*> ResourceEntry::findValuesIf(
+        const std::function<bool(ResourceConfigValue*)>& f) {
+    std::vector<ResourceConfigValue*> results;
+    for (auto& configValue : values) {
+        if (f(configValue.get())) {
+            results.push_back(configValue.get());
+        }
+    }
+    return results;
+}
+
 /**
  * The default handler for collisions. A return value of -1 means keep the
  * existing value, 0 means fail, and +1 means take the incoming value.
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 7f5c2b8..5690ea6 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -26,6 +26,7 @@
 #include "io/File.h"
 
 #include <android-base/macros.h>
+#include <functional>
 #include <map>
 #include <memory>
 #include <string>
@@ -109,6 +110,9 @@
     ResourceConfigValue* findOrCreateValue(const ConfigDescription& config,
                                            const StringPiece& product);
     std::vector<ResourceConfigValue*> findAllValues(const ConfigDescription& config);
+    std::vector<ResourceConfigValue*> findValuesIf(
+            const std::function<bool(ResourceConfigValue*)>& f);
+
 
 private:
     DISALLOW_COPY_AND_ASSIGN(ResourceEntry);
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 74c48b0..a0a7efc 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -289,7 +289,7 @@
 std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute* flagAttr,
                                                     const StringPiece16& str) {
     android::Res_value flags = { };
-    flags.dataType = android::Res_value::TYPE_INT_DEC;
+    flags.dataType = android::Res_value::TYPE_INT_HEX;
     flags.data = 0u;
 
     if (util::trimWhitespace(str).empty()) {
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index dd7ff01..c10b134 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -39,6 +39,14 @@
 RawString::RawString(const StringPool::Ref& ref) : value(ref) {
 }
 
+bool RawString::equals(const Value* value) const {
+    const RawString* other = valueCast<RawString>(value);
+    if (!other) {
+        return false;
+    }
+    return *this->value == *other->value;
+}
+
 RawString* RawString::clone(StringPool* newPool) const {
     RawString* rs = new RawString(newPool->makeRef(*value));
     rs->mComment = mComment;
@@ -66,6 +74,15 @@
 Reference::Reference(const ResourceId& i, Type type) : id(i), referenceType(type) {
 }
 
+bool Reference::equals(const Value* value) const {
+    const Reference* other = valueCast<Reference>(value);
+    if (!other) {
+        return false;
+    }
+    return referenceType == other->referenceType && privateReference == other->privateReference &&
+            id == other->id && name == other->name;
+}
+
 bool Reference::flatten(android::Res_value* outValue) const {
     outValue->dataType = (referenceType == Reference::Type::kResource) ?
             android::Res_value::TYPE_REFERENCE : android::Res_value::TYPE_ATTRIBUTE;
@@ -97,6 +114,10 @@
     }
 }
 
+bool Id::equals(const Value* value) const {
+    return valueCast<Id>(value) != nullptr;
+}
+
 bool Id::flatten(android::Res_value* out) const {
     out->dataType = android::Res_value::TYPE_INT_BOOLEAN;
     out->data = util::hostToDevice32(0);
@@ -111,15 +132,15 @@
     *out << "(id)";
 }
 
-String::String(const StringPool::Ref& ref) : value(ref), mTranslateable(true) {
+String::String(const StringPool::Ref& ref) : value(ref) {
 }
 
-void String::setTranslateable(bool val) {
-    mTranslateable = val;
-}
-
-bool String::isTranslateable() const {
-    return mTranslateable;
+bool String::equals(const Value* value) const {
+    const String* other = valueCast<String>(value);
+    if (!other) {
+        return false;
+    }
+    return *this->value == *other->value;
 }
 
 bool String::flatten(android::Res_value* outValue) const {
@@ -144,15 +165,24 @@
     *out << "(string) \"" << *value << "\"";
 }
 
-StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref), mTranslateable(true) {
+StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) {
 }
 
-void StyledString::setTranslateable(bool val) {
-    mTranslateable = val;
-}
+bool StyledString::equals(const Value* value) const {
+    const StyledString* other = valueCast<StyledString>(value);
+    if (!other) {
+        return false;
+    }
 
-bool StyledString::isTranslateable() const {
-    return mTranslateable;
+    if (*this->value->str == *other->value->str) {
+        const std::vector<StringPool::Span>& spansA = this->value->spans;
+        const std::vector<StringPool::Span>& spansB = other->value->spans;
+        return std::equal(spansA.begin(), spansA.end(), spansB.begin(),
+                          [](const StringPool::Span& a, const StringPool::Span& b) -> bool {
+            return *a.name == *b.name && a.firstChar == b.firstChar && a.lastChar == b.lastChar;
+        });
+    }
+    return false;
 }
 
 bool StyledString::flatten(android::Res_value* outValue) const {
@@ -174,11 +204,22 @@
 
 void StyledString::print(std::ostream* out) const {
     *out << "(styled string) \"" << *value->str << "\"";
+    for (const StringPool::Span& span : value->spans) {
+        *out << " "<< *span.name << ":" << span.firstChar << "," << span.lastChar;
+    }
 }
 
 FileReference::FileReference(const StringPool::Ref& _path) : path(_path) {
 }
 
+bool FileReference::equals(const Value* value) const {
+    const FileReference* other = valueCast<FileReference>(value);
+    if (!other) {
+        return false;
+    }
+    return *path == *other->path;
+}
+
 bool FileReference::flatten(android::Res_value* outValue) const {
     if (path.getIndex() > std::numeric_limits<uint32_t>::max()) {
         return false;
@@ -209,6 +250,14 @@
     value.data = data;
 }
 
+bool BinaryPrimitive::equals(const Value* value) const {
+    const BinaryPrimitive* other = valueCast<BinaryPrimitive>(value);
+    if (!other) {
+        return false;
+    }
+    return this->value.dataType == other->value.dataType && this->value.data == other->value.data;
+}
+
 bool BinaryPrimitive::flatten(android::Res_value* outValue) const {
     outValue->dataType = value.dataType;
     outValue->data = util::hostToDevice32(value.data);
@@ -228,7 +277,7 @@
             *out << "(integer) " << static_cast<int32_t>(value.data);
             break;
         case android::Res_value::TYPE_INT_HEX:
-            *out << "(integer) " << std::hex << value.data << std::dec;
+            *out << "(integer) 0x" << std::hex << value.data << std::dec;
             break;
         case android::Res_value::TYPE_INT_BOOLEAN:
             *out << "(boolean) " << (value.data != 0 ? "true" : "false");
@@ -253,6 +302,21 @@
     mWeak = w;
 }
 
+bool Attribute::equals(const Value* value) const {
+    const Attribute* other = valueCast<Attribute>(value);
+    if (!other) {
+        return false;
+    }
+
+    return this->typeMask == other->typeMask && this->minInt == other->minInt &&
+            this->maxInt == other->maxInt &&
+            std::equal(this->symbols.begin(), this->symbols.end(),
+                       other->symbols.begin(),
+                       [](const Symbol& a, const Symbol& b) -> bool {
+        return a.symbol.equals(&b.symbol) && a.value == b.value;
+    });
+}
+
 Attribute* Attribute::clone(StringPool* /*newPool*/) const {
     return new Attribute(*this);
 }
@@ -365,6 +429,14 @@
             << "]";
     }
 
+    if (minInt != std::numeric_limits<int32_t>::min()) {
+        *out << " min=" << minInt;
+    }
+
+    if (maxInt != std::numeric_limits<int32_t>::max()) {
+        *out << " max=" << maxInt;
+    }
+
     if (isWeak()) {
         *out << " [weak]";
     }
@@ -445,6 +517,21 @@
     return true;
 }
 
+bool Style::equals(const Value* value) const {
+    const Style* other = valueCast<Style>(value);
+    if (!other) {
+        return false;
+    }
+    if (bool(parent) != bool(other->parent) ||
+            (parent && other->parent && !parent.value().equals(&other->parent.value()))) {
+        return false;
+    }
+    return std::equal(entries.begin(), entries.end(), other->entries.begin(),
+                      [](const Entry& a, const Entry& b) -> bool {
+        return a.key.equals(&b.key) && a.value->equals(b.value.get());
+    });
+}
+
 Style* Style::clone(StringPool* newPool) const {
     Style* style = new Style();
     style->parent = parent;
@@ -484,6 +571,18 @@
     return out;
 }
 
+bool Array::equals(const Value* value) const {
+    const Array* other = valueCast<Array>(value);
+    if (!other) {
+        return false;
+    }
+
+    return std::equal(items.begin(), items.end(), other->items.begin(),
+                      [](const std::unique_ptr<Item>& a, const std::unique_ptr<Item>& b) -> bool {
+        return a->equals(b.get());
+    });
+}
+
 Array* Array::clone(StringPool* newPool) const {
     Array* array = new Array();
     array->mComment = mComment;
@@ -500,6 +599,21 @@
         << "]";
 }
 
+bool Plural::equals(const Value* value) const {
+    const Plural* other = valueCast<Plural>(value);
+    if (!other) {
+        return false;
+    }
+
+    return std::equal(values.begin(), values.end(), other->values.begin(),
+                      [](const std::unique_ptr<Item>& a, const std::unique_ptr<Item>& b) -> bool {
+        if (bool(a) != bool(b)) {
+            return false;
+        }
+        return bool(a) == bool(b) || a->equals(b.get());
+    });
+}
+
 Plural* Plural::clone(StringPool* newPool) const {
     Plural* p = new Plural();
     p->mComment = mComment;
@@ -515,12 +629,42 @@
 
 void Plural::print(std::ostream* out) const {
     *out << "(plural)";
+    if (values[Zero]) {
+        *out << " zero=" << *values[Zero];
+    }
+
+    if (values[One]) {
+        *out << " one=" << *values[One];
+    }
+
+    if (values[Two]) {
+        *out << " two=" << *values[Two];
+    }
+
+    if (values[Few]) {
+        *out << " few=" << *values[Few];
+    }
+
+    if (values[Many]) {
+        *out << " many=" << *values[Many];
+    }
 }
 
 static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Item>& item) {
     return out << *item;
 }
 
+bool Styleable::equals(const Value* value) const {
+    const Styleable* other = valueCast<Styleable>(value);
+    if (!other) {
+        return false;
+    }
+    return std::equal(entries.begin(), entries.end(), other->entries.begin(),
+                      [](const Reference& a, const Reference& b) -> bool {
+        return a.equals(&b);
+    });
+}
+
 Styleable* Styleable::clone(StringPool* /*newPool*/) const {
     return new Styleable(*this);
 }
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index 43354ac..aa1b550 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -54,6 +54,18 @@
         mWeak = val;
     }
 
+    // Whether the value is marked as translateable.
+    // This does not persist when flattened.
+    // It is only used during compilation phase.
+    void setTranslateable(bool val) {
+        mTranslateable = val;
+    }
+
+    // Default true.
+    bool isTranslateable() const {
+        return mTranslateable;
+    }
+
     /**
      * Returns the source where this value was defined.
      */
@@ -84,6 +96,8 @@
         mComment = std::move(str);
     }
 
+    virtual bool equals(const Value* value) const = 0;
+
     /**
      * Calls the appropriate overload of ValueVisitor.
      */
@@ -103,6 +117,7 @@
     Source mSource;
     std::u16string mComment;
     bool mWeak = false;
+    bool mTranslateable = true;
 };
 
 /**
@@ -158,6 +173,7 @@
     explicit Reference(const ResourceNameRef& n, Type type = Type::kResource);
     explicit Reference(const ResourceId& i, Type type = Type::kResource);
 
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     Reference* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
@@ -168,6 +184,7 @@
  */
 struct Id : public BaseItem<Id> {
     Id() { mWeak = true; }
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* out) const override;
     Id* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
@@ -183,6 +200,7 @@
 
     RawString(const StringPool::Ref& ref);
 
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     RawString* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
@@ -193,17 +211,10 @@
 
     String(const StringPool::Ref& ref);
 
-    // Whether the string is marked as translateable. This does not persist when flattened.
-    // It is only used during compilation phase.
-    void setTranslateable(bool val);
-    bool isTranslateable() const;
-
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     String* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
-
-private:
-    bool mTranslateable;
 };
 
 struct StyledString : public BaseItem<StyledString> {
@@ -211,17 +222,10 @@
 
     StyledString(const StringPool::StyleRef& ref);
 
-    // Whether the string is marked as translateable. This does not persist when flattened.
-    // It is only used during compilation phase.
-    void setTranslateable(bool val);
-    bool isTranslateable() const;
-
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     StyledString* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
-
-private:
-    bool mTranslateable;
 };
 
 struct FileReference : public BaseItem<FileReference> {
@@ -235,6 +239,7 @@
     FileReference() = default;
     FileReference(const StringPool::Ref& path);
 
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     FileReference* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
@@ -250,6 +255,7 @@
     BinaryPrimitive(const android::Res_value& val);
     BinaryPrimitive(uint8_t dataType, uint32_t data);
 
+    bool equals(const Value* value) const override;
     bool flatten(android::Res_value* outValue) const override;
     BinaryPrimitive* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
@@ -268,6 +274,7 @@
 
     Attribute(bool w, uint32_t t = 0u);
 
+    bool equals(const Value* value) const override;
     Attribute* clone(StringPool* newPool) const override;
     void printMask(std::ostream* out) const;
     void print(std::ostream* out) const override;
@@ -290,6 +297,7 @@
 
     std::vector<Entry> entries;
 
+    bool equals(const Value* value) const override;
     Style* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
 };
@@ -297,6 +305,7 @@
 struct Array : public BaseValue<Array> {
     std::vector<std::unique_ptr<Item>> items;
 
+    bool equals(const Value* value) const override;
     Array* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
 };
@@ -314,6 +323,7 @@
 
     std::array<std::unique_ptr<Item>, Count> values;
 
+    bool equals(const Value* value) const override;
     Plural* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
 };
@@ -321,6 +331,7 @@
 struct Styleable : public BaseValue<Styleable> {
     std::vector<Reference> entries;
 
+    bool equals(const Value* value) const override;
     Styleable* clone(StringPool* newPool) const override;
     void print(std::ostream* out) const override;
 };
diff --git a/tools/aapt2/ValueVisitor.h b/tools/aapt2/ValueVisitor.h
index ea2aa55..b8bc5db 100644
--- a/tools/aapt2/ValueVisitor.h
+++ b/tools/aapt2/ValueVisitor.h
@@ -127,6 +127,11 @@
     }
 };
 
+template <typename T>
+const T* valueCast(const Value* value) {
+    return valueCast<T>(const_cast<Value*>(value));
+}
+
 /**
  * Returns a valid pointer to T if the Value is of subtype T.
  * Otherwise, returns nullptr.
@@ -141,7 +146,6 @@
     return visitor.value;
 }
 
-
 inline void visitAllValuesInPackage(ResourceTablePackage* pkg, RawValueVisitor* visitor) {
     for (auto& type : pkg->types) {
         for (auto& entry : type->entries) {
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp
index 99c2077..d080e16 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp
@@ -128,23 +128,6 @@
             mPool(pool), mMethod(method), mLocalizer(method) {
     }
 
-    void visit(Array* array) override {
-        std::unique_ptr<Array> localized = util::make_unique<Array>();
-        localized->items.resize(array->items.size());
-        for (size_t i = 0; i < array->items.size(); i++) {
-            Visitor subVisitor(mPool, mMethod);
-            array->items[i]->accept(&subVisitor);
-            if (subVisitor.mItem) {
-                localized->items[i] = std::move(subVisitor.mItem);
-            } else {
-                localized->items[i] = std::unique_ptr<Item>(array->items[i]->clone(mPool));
-            }
-        }
-        localized->setSource(array->getSource());
-        localized->setWeak(true);
-        mValue = std::move(localized);
-    }
-
     void visit(Plural* plural) override {
         std::unique_ptr<Plural> localized = util::make_unique<Plural>();
         for (size_t i = 0; i < plural->values.size(); i++) {
@@ -164,10 +147,6 @@
     }
 
     void visit(String* string) override {
-        if (!string->isTranslateable()) {
-            return;
-        }
-
         std::u16string result = mLocalizer.start() + mLocalizer.text(*string->value) +
                 mLocalizer.end();
         std::unique_ptr<String> localized = util::make_unique<String>(mPool->makeRef(result));
@@ -177,10 +156,6 @@
     }
 
     void visit(StyledString* string) override {
-        if (!string->isTranslateable()) {
-            return;
-        }
-
         mItem = pseudolocalizeStyledString(string, mMethod, mPool);
         mItem->setWeak(true);
     }
@@ -238,14 +213,26 @@
     }
 }
 
+/**
+ * A value is pseudolocalizable if it does not define a locale (or is the default locale)
+ * and is translateable.
+ */
+static bool isPseudolocalizable(ResourceConfigValue* configValue) {
+    const int diff = configValue->config.diff(ConfigDescription::defaultConfig());
+    if (diff & ConfigDescription::CONFIG_LOCALE) {
+        return false;
+    }
+    return configValue->value->isTranslateable();
+}
+
 } // namespace
 
 bool PseudolocaleGenerator::consume(IAaptContext* context, ResourceTable* table) {
     for (auto& package : table->packages) {
         for (auto& type : package->types) {
             for (auto& entry : type->entries) {
-                std::vector<ResourceConfigValue*> values = entry->findAllValues(
-                        ConfigDescription::defaultConfig());
+                std::vector<ResourceConfigValue*> values = entry->findValuesIf(isPseudolocalizable);
+
                 for (ResourceConfigValue* value : values) {
                     pseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value,
                                            &table->stringPool, entry.get());
diff --git a/tools/aapt2/diff/Diff.cpp b/tools/aapt2/diff/Diff.cpp
new file mode 100644
index 0000000..20b7b59
--- /dev/null
+++ b/tools/aapt2/diff/Diff.cpp
@@ -0,0 +1,411 @@
+/*
+ * 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 "Flags.h"
+#include "ResourceTable.h"
+#include "io/ZipArchive.h"
+#include "process/IResourceTableConsumer.h"
+#include "process/SymbolTable.h"
+#include "unflatten/BinaryResourceParser.h"
+
+#include <android-base/macros.h>
+
+namespace aapt {
+
+class DiffContext : public IAaptContext {
+public:
+    const std::u16string& getCompilationPackage() override {
+        return mEmpty;
+    }
+
+    uint8_t getPackageId() override {
+        return 0x0;
+    }
+
+    IDiagnostics* getDiagnostics() override {
+        return &mDiagnostics;
+    }
+
+    NameMangler* getNameMangler() override {
+        return &mNameMangler;
+    }
+
+    SymbolTable* getExternalSymbols() override {
+        return &mSymbolTable;
+    }
+
+    bool verbose() override {
+        return false;
+    }
+
+private:
+    std::u16string mEmpty;
+    StdErrDiagnostics mDiagnostics;
+    NameMangler mNameMangler = NameMangler(NameManglerPolicy{});
+    SymbolTable mSymbolTable;
+};
+
+class LoadedApk {
+public:
+    LoadedApk(const Source& source, std::unique_ptr<io::IFileCollection> apk,
+              std::unique_ptr<ResourceTable> table) :
+            mSource(source), mApk(std::move(apk)), mTable(std::move(table)) {
+    }
+
+    io::IFileCollection* getFileCollection() {
+        return mApk.get();
+    }
+
+    ResourceTable* getResourceTable() {
+        return mTable.get();
+    }
+
+    const Source& getSource() {
+        return mSource;
+    }
+
+private:
+    Source mSource;
+    std::unique_ptr<io::IFileCollection> mApk;
+    std::unique_ptr<ResourceTable> mTable;
+
+    DISALLOW_COPY_AND_ASSIGN(LoadedApk);
+};
+
+static std::unique_ptr<LoadedApk> loadApkFromPath(IAaptContext* context, const StringPiece& path) {
+    Source source(path);
+    std::string error;
+    std::unique_ptr<io::ZipFileCollection> apk = io::ZipFileCollection::create(path, &error);
+    if (!apk) {
+        context->getDiagnostics()->error(DiagMessage(source) << error);
+        return {};
+    }
+
+    io::IFile* file = apk->findFile("resources.arsc");
+    if (!file) {
+        context->getDiagnostics()->error(DiagMessage(source) << "no resources.arsc found");
+        return {};
+    }
+
+    std::unique_ptr<io::IData> data = file->openAsData();
+    if (!data) {
+        context->getDiagnostics()->error(DiagMessage(source) << "could not open resources.arsc");
+        return {};
+    }
+
+    std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
+    BinaryResourceParser parser(context, table.get(), source, data->data(), data->size());
+    if (!parser.parse()) {
+        return {};
+    }
+
+    return util::make_unique<LoadedApk>(source, std::move(apk), std::move(table));
+}
+
+static void emitDiffLine(const Source& source, const StringPiece& message) {
+    std::cerr << source << ": " << message << "\n";
+}
+
+static bool isSymbolVisibilityDifferent(const Symbol& symbolA, const Symbol& symbolB) {
+    return symbolA.state != symbolB.state;
+}
+
+template <typename Id>
+static bool isIdDiff(const Symbol& symbolA, const Maybe<Id>& idA,
+                     const Symbol& symbolB, const Maybe<Id>& idB) {
+    if (symbolA.state == SymbolState::kPublic || symbolB.state == SymbolState::kPublic) {
+        return idA != idB;
+    }
+    return false;
+}
+
+static bool emitResourceConfigValueDiff(IAaptContext* context,
+                                        LoadedApk* apkA,
+                                        ResourceTablePackage* pkgA,
+                                        ResourceTableType* typeA,
+                                        ResourceEntry* entryA,
+                                        ResourceConfigValue* configValueA,
+                                        LoadedApk* apkB,
+                                        ResourceTablePackage* pkgB,
+                                        ResourceTableType* typeB,
+                                        ResourceEntry* entryB,
+                                        ResourceConfigValue* configValueB) {
+    Value* valueA = configValueA->value.get();
+    Value* valueB = configValueB->value.get();
+    if (!valueA->equals(valueB)) {
+        std::stringstream strStream;
+        strStream << "value " << pkgA->name << ":" << typeA->type << "/" << entryA->name
+                << " config=" << configValueA->config << " does not match:\n";
+        valueA->print(&strStream);
+        strStream << "\n vs \n";
+        valueB->print(&strStream);
+        emitDiffLine(apkB->getSource(), strStream.str());
+        return true;
+    }
+    return false;
+}
+
+static bool emitResourceEntryDiff(IAaptContext* context,
+                                  LoadedApk* apkA,
+                                  ResourceTablePackage* pkgA,
+                                  ResourceTableType* typeA,
+                                  ResourceEntry* entryA,
+                                  LoadedApk* apkB,
+                                  ResourceTablePackage* pkgB,
+                                  ResourceTableType* typeB,
+                                  ResourceEntry* entryB) {
+    bool diff = false;
+    for (std::unique_ptr<ResourceConfigValue>& configValueA : entryA->values) {
+        ResourceConfigValue* configValueB = entryB->findValue(configValueA->config);
+        if (!configValueB) {
+            std::stringstream strStream;
+            strStream << "missing " << pkgA->name << ":" << typeA->type << "/" << entryA->name
+                    << " config=" << configValueA->config;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        } else {
+            diff |= emitResourceConfigValueDiff(context, apkA, pkgA, typeA, entryA,
+                                                configValueA.get(), apkB, pkgB, typeB, entryB,
+                                                configValueB);
+        }
+    }
+
+    // Check for any newly added config values.
+    for (std::unique_ptr<ResourceConfigValue>& configValueB : entryB->values) {
+        ResourceConfigValue* configValueA = entryA->findValue(configValueB->config);
+        if (!configValueA) {
+            std::stringstream strStream;
+            strStream << "new config " << pkgB->name << ":" << typeB->type << "/" << entryB->name
+                    << " config=" << configValueB->config;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        }
+    }
+    return false;
+}
+
+static bool emitResourceTypeDiff(IAaptContext* context,
+                                 LoadedApk* apkA,
+                                 ResourceTablePackage* pkgA,
+                                 ResourceTableType* typeA,
+                                 LoadedApk* apkB,
+                                 ResourceTablePackage* pkgB,
+                                 ResourceTableType* typeB) {
+    bool diff = false;
+    for (std::unique_ptr<ResourceEntry>& entryA : typeA->entries) {
+        ResourceEntry* entryB = typeB->findEntry(entryA->name);
+        if (!entryB) {
+            std::stringstream strStream;
+            strStream << "missing " << pkgA->name << ":" << typeA->type << "/" << entryA->name;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        } else {
+            if (isSymbolVisibilityDifferent(entryA->symbolStatus, entryB->symbolStatus)) {
+                std::stringstream strStream;
+                strStream << pkgA->name << ":" << typeA->type << "/" << entryA->name
+                        << " has different visibility (";
+                if (entryB->symbolStatus.state == SymbolState::kPublic) {
+                    strStream << "PUBLIC";
+                } else {
+                    strStream << "PRIVATE";
+                }
+                strStream << " vs ";
+                if (entryA->symbolStatus.state == SymbolState::kPublic) {
+                    strStream << "PUBLIC";
+                } else {
+                    strStream << "PRIVATE";
+                }
+                strStream << ")";
+                emitDiffLine(apkB->getSource(), strStream.str());
+                diff = true;
+            } else if (isIdDiff(entryA->symbolStatus, entryA->id,
+                                entryB->symbolStatus, entryB->id)) {
+                std::stringstream strStream;
+                strStream << pkgA->name << ":" << typeA->type << "/" << entryA->name
+                        << " has different public ID (";
+                if (entryB->id) {
+                    strStream << "0x" << std::hex << entryB->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << " vs ";
+                if (entryA->id) {
+                    strStream << "0x " << std::hex << entryA->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << ")";
+                emitDiffLine(apkB->getSource(), strStream.str());
+                diff = true;
+            }
+            diff |= emitResourceEntryDiff(context, apkA, pkgA, typeA, entryA.get(),
+                                          apkB, pkgB, typeB, entryB);
+        }
+    }
+
+    // Check for any newly added entries.
+    for (std::unique_ptr<ResourceEntry>& entryB : typeB->entries) {
+        ResourceEntry* entryA = typeA->findEntry(entryB->name);
+        if (!entryA) {
+            std::stringstream strStream;
+            strStream << "new entry " << pkgB->name << ":" << typeB->type << "/" << entryB->name;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        }
+    }
+    return diff;
+}
+
+static bool emitResourcePackageDiff(IAaptContext* context, LoadedApk* apkA,
+                                    ResourceTablePackage* pkgA,
+                                    LoadedApk* apkB, ResourceTablePackage* pkgB) {
+    bool diff = false;
+    for (std::unique_ptr<ResourceTableType>& typeA : pkgA->types) {
+        ResourceTableType* typeB = pkgB->findType(typeA->type);
+        if (!typeB) {
+            std::stringstream strStream;
+            strStream << "missing " << pkgA->name << ":" << typeA->type;
+            emitDiffLine(apkA->getSource(), strStream.str());
+            diff = true;
+        } else {
+            if (isSymbolVisibilityDifferent(typeA->symbolStatus, typeB->symbolStatus)) {
+                std::stringstream strStream;
+                strStream << pkgA->name << ":" << typeA->type << " has different visibility (";
+                if (typeB->symbolStatus.state == SymbolState::kPublic) {
+                    strStream << "PUBLIC";
+                } else {
+                    strStream << "PRIVATE";
+                }
+                strStream << " vs ";
+                if (typeA->symbolStatus.state == SymbolState::kPublic) {
+                    strStream << "PUBLIC";
+                } else {
+                    strStream << "PRIVATE";
+                }
+                strStream << ")";
+                emitDiffLine(apkB->getSource(), strStream.str());
+                diff = true;
+            } else if (isIdDiff(typeA->symbolStatus, typeA->id, typeB->symbolStatus, typeB->id)) {
+                std::stringstream strStream;
+                strStream << pkgA->name << ":" << typeA->type << " has different public ID (";
+                if (typeB->id) {
+                    strStream << "0x" << std::hex << typeB->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << " vs ";
+                if (typeA->id) {
+                    strStream << "0x " << std::hex << typeA->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << ")";
+                emitDiffLine(apkB->getSource(), strStream.str());
+                diff = true;
+            }
+            diff |= emitResourceTypeDiff(context, apkA, pkgA, typeA.get(), apkB, pkgB, typeB);
+        }
+    }
+
+    // Check for any newly added types.
+    for (std::unique_ptr<ResourceTableType>& typeB : pkgB->types) {
+        ResourceTableType* typeA = pkgA->findType(typeB->type);
+        if (!typeA) {
+            std::stringstream strStream;
+            strStream << "new type " << pkgB->name << ":" << typeB->type;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        }
+    }
+    return diff;
+}
+
+static bool emitResourceTableDiff(IAaptContext* context, LoadedApk* apkA, LoadedApk* apkB) {
+    ResourceTable* tableA = apkA->getResourceTable();
+    ResourceTable* tableB = apkB->getResourceTable();
+
+    bool diff = false;
+    for (std::unique_ptr<ResourceTablePackage>& pkgA : tableA->packages) {
+        ResourceTablePackage* pkgB = tableB->findPackage(pkgA->name);
+        if (!pkgB) {
+            std::stringstream strStream;
+            strStream << "missing package " << pkgA->name;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        } else {
+            if (pkgA->id != pkgB->id) {
+                std::stringstream strStream;
+                strStream << "package '" << pkgA->name << "' has different id (";
+                if (pkgB->id) {
+                    strStream << "0x" << std::hex << pkgB->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << " vs ";
+                if (pkgA->id) {
+                    strStream << "0x" << std::hex << pkgA->id.value();
+                } else {
+                    strStream << "none";
+                }
+                strStream << ")";
+                emitDiffLine(apkB->getSource(), strStream.str());
+                diff = true;
+            }
+            diff |= emitResourcePackageDiff(context, apkA, pkgA.get(), apkB, pkgB);
+        }
+    }
+
+    // Check for any newly added packages.
+    for (std::unique_ptr<ResourceTablePackage>& pkgB : tableB->packages) {
+        ResourceTablePackage* pkgA = tableA->findPackage(pkgB->name);
+        if (!pkgA) {
+            std::stringstream strStream;
+            strStream << "new package " << pkgB->name;
+            emitDiffLine(apkB->getSource(), strStream.str());
+            diff = true;
+        }
+    }
+    return diff;
+}
+
+int diff(const std::vector<StringPiece>& args) {
+    DiffContext context;
+
+    Flags flags;
+    if (!flags.parse("aapt2 diff", args, &std::cerr)) {
+        return 1;
+    }
+
+    if (flags.getArgs().size() != 2u) {
+        std::cerr << "must have two apks as arguments.\n\n";
+        flags.usage("aapt2 diff", &std::cerr);
+        return 1;
+    }
+
+    std::unique_ptr<LoadedApk> apkA = loadApkFromPath(&context, flags.getArgs()[0]);
+    std::unique_ptr<LoadedApk> apkB = loadApkFromPath(&context, flags.getArgs()[1]);
+    if (!apkA || !apkB) {
+        return 1;
+    }
+
+    if (emitResourceTableDiff(&context, apkA.get(), apkB.get())) {
+        // We emitted a diff, so return 1 (failure).
+        return 1;
+    }
+    return 0;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/proto/TableProtoDeserializer.cpp b/tools/aapt2/proto/TableProtoDeserializer.cpp
index 6489b86..a7798e7 100644
--- a/tools/aapt2/proto/TableProtoDeserializer.cpp
+++ b/tools/aapt2/proto/TableProtoDeserializer.cpp
@@ -234,6 +234,8 @@
                 const pb::Attribute& pbAttr = pbCompoundValue.attr();
                 std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak);
                 attr->typeMask = pbAttr.format_flags();
+                attr->minInt = pbAttr.min_int();
+                attr->maxInt = pbAttr.max_int();
                 for (const pb::Attribute_Symbol& pbSymbol : pbAttr.symbols()) {
                     Attribute::Symbol symbol;
                     deserializeItemCommon(pbSymbol, &symbol.symbol);