androidfw: Add support for compact resource entries

Bug: 237583012

Given the large number of simple resources such as strings in
Android resources, their ResTable_entry and Res_value can be
encoded together in a compact way.  This allows a significant
saving in both storage and memory footprint.

The basic observations for simple resources are:

* ResTable_entry.size will always be sizeof(ResTable_entry)
  unless it's a complex entry

* ResTable_entry.key is unlikely to exceed 16-bit

* ResTable_entry.flags only uses 3 bits for now

* Res_value.size will always be sizeof(Res_value)

Given the above, we could well encode the information into
a compact/compatible structure.

  struct compact {
      uint16_t key;
      uint16_t flags;
      uint32_t data;
  };

The layout of this structure will allow maximum backward
compatibility. e.g. the flags will be at the same offset,
and a

  `dtohs((ResTable_entry *)entry->flags) & FLAG_COMPACT`

would tell if this entry is a compact one or not. For a
compact entry:

  struct compact *entry;

  entry_size  == sizeof(*entry)
  entry_key   == static_cast<uint32_t>(dtohs(entry->key))
  entry_flags == dtohs(entry->flags) & 0xff	// low 8-bit
  data_type   == dtohs(entry->flags) >> 8	// high 8-bit
  data_size   == sizeof(Res_value)
  data_value  == dtohl(entry->data)

To allow minimum code change and backward compatibility,
we change 'struct ResTable_entry' to 'union ResTable_entry',
with an anonymous structure inside that's fully backward
compatible. Thus, any existing reference such as:

  ResTable_entry *entry = ...
  if (dtohs(entry->flags) & ResTable_entry::FLAG_COMPLEX) ...

would still work.

However, special care needs to be taken after an entry is
obtained, and when value needs to be extracted.

A compact entry will not encode a complex value, and hence
complex entries/values are handled the same way.

Change-Id: I15d97a4f5e85fab28c075496f7f0cf6b1fcd73e3
diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp
index 47750fc..4e597fb 100644
--- a/tools/aapt/ResourceTable.cpp
+++ b/tools/aapt/ResourceTable.cpp
@@ -3743,15 +3743,15 @@
     size_t amt = 0;
     ResTable_entry header;
     memset(&header, 0, sizeof(header));
-    header.size = htods(sizeof(header));
+    header.full.size = htods(sizeof(header));
     const type ty = mType;
     if (ty == TYPE_BAG) {
-        header.flags |= htods(header.FLAG_COMPLEX);
+        header.full.flags |= htods(header.FLAG_COMPLEX);
     }
     if (isPublic) {
-        header.flags |= htods(header.FLAG_PUBLIC);
+        header.full.flags |= htods(header.FLAG_PUBLIC);
     }
-    header.key.index = htodl(mNameIndex);
+    header.full.key.index = htodl(mNameIndex);
     if (ty != TYPE_BAG) {
         status_t err = data->writeData(&header, sizeof(header));
         if (err != NO_ERROR) {
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index f9e52b4..df87889 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -687,32 +687,32 @@
         continue;
       }
 
-      printer_->Print((entry->flags & ResTable_entry::FLAG_COMPLEX) ? "[ResTable_map_entry]"
-                                                                    : "[ResTable_entry]");
+      if (entry->is_complex()) {
+        printer_->Print("[ResTable_map_entry]");
+      } else if (entry->is_compact()) {
+        printer_->Print("[ResTable_entry_compact]");
+      } else {
+        printer_->Print("[ResTable_entry]");
+      }
+
       printer_->Print(StringPrintf(" id: 0x%04x", it.index()));
       printer_->Print(StringPrintf(
-          " name: %s",
-          android::util::GetString(key_pool_, android::util::DeviceToHost32(entry->key.index))
-              .c_str()));
-      printer_->Print(
-          StringPrintf(" keyIndex: %u", android::util::DeviceToHost32(entry->key.index)));
-      printer_->Print(StringPrintf(" size: %u", android::util::DeviceToHost32(entry->size)));
-      printer_->Print(StringPrintf(" flags: 0x%04x", android::util::DeviceToHost32(entry->flags)));
+          " name: %s", android::util::GetString(key_pool_, entry->key()).c_str()));
+      printer_->Print(StringPrintf(" keyIndex: %u", entry->key()));
+      printer_->Print(StringPrintf(" size: %zu", entry->size()));
+      printer_->Print(StringPrintf(" flags: 0x%04x", entry->flags()));
 
       printer_->Indent();
 
-      if (entry->flags & ResTable_entry::FLAG_COMPLEX) {
-        auto map_entry = (const ResTable_map_entry*)entry;
-        printer_->Print(
-            StringPrintf(" count: 0x%04x", android::util::DeviceToHost32(map_entry->count)));
+      if (auto map_entry = entry->map_entry()) {
+        uint32_t map_entry_count = android::util::DeviceToHost32(map_entry->count);
+        printer_->Print(StringPrintf(" count: 0x%04x", map_entry_count));
         printer_->Print(StringPrintf(" parent: 0x%08x\n",
                                      android::util::DeviceToHost32(map_entry->parent.ident)));
 
         // Print the name and value mappings
-        auto maps = (const ResTable_map*)((const uint8_t*)entry +
-                                          android::util::DeviceToHost32(entry->size));
-        for (size_t i = 0, count = android::util::DeviceToHost32(map_entry->count); i < count;
-             i++) {
+        auto maps = (const ResTable_map*)((const uint8_t*)entry + entry->size());
+        for (size_t i = 0; i < map_entry_count; i++) {
           PrintResValue(&(maps[i].value), config, type);
 
           printer_->Print(StringPrintf(
@@ -725,9 +725,8 @@
         printer_->Print("\n");
 
         // Print the value of the entry
-        auto value =
-            (const Res_value*)((const uint8_t*)entry + android::util::DeviceToHost32(entry->size));
-        PrintResValue(value, config, type);
+        Res_value value = entry->value();
+        PrintResValue(&value, config, type);
       }
 
       printer_->Undent();
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index cffcdf2..5fdfb66 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -159,6 +159,9 @@
     AddOptionalSwitch("--enable-sparse-encoding",
                       "This decreases APK size at the cost of resource retrieval performance.",
                       &options_.use_sparse_encoding);
+    AddOptionalSwitch("--enable-compact-entries",
+        "This decreases APK size by using compact resource entries for simple data types.",
+        &options_.table_flattener_options.use_compact_entries);
     AddOptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.",
         &legacy_x_flag_);
     AddOptionalSwitch("-z", "Require localization of strings marked 'suggested'.",
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index d9e379d..8291862 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -384,21 +384,16 @@
       continue;
     }
 
-    const ResourceName name(
-        package->name, *parsed_type,
-        android::util::GetString(key_pool_, android::util::DeviceToHost32(entry->key.index)));
+    const ResourceName name(package->name, *parsed_type,
+        android::util::GetString(key_pool_, entry->key()));
     const ResourceId res_id(package_id, type->id, static_cast<uint16_t>(it.index()));
 
     std::unique_ptr<Value> resource_value;
-    if (entry->flags & ResTable_entry::FLAG_COMPLEX) {
-      const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry);
-
+    if (auto mapEntry = entry->map_entry()) {
       // TODO(adamlesinski): Check that the entry count is valid.
       resource_value = ParseMapEntry(name, config, mapEntry);
     } else {
-      const Res_value* value =
-          (const Res_value*)((const uint8_t*)entry + android::util::DeviceToHost32(entry->size));
-      resource_value = ParseValue(name, config, *value);
+      resource_value = ParseValue(name, config, entry->value());
     }
 
     if (!resource_value) {
@@ -419,7 +414,7 @@
         .SetId(res_id, OnIdConflict::CREATE_ENTRY)
         .SetAllowMangled(true);
 
-    if (entry->flags & ResTable_entry::FLAG_PUBLIC) {
+    if (entry->flags() & ResTable_entry::FLAG_PUBLIC) {
       Visibility visibility{Visibility::Level::kPublic};
 
       auto spec_flags = entry_type_spec_flags_.find(res_id);
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
index 8832c24..9dc205f 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -24,11 +24,6 @@
 
 namespace aapt {
 
-using android::BigBuffer;
-using android::Res_value;
-using android::ResTable_entry;
-using android::ResTable_map;
-
 struct less_style_entries {
   bool operator()(const Style::Entry* a, const Style::Entry* b) const {
     if (a->key.id) {
@@ -189,26 +184,40 @@
 };
 
 template <typename T>
-void WriteEntry(const FlatEntry* entry, T* out_result) {
+void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) {
   static_assert(std::is_same_v<ResTable_entry, T> || std::is_same_v<ResTable_entry_ext, T>,
                 "T must be ResTable_entry or ResTable_entry_ext");
 
   ResTable_entry* out_entry = (ResTable_entry*)out_result;
+  uint16_t flags = 0;
+
   if (entry->entry->visibility.level == Visibility::Level::kPublic) {
-    out_entry->flags |= ResTable_entry::FLAG_PUBLIC;
+    flags |= ResTable_entry::FLAG_PUBLIC;
   }
 
   if (entry->value->IsWeak()) {
-    out_entry->flags |= ResTable_entry::FLAG_WEAK;
+    flags |= ResTable_entry::FLAG_WEAK;
   }
 
   if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
-    out_entry->flags |= ResTable_entry::FLAG_COMPLEX;
+    flags |= ResTable_entry::FLAG_COMPLEX;
   }
 
-  out_entry->flags = android::util::HostToDevice16(out_entry->flags);
-  out_entry->key.index = android::util::HostToDevice32(entry->entry_key);
-  out_entry->size = android::util::HostToDevice16(sizeof(T));
+  if (!compact) {
+    out_entry->full.flags = android::util::HostToDevice16(flags);
+    out_entry->full.key.index = android::util::HostToDevice32(entry->entry_key);
+    out_entry->full.size = android::util::HostToDevice16(sizeof(T));
+  } else {
+    Res_value value;
+    CHECK(entry->entry_key < 0xffffu) << "cannot encode key in 16-bit";
+    CHECK(compact && (std::is_same_v<ResTable_entry, T>)) << "cannot encode complex entry";
+    CHECK(ValueCast<Item>(entry->value)->Flatten(&value)) << "flatten failed";
+
+    flags |= ResTable_entry::FLAG_COMPACT | (value.dataType << 8);
+    out_entry->compact.flags = android::util::HostToDevice16(flags);
+    out_entry->compact.key = android::util::HostToDevice16(entry->entry_key);
+    out_entry->compact.data = value.data;
+  }
 }
 
 int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer) {
@@ -222,57 +231,26 @@
   return offset;
 }
 
-void WriteItemToPair(const FlatEntry* item_entry, ResEntryValuePair* out_pair) {
-  static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value),
-                "ResEntryValuePair must not have padding between entry and value.");
+template <bool compact_entry, typename T>
+std::pair<int32_t, T*> WriteItemToBuffer(const FlatEntry* item_entry, BigBuffer* buffer) {
+  int32_t offset = buffer->size();
+  T* out_entry = buffer->NextBlock<T>();
 
-  WriteEntry<ResTable_entry>(item_entry, &out_pair->entry);
-
-  CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_pair->value)) << "flatten failed";
-  out_pair->value.size = android::util::HostToDevice16(sizeof(out_pair->value));
-}
-
-int32_t SequentialResEntryWriter::WriteMap(const FlatEntry* entry) {
-  return WriteMapToBuffer(entry, entries_buffer_);
-}
-
-int32_t SequentialResEntryWriter::WriteItem(const FlatEntry* entry) {
-  int32_t offset = entries_buffer_->size();
-  auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
-  WriteItemToPair(entry, out_pair);
-  return offset;
-}
-
-std::size_t ResEntryValuePairContentHasher::operator()(const ResEntryValuePairRef& ref) const {
-  return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(ResEntryValuePair));
-}
-
-bool ResEntryValuePairContentEqualTo::operator()(const ResEntryValuePairRef& a,
-                                                 const ResEntryValuePairRef& b) const {
-  return std::memcmp(a.ptr, b.ptr, sizeof(ResEntryValuePair)) == 0;
-}
-
-int32_t DeduplicateItemsResEntryWriter::WriteMap(const FlatEntry* entry) {
-  return WriteMapToBuffer(entry, entries_buffer_);
-}
-
-int32_t DeduplicateItemsResEntryWriter::WriteItem(const FlatEntry* entry) {
-  int32_t initial_offset = entries_buffer_->size();
-
-  auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
-  WriteItemToPair(entry, out_pair);
-
-  auto ref = ResEntryValuePairRef{*out_pair};
-  auto [it, inserted] = entry_offsets.insert({ref, initial_offset});
-  if (inserted) {
-    // If inserted just return a new offset as this is a first time we store
-    // this entry.
-    return initial_offset;
+  if constexpr (compact_entry) {
+    WriteEntry(item_entry, out_entry, true);
+  } else {
+    WriteEntry(item_entry, &out_entry->entry);
+    CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_entry->value)) << "flatten failed";
+    out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value));
   }
-  // If not inserted this means that this is a duplicate, backup allocated block to the buffer
-  // and return offset of previously stored entry.
-  entries_buffer_->BackUp(sizeof(ResEntryValuePair));
-  return it->second;
+  return {offset, out_entry};
 }
 
-}  // namespace aapt
\ No newline at end of file
+// explicitly specialize both versions
+template std::pair<int32_t, ResEntryValue<false>*> WriteItemToBuffer<false>(
+        const FlatEntry* item_entry, BigBuffer* buffer);
+
+template std::pair<int32_t, ResEntryValue<true>*> WriteItemToBuffer<true>(
+        const FlatEntry* item_entry, BigBuffer* buffer);
+
+}  // namespace aapt
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
index a36ceec..c11598e 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.h
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -27,6 +27,11 @@
 
 namespace aapt {
 
+using android::BigBuffer;
+using android::Res_value;
+using android::ResTable_entry;
+using android::ResTable_map;
+
 struct FlatEntry {
   const ResourceTableEntryView* entry;
   const Value* value;
@@ -39,28 +44,42 @@
 // We introduce this structure for ResEntryWriter to a have single allocation using
 // BigBuffer::NextBlock which allows to return it back with BigBuffer::Backup.
 struct ResEntryValuePair {
-  android::ResTable_entry entry;
-  android::Res_value value;
+  ResTable_entry entry;
+  Res_value value;
 };
 
-// References ResEntryValuePair object stored in BigBuffer used as a key in std::unordered_map.
-// Allows access to memory address where ResEntryValuePair is stored.
-union ResEntryValuePairRef {
-  const std::reference_wrapper<const ResEntryValuePair> pair;
+static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value),
+              "ResEntryValuePair must not have padding between entry and value.");
+
+template <bool compact>
+using ResEntryValue = std::conditional_t<compact, ResTable_entry, ResEntryValuePair>;
+
+// References ResEntryValue object stored in BigBuffer used as a key in std::unordered_map.
+// Allows access to memory address where ResEntryValue is stored.
+template <bool compact>
+union ResEntryValueRef {
+  using T = ResEntryValue<compact>;
+  const std::reference_wrapper<const T> ref;
   const u_char* ptr;
 
-  explicit ResEntryValuePairRef(const ResEntryValuePair& ref) : pair(ref) {
+  explicit ResEntryValueRef(const T& rev) : ref(rev) {
   }
 };
 
-// Hasher which computes hash of ResEntryValuePair using its bytes representation in memory.
-struct ResEntryValuePairContentHasher {
-  std::size_t operator()(const ResEntryValuePairRef& ref) const;
+// Hasher which computes hash of ResEntryValue using its bytes representation in memory.
+struct ResEntryValueContentHasher {
+  template <typename R>
+  std::size_t operator()(const R& ref) const {
+    return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(typename R::T));
+  }
 };
 
 // Equaler which compares ResEntryValuePairs using theirs bytes representation in memory.
-struct ResEntryValuePairContentEqualTo {
-  bool operator()(const ResEntryValuePairRef& a, const ResEntryValuePairRef& b) const;
+struct ResEntryValueContentEqualTo {
+  template <typename R>
+  bool operator()(const R& a, const R& b) const {
+    return std::memcmp(a.ptr, b.ptr, sizeof(typename R::T)) == 0;
+  }
 };
 
 // Base class that allows to write FlatEntries into entries_buffer.
@@ -79,9 +98,9 @@
   }
 
  protected:
-  ResEntryWriter(android::BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) {
+  ResEntryWriter(BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) {
   }
-  android::BigBuffer* entries_buffer_;
+  BigBuffer* entries_buffer_;
 
   virtual int32_t WriteItem(const FlatEntry* entry) = 0;
 
@@ -91,18 +110,29 @@
   DISALLOW_COPY_AND_ASSIGN(ResEntryWriter);
 };
 
+int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer);
+
+template <bool compact_entry, typename T=ResEntryValue<compact_entry>>
+std::pair<int32_t, T*> WriteItemToBuffer(const FlatEntry* item_entry, BigBuffer* buffer);
+
 // ResEntryWriter which writes FlatEntries sequentially into entries_buffer.
 // Next entry is always written right after previous one in the buffer.
+template <bool compact_entry = false>
 class SequentialResEntryWriter : public ResEntryWriter {
  public:
-  explicit SequentialResEntryWriter(android::BigBuffer* entries_buffer)
+  explicit SequentialResEntryWriter(BigBuffer* entries_buffer)
       : ResEntryWriter(entries_buffer) {
   }
   ~SequentialResEntryWriter() override = default;
 
-  int32_t WriteItem(const FlatEntry* entry) override;
+  int32_t WriteItem(const FlatEntry* entry) override {
+      auto result = WriteItemToBuffer<compact_entry>(entry, entries_buffer_);
+      return result.first;
+  }
 
-  int32_t WriteMap(const FlatEntry* entry) override;
+  int32_t WriteMap(const FlatEntry* entry) override {
+      return WriteMapToBuffer(entry, entries_buffer_);
+  }
 
  private:
   DISALLOW_COPY_AND_ASSIGN(SequentialResEntryWriter);
@@ -111,25 +141,44 @@
 // ResEntryWriter that writes only unique entry and value pairs into entries_buffer.
 // Next entry is written into buffer only if there is no entry with the same bytes representation
 // in memory written before. Otherwise returns offset of already written entry.
+template <bool compact_entry = false>
 class DeduplicateItemsResEntryWriter : public ResEntryWriter {
  public:
-  explicit DeduplicateItemsResEntryWriter(android::BigBuffer* entries_buffer)
+  explicit DeduplicateItemsResEntryWriter(BigBuffer* entries_buffer)
       : ResEntryWriter(entries_buffer) {
   }
   ~DeduplicateItemsResEntryWriter() override = default;
 
-  int32_t WriteItem(const FlatEntry* entry) override;
+  int32_t WriteItem(const FlatEntry* entry) override {
+    const auto& [offset, out_entry] = WriteItemToBuffer<compact_entry>(entry, entries_buffer_);
 
-  int32_t WriteMap(const FlatEntry* entry) override;
+    auto [it, inserted] = entry_offsets.insert({Ref{*out_entry}, offset});
+    if (inserted) {
+      // If inserted just return a new offset as this is a first time we store
+      // this entry
+      return offset;
+    }
+
+    // If not inserted this means that this is a duplicate, backup allocated block to the buffer
+    // and return offset of previously stored entry
+    entries_buffer_->BackUp(sizeof(*out_entry));
+    return it->second;
+  }
+
+  int32_t WriteMap(const FlatEntry* entry) override {
+      return WriteMapToBuffer(entry, entries_buffer_);
+  }
 
  private:
   DISALLOW_COPY_AND_ASSIGN(DeduplicateItemsResEntryWriter);
 
-  std::unordered_map<ResEntryValuePairRef, int32_t, ResEntryValuePairContentHasher,
-                     ResEntryValuePairContentEqualTo>
-      entry_offsets;
+  using Ref = ResEntryValueRef<compact_entry>;
+  using Map = std::unordered_map<Ref, int32_t,
+                        ResEntryValueContentHasher,
+                        ResEntryValueContentEqualTo>;
+  Map entry_offsets;
 };
 
 }  // namespace aapt
 
-#endif
\ No newline at end of file
+#endif
diff --git a/tools/aapt2/format/binary/ResEntryWriter_test.cpp b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
index 56ca133..4cb17c3 100644
--- a/tools/aapt2/format/binary/ResEntryWriter_test.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
@@ -56,14 +56,28 @@
           .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
           .Build();
 
-  BigBuffer out(512);
-  SequentialResEntryWriter writer(&out);
-  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+  {
+    BigBuffer out(512);
+    SequentialResEntryWriter<false> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
 
-  std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair),
-                                        2 * sizeof(ResEntryValuePair)};
-  EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair));
-  EXPECT_EQ(offsets, expected_offsets);
+    std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair),
+                                          2 * sizeof(ResEntryValuePair)};
+    EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
+
+  {
+    /* expect a compact entry to only take sizeof(ResTable_entry) */
+    BigBuffer out(512);
+    SequentialResEntryWriter<true> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+    std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry),
+                                          2 * sizeof(ResTable_entry)};
+    EXPECT_EQ(out.size(), 3 * sizeof(ResTable_entry));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
 };
 
 TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) {
@@ -83,13 +97,26 @@
                                              .AddValue("com.app.test:array/arr2", std::move(array2))
                                              .Build();
 
-  BigBuffer out(512);
-  SequentialResEntryWriter writer(&out);
-  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+  {
+    BigBuffer out(512);
+    SequentialResEntryWriter<false> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
 
-  std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
-  EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
-  EXPECT_EQ(offsets, expected_offsets);
+    std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+    EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
+
+  {
+    /* compact_entry should have no impact to map items */
+    BigBuffer out(512);
+    SequentialResEntryWriter<true> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+    std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+    EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
 };
 
 TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) {
@@ -100,13 +127,26 @@
           .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
           .Build();
 
-  BigBuffer out(512);
-  DeduplicateItemsResEntryWriter writer(&out);
-  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+  {
+    BigBuffer out(512);
+    DeduplicateItemsResEntryWriter<false> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
 
-  std::vector<int32_t> expected_offsets{0, 0, 0};
-  EXPECT_EQ(out.size(), sizeof(ResEntryValuePair));
-  EXPECT_EQ(offsets, expected_offsets);
+    std::vector<int32_t> expected_offsets{0, 0, 0};
+    EXPECT_EQ(out.size(), sizeof(ResEntryValuePair));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
+
+  {
+    /* expect a compact entry to only take sizeof(ResTable_entry) */
+    BigBuffer out(512);
+    DeduplicateItemsResEntryWriter<true> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+    std::vector<int32_t> expected_offsets{0, 0, 0};
+    EXPECT_EQ(out.size(), sizeof(ResTable_entry));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
 };
 
 TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) {
@@ -126,13 +166,26 @@
                                              .AddValue("com.app.test:array/arr2", std::move(array2))
                                              .Build();
 
-  BigBuffer out(512);
-  DeduplicateItemsResEntryWriter writer(&out);
-  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+  {
+    BigBuffer out(512);
+    DeduplicateItemsResEntryWriter<false> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
 
-  std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
-  EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
-  EXPECT_EQ(offsets, expected_offsets);
-};
+    std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+    EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
+
+  {
+    /* compact_entry should have no impact to map items */
+    BigBuffer out(512);
+    DeduplicateItemsResEntryWriter<true> writer(&out);
+    auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+    std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+    EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+    EXPECT_EQ(offsets, expected_offsets);
+  }
+ };
 
 }  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 318b8b6..c431730 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -67,7 +67,9 @@
  public:
   PackageFlattener(IAaptContext* context, const ResourceTablePackageView& package,
                    const std::map<size_t, std::string>* shared_libs,
-                   SparseEntriesMode sparse_entries, bool collapse_key_stringpool,
+                   SparseEntriesMode sparse_entries,
+                   bool compact_entries,
+                   bool collapse_key_stringpool,
                    const std::set<ResourceName>& name_collapse_exemptions,
                    bool deduplicate_entry_values)
       : context_(context),
@@ -75,6 +77,7 @@
         package_(package),
         shared_libs_(shared_libs),
         sparse_entries_(sparse_entries),
+        compact_entries_(compact_entries),
         collapse_key_stringpool_(collapse_key_stringpool),
         name_collapse_exemptions_(name_collapse_exemptions),
         deduplicate_entry_values_(deduplicate_entry_values) {
@@ -135,6 +138,33 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(PackageFlattener);
 
+  // Use compact entries only if
+  // 1) it is enabled, and that
+  // 2) the entries will be accessed on platforms U+, and
+  // 3) all entry keys can be encoded in 16 bits
+  bool UseCompactEntries(const ConfigDescription& config, std::vector<FlatEntry>* entries) const {
+    return compact_entries_ &&
+        (context_->GetMinSdkVersion() > SDK_TIRAMISU || config.sdkVersion > SDK_TIRAMISU) &&
+        std::none_of(entries->cbegin(), entries->cend(),
+          [](const auto& e) { return e.entry_key >= std::numeric_limits<uint16_t>::max(); });
+  }
+
+  std::unique_ptr<ResEntryWriter> GetResEntryWriter(bool dedup, bool compact, BigBuffer* buffer) {
+    if (dedup) {
+      if (compact) {
+        return std::make_unique<DeduplicateItemsResEntryWriter<true>>(buffer);
+      } else {
+        return std::make_unique<DeduplicateItemsResEntryWriter<false>>(buffer);
+      }
+    } else {
+      if (compact) {
+        return std::make_unique<SequentialResEntryWriter<true>>(buffer);
+      } else {
+        return std::make_unique<SequentialResEntryWriter<false>>(buffer);
+      }
+    }
+  }
+
   bool FlattenConfig(const ResourceTableTypeView& type, const ConfigDescription& config,
                      const size_t num_total_entries, std::vector<FlatEntry>* entries,
                      BigBuffer* buffer) {
@@ -150,15 +180,11 @@
     std::vector<uint32_t> offsets;
     offsets.resize(num_total_entries, 0xffffffffu);
 
+    bool compact_entry = UseCompactEntries(config, entries);
+
     android::BigBuffer values_buffer(512);
-    std::variant<std::monostate, DeduplicateItemsResEntryWriter, SequentialResEntryWriter>
-        writer_variant;
-    ResEntryWriter* res_entry_writer;
-    if (deduplicate_entry_values_) {
-      res_entry_writer = &writer_variant.emplace<DeduplicateItemsResEntryWriter>(&values_buffer);
-    } else {
-      res_entry_writer = &writer_variant.emplace<SequentialResEntryWriter>(&values_buffer);
-    }
+    auto res_entry_writer = GetResEntryWriter(deduplicate_entry_values_,
+                                              compact_entry, &values_buffer);
 
     for (FlatEntry& flat_entry : *entries) {
       CHECK(static_cast<size_t>(flat_entry.entry->id.value()) < num_total_entries);
@@ -512,6 +538,7 @@
   const ResourceTablePackageView package_;
   const std::map<size_t, std::string>* shared_libs_;
   SparseEntriesMode sparse_entries_;
+  bool compact_entries_;
   android::StringPool type_pool_;
   android::StringPool key_pool_;
   bool collapse_key_stringpool_;
@@ -568,7 +595,9 @@
     }
 
     PackageFlattener flattener(context, package, &table->included_packages_,
-                               options_.sparse_entries, options_.collapse_key_stringpool,
+                               options_.sparse_entries,
+                               options_.use_compact_entries,
+                               options_.collapse_key_stringpool,
                                options_.name_collapse_exemptions,
                                options_.deduplicate_entry_values);
     if (!flattener.FlattenPackage(&package_buffer)) {
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 6151b7e..35254ba 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -44,6 +44,9 @@
   // as a sparse map of entry ID and offset to actual data.
   SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled;
 
+  // When true, use compact entries for simple data
+  bool use_compact_entries = false;
+
   // When true, the key string pool in the final ResTable
   // is collapsed to a single entry. All resource entries
   // have name indices that point to this single value