Merge "aconfig: Print flags with namespace, and sort them" into main
diff --git a/core/Makefile b/core/Makefile
index 7d7457e..6edac1a 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -4600,10 +4600,18 @@
     --prop com.android.build.pvmfw.security_patch:$(PVMFW_SECURITY_PATCH)
 endif
 
-# Append avbpubkey of microdroid-vendor partition into vendor_boot partition.
-ifdef MICRODROID_VENDOR_AVBKEY
-BOARD_AVB_VENDOR_BOOT_ADD_HASH_FOOTER_ARGS += \
-    --prop_from_file com.android.build.microdroid-vendor.avbpubkey:$(MICRODROID_VENDOR_AVBKEY)
+# Append root digest of microdroid-vendor partition's hashtree descriptor into vendor partition.
+ifdef MICRODROID_VENDOR_IMAGE_MODULE
+MICRODROID_VENDOR_IMAGE := \
+    $(call intermediates-dir-for,ETC,$(MICRODROID_VENDOR_IMAGE_MODULE))/$(MICRODROID_VENDOR_IMAGE_MODULE)
+MICRODROID_VENDOR_ROOT_DIGEST := $(PRODUCT_OUT)/microdroid_vendor_root_digest
+BOARD_AVB_VENDOR_ADD_HASHTREE_FOOTER_ARGS += \
+    --prop_from_file com.android.build.microdroid-vendor.root_digest:$(MICRODROID_VENDOR_ROOT_DIGEST)
+$(MICRODROID_VENDOR_ROOT_DIGEST): $(AVBTOOL) $(MICRODROID_VENDOR_IMAGE)
+	$(AVBTOOL) print_partition_digests \
+      --image $(MICRODROID_VENDOR_IMAGE) \
+      | tr -d '\n' | sed -E 's/.*: //g' > $@
+$(INSTALLED_VENDORIMAGE_TARGET): $(MICRODROID_VENDOR_ROOT_DIGEST)
 endif
 
 BOOT_FOOTER_ARGS := BOARD_AVB_BOOT_ADD_HASH_FOOTER_ARGS
@@ -5626,7 +5634,12 @@
 FASTBOOT_INFO_VERSION = 1
 
 INSTALLED_FASTBOOT_INFO_TARGET := $(PRODUCT_OUT)/fastboot-info.txt
-
+ifdef TARGET_BOARD_FASTBOOT_INFO_FILE
+$(INSTALLED_FASTBOOT_INFO_TARGET): $(TARGET_BOARD_FASTBOOT_INFO_FILE)
+	rm -f $@
+	$(call pretty,"Target fastboot-info.txt: $@")
+	$(hide) cp $< $@
+else
 $(INSTALLED_FASTBOOT_INFO_TARGET):
 	rm -f $@
 	$(call pretty,"Target fastboot-info.txt: $@")
@@ -5685,6 +5698,7 @@
 ifeq ($(BOARD_USES_METADATA_PARTITION),true)
 	$(hide) echo "if-wipe erase metadata" >> $@
 endif
+endif
 
 # -----------------------------------------------------------------
 #  misc_info.txt
@@ -6744,7 +6758,7 @@
 
 $(BUILT_TARGET_FILES_PACKAGE): $(BUILT_TARGET_FILES_DIR)
 	@echo "Packaging target files: $@"
-	$(hide) $(SOONG_ZIP) -d -o $@ -C $(zip_root) -r $@.list
+	$(hide) $(SOONG_ZIP) -d -o $@ -C $(zip_root) -r $@.list -sha256
 
 .PHONY: target-files-package
 target-files-package: $(BUILT_TARGET_FILES_PACKAGE)
diff --git a/core/base_rules.mk b/core/base_rules.mk
index f533358..254bfeb 100644
--- a/core/base_rules.mk
+++ b/core/base_rules.mk
@@ -121,9 +121,17 @@
    $(LOCAL_PROPRIETARY_MODULE))
 
 include $(BUILD_SYSTEM)/local_vndk.mk
-include $(BUILD_SYSTEM)/local_systemsdk.mk
+
+# local_current_sdk needs to run before local_systemsdk because the former may override
+# LOCAL_SDK_VERSION which is used by the latter.
 include $(BUILD_SYSTEM)/local_current_sdk.mk
 
+# Check if the use of System SDK is correct. Note that, for Soong modules, the system sdk version
+# check is done in Soong. No need to do it twice.
+ifneq ($(LOCAL_MODULE_MAKEFILE),$(SOONG_ANDROID_MK))
+include $(BUILD_SYSTEM)/local_systemsdk.mk
+endif
+
 # Ninja has an implicit dependency on the command being run, and kati will
 # regenerate the ninja manifest if any read makefile changes, so there is no
 # need to have dependencies on makefiles.
@@ -541,13 +549,14 @@
 
       # Only set up copy rules once, even if another arch variant shares it
       my_vintf_new_pairs := $(filter-out $(ALL_VINTF_MANIFEST_FRAGMENTS_LIST),$(my_vintf_pairs))
-      my_vintf_new_installed := $(call copy-many-vintf-manifest-files-checked,$(my_vintf_new_pairs))
-
       ALL_VINTF_MANIFEST_FRAGMENTS_LIST += $(my_vintf_new_pairs)
 
-      $(my_all_targets) : $(my_vintf_installed)
-      # Install fragments together with the target
-      $(LOCAL_INSTALLED_MODULE) : | $(my_vintf_installed)
+      ifneq ($(LOCAL_MODULE_MAKEFILE),$(SOONG_ANDROID_MK))
+        $(call copy-many-vintf-manifest-files-checked,$(my_vintf_new_pairs))
+        $(my_all_targets) : $(my_vintf_installed)
+        # Install fragments together with the target
+        $(LOCAL_INSTALLED_MODULE) : | $(my_vintf_installed)
+     endif
     endif # my_vintf_fragments
 
     # Rule to install the module's companion init.rc.
@@ -579,13 +588,14 @@
       # Make sure we only set up the copy rules once, even if another arch variant
       # shares a common LOCAL_INIT_RC.
       my_init_rc_new_pairs := $(filter-out $(ALL_INIT_RC_INSTALLED_PAIRS),$(my_init_rc_pairs))
-      my_init_rc_new_installed := $(call copy-many-init-script-files-checked,$(my_init_rc_new_pairs))
-
       ALL_INIT_RC_INSTALLED_PAIRS += $(my_init_rc_new_pairs)
 
-      $(my_all_targets) : $(my_init_rc_installed)
-      # Install init_rc together with the target
-      $(LOCAL_INSTALLED_MODULE) : | $(my_init_rc_installed)
+      ifneq ($(LOCAL_MODULE_MAKEFILE),$(SOONG_ANDROID_MK))
+        $(call copy-many-init-script-files-checked,$(my_init_rc_new_pairs))
+        $(my_all_targets) : $(my_init_rc_installed)
+        # Install init_rc together with the target
+        $(LOCAL_INSTALLED_MODULE) : | $(my_init_rc_installed)
+      endif
     endif # my_init_rc
 
   endif # !LOCAL_IS_HOST_MODULE
@@ -1007,46 +1017,78 @@
 my_required_modules += $(LOCAL_REQUIRED_MODULES_$($(my_prefix)OS))
 endif
 
-ALL_MODULES.$(my_register_name).SHARED_LIBS := \
-    $(ALL_MODULES.$(my_register_name).SHARED_LIBS) $(LOCAL_SHARED_LIBRARIES)
+ifndef LOCAL_SOONG_MODULE_INFO_JSON
+  ALL_MAKE_MODULE_INFO_JSON_MODULES += $(my_register_name)
+  ALL_MODULES.$(my_register_name).SHARED_LIBS := \
+      $(ALL_MODULES.$(my_register_name).SHARED_LIBS) $(LOCAL_SHARED_LIBRARIES)
 
-ALL_MODULES.$(my_register_name).STATIC_LIBS := \
-    $(ALL_MODULES.$(my_register_name).STATIC_LIBS) $(LOCAL_STATIC_LIBRARIES)
+  ALL_MODULES.$(my_register_name).STATIC_LIBS := \
+      $(ALL_MODULES.$(my_register_name).STATIC_LIBS) $(LOCAL_STATIC_LIBRARIES)
 
-ALL_MODULES.$(my_register_name).SYSTEM_SHARED_LIBS := \
-    $(ALL_MODULES.$(my_register_name).SYSTEM_SHARED_LIBS) $(LOCAL_SYSTEM_SHARED_LIBRARIES)
+  ALL_MODULES.$(my_register_name).SYSTEM_SHARED_LIBS := \
+      $(ALL_MODULES.$(my_register_name).SYSTEM_SHARED_LIBS) $(LOCAL_SYSTEM_SHARED_LIBRARIES)
 
-ALL_MODULES.$(my_register_name).LOCAL_RUNTIME_LIBRARIES := \
-    $(ALL_MODULES.$(my_register_name).LOCAL_RUNTIME_LIBRARIES) $(LOCAL_RUNTIME_LIBRARIES) \
-    $(LOCAL_JAVA_LIBRARIES)
+  ALL_MODULES.$(my_register_name).LOCAL_RUNTIME_LIBRARIES := \
+      $(ALL_MODULES.$(my_register_name).LOCAL_RUNTIME_LIBRARIES) $(LOCAL_RUNTIME_LIBRARIES) \
+      $(LOCAL_JAVA_LIBRARIES)
 
-ALL_MODULES.$(my_register_name).LOCAL_STATIC_LIBRARIES := \
-    $(ALL_MODULES.$(my_register_name).LOCAL_STATIC_LIBRARIES) $(LOCAL_STATIC_JAVA_LIBRARIES)
+  ALL_MODULES.$(my_register_name).LOCAL_STATIC_LIBRARIES := \
+      $(ALL_MODULES.$(my_register_name).LOCAL_STATIC_LIBRARIES) $(LOCAL_STATIC_JAVA_LIBRARIES)
 
-ifneq ($(my_test_data_file_pairs),)
-  # Export the list of targets that are handled as data inputs and required
-  # by tests at runtime. The format of my_test_data_file_pairs is
-  # is $(path):$(relative_file) but for module-info, only the string after
-  # ":" is needed.
-  ALL_MODULES.$(my_register_name).TEST_DATA := \
-    $(strip $(ALL_MODULES.$(my_register_name).TEST_DATA) \
-      $(foreach f, $(my_test_data_file_pairs),\
-        $(call word-colon,2,$(f))))
+  ifneq ($(my_test_data_file_pairs),)
+    # Export the list of targets that are handled as data inputs and required
+    # by tests at runtime. The format of my_test_data_file_pairs is
+    # is $(path):$(relative_file) but for module-info, only the string after
+    # ":" is needed.
+    ALL_MODULES.$(my_register_name).TEST_DATA := \
+      $(strip $(ALL_MODULES.$(my_register_name).TEST_DATA) \
+        $(foreach f, $(my_test_data_file_pairs),\
+          $(call word-colon,2,$(f))))
+  endif
+
+  ifdef LOCAL_TEST_DATA_BINS
+    ALL_MODULES.$(my_register_name).TEST_DATA_BINS := \
+        $(ALL_MODULES.$(my_register_name).TEST_DATA_BINS) $(LOCAL_TEST_DATA_BINS)
+  endif
+
+  ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS := \
+      $(ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS) \
+      $(filter-out $(ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS),$(my_supported_variant))
+
+  ALL_MODULES.$(my_register_name).ACONFIG_FILES := \
+      $(ALL_MODULES.$(my_register_name).ACONFIG_FILES) $(LOCAL_ACONFIG_FILES)
+
+  ALL_MODULES.$(my_register_name).COMPATIBILITY_SUITES := \
+      $(ALL_MODULES.$(my_register_name).COMPATIBILITY_SUITES) $(LOCAL_COMPATIBILITY_SUITE)
+  ALL_MODULES.$(my_register_name).MODULE_NAME := $(LOCAL_MODULE)
+  ALL_MODULES.$(my_register_name).TEST_CONFIG := $(test_config)
+  ALL_MODULES.$(my_register_name).EXTRA_TEST_CONFIGS := $(LOCAL_EXTRA_FULL_TEST_CONFIGS)
+  ALL_MODULES.$(my_register_name).TEST_MAINLINE_MODULES := $(LOCAL_TEST_MAINLINE_MODULES)
+  ifdef LOCAL_IS_UNIT_TEST
+    ALL_MODULES.$(my_register_name).IS_UNIT_TEST := $(LOCAL_IS_UNIT_TEST)
+  endif
+  ifdef LOCAL_TEST_OPTIONS_TAGS
+    ALL_MODULES.$(my_register_name).TEST_OPTIONS_TAGS := $(LOCAL_TEST_OPTIONS_TAGS)
+  endif
+
+  ##########################################################
+  # Track module-level dependencies.
+  # (b/204397180) Unlock RECORD_ALL_DEPS was acknowledged reasonable for better Atest performance.
+  ALL_MODULES.$(my_register_name).ALL_DEPS := \
+    $(ALL_MODULES.$(my_register_name).ALL_DEPS) \
+    $(LOCAL_STATIC_LIBRARIES) \
+    $(LOCAL_WHOLE_STATIC_LIBRARIES) \
+    $(LOCAL_SHARED_LIBRARIES) \
+    $(LOCAL_DYLIB_LIBRARIES) \
+    $(LOCAL_RLIB_LIBRARIES) \
+    $(LOCAL_PROC_MACRO_LIBRARIES) \
+    $(LOCAL_HEADER_LIBRARIES) \
+    $(LOCAL_STATIC_JAVA_LIBRARIES) \
+    $(LOCAL_JAVA_LIBRARIES) \
+    $(LOCAL_JNI_SHARED_LIBRARIES)
+
 endif
 
-ifdef LOCAL_TEST_DATA_BINS
-  ALL_MODULES.$(my_register_name).TEST_DATA_BINS := \
-    $(ALL_MODULES.$(my_register_name).TEST_DATA_BINS) $(LOCAL_TEST_DATA_BINS)
-endif
-
-ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS := \
-  $(ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS) \
-  $(filter-out $(ALL_MODULES.$(my_register_name).SUPPORTED_VARIANTS),$(my_supported_variant))
-
-ALL_MODULES.$(my_register_name).ACONFIG_FILES := \
-    $(ALL_MODULES.$(my_register_name).ACONFIG_FILES) $(LOCAL_ACONFIG_FILES)
-
-
 ##########################################################################
 ## When compiling against API imported module, use API import stub
 ## libraries.
@@ -1126,55 +1168,32 @@
         $(call pretty-error,LOCAL_TARGET_REQUIRED_MODULES may not be used from target modules. Use LOCAL_REQUIRED_MODULES instead)
     endif
 endif
-ALL_MODULES.$(my_register_name).EVENT_LOG_TAGS := \
-    $(ALL_MODULES.$(my_register_name).EVENT_LOG_TAGS) $(event_log_tags)
+
+ifdef event_log_tags
+  ALL_MODULES.$(my_register_name).EVENT_LOG_TAGS := \
+      $(ALL_MODULES.$(my_register_name).EVENT_LOG_TAGS) $(event_log_tags)
+endif
+
 ALL_MODULES.$(my_register_name).MAKEFILE := \
     $(ALL_MODULES.$(my_register_name).MAKEFILE) $(LOCAL_MODULE_MAKEFILE)
+
 ifdef LOCAL_MODULE_OWNER
-ALL_MODULES.$(my_register_name).OWNER := \
-    $(sort $(ALL_MODULES.$(my_register_name).OWNER) $(LOCAL_MODULE_OWNER))
+  ALL_MODULES.$(my_register_name).OWNER := \
+      $(sort $(ALL_MODULES.$(my_register_name).OWNER) $(LOCAL_MODULE_OWNER))
 endif
+
 ifdef LOCAL_2ND_ARCH_VAR_PREFIX
 ALL_MODULES.$(my_register_name).FOR_2ND_ARCH := true
 endif
 ALL_MODULES.$(my_register_name).FOR_HOST_CROSS := $(my_host_cross)
-ALL_MODULES.$(my_register_name).MODULE_NAME := $(LOCAL_MODULE)
-ALL_MODULES.$(my_register_name).COMPATIBILITY_SUITES := \
-  $(ALL_MODULES.$(my_register_name).COMPATIBILITY_SUITES) \
-  $(filter-out $(ALL_MODULES.$(my_register_name).COMPATIBILITY_SUITES),$(LOCAL_COMPATIBILITY_SUITE))
-ALL_MODULES.$(my_register_name).TEST_CONFIG := $(test_config)
-ALL_MODULES.$(my_register_name).EXTRA_TEST_CONFIGS := $(LOCAL_EXTRA_FULL_TEST_CONFIGS)
-ALL_MODULES.$(my_register_name).TEST_MAINLINE_MODULES := $(LOCAL_TEST_MAINLINE_MODULES)
 ifndef LOCAL_IS_HOST_MODULE
 ALL_MODULES.$(my_register_name).FILE_CONTEXTS := $(LOCAL_FILE_CONTEXTS)
 ALL_MODULES.$(my_register_name).APEX_KEYS_FILE := $(LOCAL_APEX_KEY_PATH)
 endif
-ifdef LOCAL_IS_UNIT_TEST
-ALL_MODULES.$(my_register_name).IS_UNIT_TEST := $(LOCAL_IS_UNIT_TEST)
-endif
-ifdef LOCAL_TEST_OPTIONS_TAGS
-ALL_MODULES.$(my_register_name).TEST_OPTIONS_TAGS := $(LOCAL_TEST_OPTIONS_TAGS)
-endif
 test_config :=
 
 INSTALLABLE_FILES.$(LOCAL_INSTALLED_MODULE).MODULE := $(my_register_name)
 
-##########################################################
-# Track module-level dependencies.
-# (b/204397180) Unlock RECORD_ALL_DEPS was acknowledged reasonable for better Atest performance.
-ALL_MODULES.$(my_register_name).ALL_DEPS := \
-  $(ALL_MODULES.$(my_register_name).ALL_DEPS) \
-  $(LOCAL_STATIC_LIBRARIES) \
-  $(LOCAL_WHOLE_STATIC_LIBRARIES) \
-  $(LOCAL_SHARED_LIBRARIES) \
-  $(LOCAL_DYLIB_LIBRARIES) \
-  $(LOCAL_RLIB_LIBRARIES) \
-  $(LOCAL_PROC_MACRO_LIBRARIES) \
-  $(LOCAL_HEADER_LIBRARIES) \
-  $(LOCAL_STATIC_JAVA_LIBRARIES) \
-  $(LOCAL_JAVA_LIBRARIES) \
-  $(LOCAL_JNI_SHARED_LIBRARIES)
-
 ###########################################################
 ## umbrella targets used to verify builds
 ###########################################################
diff --git a/core/binary.mk b/core/binary.mk
index 8c107bd..7998a5a 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -274,6 +274,13 @@
   endif
 endif
 
+ifneq ($(LOCAL_MIN_SDK_VERSION),)
+  ifdef LOCAL_IS_HOST_MODULE
+    $(error $(LOCAL_PATH): LOCAL_MIN_SDK_VERSION cannot be used in host module)
+  endif
+  my_api_level := $(LOCAL_MIN_SDK_VERSION)
+endif
+
 ifeq ($(NATIVE_COVERAGE),true)
   ifndef LOCAL_IS_HOST_MODULE
     my_ldflags += -Wl,--wrap,getenv
diff --git a/core/board_config.mk b/core/board_config.mk
index ae11eb6..5a1a781 100644
--- a/core/board_config.mk
+++ b/core/board_config.mk
@@ -186,6 +186,7 @@
   BUILD_BROKEN_VINTF_PRODUCT_COPY_FILES \
   BUILD_BROKEN_INCORRECT_PARTITION_IMAGES \
   BUILD_BROKEN_GENRULE_SANDBOXING \
+  BUILD_BROKEN_DONT_CHECK_SYSTEMSDK \
 
 _build_broken_var_list += \
   $(foreach m,$(AVAILABLE_BUILD_MODULE_TYPES) \
diff --git a/core/clear_vars.mk b/core/clear_vars.mk
index b73e9b4..2b84fcd 100644
--- a/core/clear_vars.mk
+++ b/core/clear_vars.mk
@@ -265,6 +265,7 @@
 LOCAL_SOONG_LICENSE_METADATA :=
 LOCAL_SOONG_LINK_TYPE :=
 LOCAL_SOONG_LINT_REPORTS :=
+LOCAL_SOONG_MODULE_INFO_JSON :=
 LOCAL_SOONG_MODULE_TYPE :=
 LOCAL_SOONG_PROGUARD_DICT :=
 LOCAL_SOONG_PROGUARD_USAGE_ZIP :=
diff --git a/core/combo/arch/x86/goldmont-without-xsaves.mk b/core/combo/arch/x86/goldmont-without-xsaves.mk
new file mode 100644
index 0000000..1b93c17
--- /dev/null
+++ b/core/combo/arch/x86/goldmont-without-xsaves.mk
@@ -0,0 +1,7 @@
+# This file contains feature macro definitions specific to the
+# goldmont-without-xsaves arch variant.
+#
+# See build/make/core/combo/arch/x86/x86-atom.mk for differences.
+#
+
+ARCH_X86_HAVE_SSE4_1 := true
diff --git a/core/combo/arch/x86_64/goldmont-without-xsaves.mk b/core/combo/arch/x86_64/goldmont-without-xsaves.mk
new file mode 100644
index 0000000..1b93c17
--- /dev/null
+++ b/core/combo/arch/x86_64/goldmont-without-xsaves.mk
@@ -0,0 +1,7 @@
+# This file contains feature macro definitions specific to the
+# goldmont-without-xsaves arch variant.
+#
+# See build/make/core/combo/arch/x86/x86-atom.mk for differences.
+#
+
+ARCH_X86_HAVE_SSE4_1 := true
diff --git a/core/config.mk b/core/config.mk
index f8a9879..469be30 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -110,6 +110,7 @@
 $(KATI_obsolete_var BUILD_BROKEN_ENG_DEBUG_TAGS)
 $(KATI_obsolete_export It is a global setting. See $(CHANGES_URL)#export_keyword)
 $(KATI_obsolete_var BUILD_BROKEN_ANDROIDMK_EXPORTS)
+$(KATI_obsolete_var PRODUCT_NOTICE_SPLIT_OVERRIDE,Stop using this, keep calm, and carry on.)
 $(KATI_obsolete_var PRODUCT_STATIC_BOOT_CONTROL_HAL,Use shared library module instead. See $(CHANGES_URL)#PRODUCT_STATIC_BOOT_CONTROL_HAL)
 $(KATI_obsolete_var \
   ARCH_ARM_HAVE_ARMV7A \
@@ -777,16 +778,9 @@
   PRODUCT_FULL_TREBLE := true
 endif
 
-# TODO(b/69865032): Make PRODUCT_NOTICE_SPLIT the default behavior and remove
-#    references to it here and below.
-ifdef PRODUCT_NOTICE_SPLIT_OVERRIDE
-   $(error PRODUCT_NOTICE_SPLIT_OVERRIDE cannot be set.)
-endif
-
 requirements := \
     PRODUCT_TREBLE_LINKER_NAMESPACES \
-    PRODUCT_ENFORCE_VINTF_MANIFEST \
-    PRODUCT_NOTICE_SPLIT
+    PRODUCT_ENFORCE_VINTF_MANIFEST
 
 # If it is overriden, then the requirement override is taken, otherwise it's
 # PRODUCT_FULL_TREBLE
@@ -799,12 +793,20 @@
 PRODUCT_FULL_TREBLE_OVERRIDE ?=
 $(foreach req,$(requirements),$(eval $(req)_OVERRIDE ?=))
 
+# used to be a part of PRODUCT_FULL_TREBLE, but now always set it
+PRODUCT_NOTICE_SPLIT := true
+
 # TODO(b/114488870): disallow PRODUCT_FULL_TREBLE_OVERRIDE from being used.
 .KATI_READONLY := \
     PRODUCT_FULL_TREBLE_OVERRIDE \
     $(foreach req,$(requirements),$(req)_OVERRIDE) \
     $(requirements) \
     PRODUCT_FULL_TREBLE \
+    PRODUCT_NOTICE_SPLIT \
+
+ifneq ($(PRODUCT_FULL_TREBLE),true)
+    $(warning This device does not have Treble enabled. This is unsafe.)
+endif
 
 $(KATI_obsolete_var $(foreach req,$(requirements),$(req)_OVERRIDE) \
     ,This should be referenced without the _OVERRIDE suffix.)
@@ -829,14 +831,10 @@
 # Set BOARD_SYSTEMSDK_VERSIONS to the latest SystemSDK version starting from P-launching
 # devices if unset.
 ifndef BOARD_SYSTEMSDK_VERSIONS
-  ifdef PRODUCT_SHIPPING_API_LEVEL
-  ifneq ($(call math_gt_or_eq,$(PRODUCT_SHIPPING_API_LEVEL),28),)
-    ifeq (REL,$(PLATFORM_VERSION_CODENAME))
-      BOARD_SYSTEMSDK_VERSIONS := $(PLATFORM_SDK_VERSION)
-    else
-      BOARD_SYSTEMSDK_VERSIONS := $(PLATFORM_VERSION_CODENAME)
-    endif
-  endif
+  ifeq (REL,$(PLATFORM_VERSION_CODENAME))
+    BOARD_SYSTEMSDK_VERSIONS := $(PLATFORM_SDK_VERSION)
+  else
+    BOARD_SYSTEMSDK_VERSIONS := $(PLATFORM_VERSION_CODENAME)
   endif
 endif
 
@@ -890,40 +888,23 @@
 
 # SEPolicy versions
 
-# PLATFORM_SEPOLICY_VERSION is a number of the form "NN.m" with "NN" mapping to
-# PLATFORM_SDK_VERSION and "m" as a minor number which allows for SELinux
-# changes independent of PLATFORM_SDK_VERSION.  This value will be set to
-# 10000.0 to represent tip-of-tree development that is inherently unstable and
-# thus designed not to work with any shipping vendor policy.  This is similar in
-# spirit to how DEFAULT_APP_TARGET_SDK is set.
-# The minor version ('m' component) must be updated every time a platform release
-# is made which breaks compatibility with the previous platform sepolicy version,
-# not just on every increase in PLATFORM_SDK_VERSION.  The minor version should
-# be reset to 0 on every bump of the PLATFORM_SDK_VERSION.
-sepolicy_major_vers := 34
-sepolicy_minor_vers := 0
+# PLATFORM_SEPOLICY_VERSION is a number of the form "YYYYMM.0" with "YYYYMM"
+# mapping to vFRC version.  This value will be set to 1000000.0 to represent
+# tip-of-tree development that is inherently unstable and thus designed not to
+# work with any shipping vendor policy.  This is similar in spirit to how
+# DEFAULT_APP_TARGET_SDK is set.
+sepolicy_vers := $(BOARD_API_LEVEL).0
 
-ifneq ($(sepolicy_major_vers), $(PLATFORM_SDK_VERSION))
-$(error sepolicy_major_version does not match PLATFORM_SDK_VERSION, please update.)
-endif
-
-TOT_SEPOLICY_VERSION := 10000.0
-ifneq (REL,$(PLATFORM_VERSION_CODENAME))
-    PLATFORM_SEPOLICY_VERSION := $(TOT_SEPOLICY_VERSION)
+TOT_SEPOLICY_VERSION := 1000000.0
+ifeq (true,$(BOARD_API_LEVEL_FROZEN))
+    PLATFORM_SEPOLICY_VERSION := $(sepolicy_vers)
 else
-    PLATFORM_SEPOLICY_VERSION := $(join $(addsuffix .,$(sepolicy_major_vers)), $(sepolicy_minor_vers))
+    PLATFORM_SEPOLICY_VERSION := $(TOT_SEPOLICY_VERSION)
 endif
-sepolicy_major_vers :=
-sepolicy_minor_vers :=
+sepolicy_vers :=
 
-# BOARD_SEPOLICY_VERS must take the format "NN.m" and contain the sepolicy
-# version identifier corresponding to the sepolicy on which the non-platform
-# policy is to be based. If unspecified, this will build against the current
-# public platform policy in tree
-ifndef BOARD_SEPOLICY_VERS
-# The default platform policy version.
 BOARD_SEPOLICY_VERS := $(PLATFORM_SEPOLICY_VERSION)
-endif
+.KATI_READONLY := PLATFORM_SEPOLICY_VERSION BOARD_SEPOLICY_VERS
 
 # A list of SEPolicy versions, besides PLATFORM_SEPOLICY_VERSION, that the framework supports.
 PLATFORM_SEPOLICY_COMPAT_VERSIONS := $(filter-out $(PLATFORM_SEPOLICY_VERSION), \
diff --git a/core/definitions.mk b/core/definitions.mk
index 7a6c064..1f2d011 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -37,6 +37,8 @@
 # sub-variables.
 ALL_MODULES:=
 
+ALL_MAKE_MODULE_INFO_JSON_MODULES:=
+
 # The relative paths of the non-module targets in the system.
 ALL_NON_MODULES:=
 NON_MODULES_WITHOUT_LICENSE_METADATA:=
@@ -3120,14 +3122,12 @@
 
 # Copies many init script files and check they are well-formed.
 # $(1): The init script files to copy.  Each entry is a ':' separated src:dst pair.
-# Evaluates to the list of the dst files. (ie suitable for a dependency list.)
 define copy-many-init-script-files-checked
 $(foreach f, $(1), $(strip \
     $(eval _cmf_tuple := $(subst :, ,$(f))) \
     $(eval _cmf_src := $(word 1,$(_cmf_tuple))) \
     $(eval _cmf_dest := $(word 2,$(_cmf_tuple))) \
-    $(eval $(call copy-init-script-file-checked,$(_cmf_src),$(_cmf_dest))) \
-    $(_cmf_dest)))
+    $(eval $(call copy-init-script-file-checked,$(_cmf_src),$(_cmf_dest)))))
 endef
 
 # Copy the file only if it's a well-formed xml file. For use via $(eval).
@@ -3165,14 +3165,12 @@
 
 # Copies many vintf manifest files checked.
 # $(1): The files to copy.  Each entry is a ':' separated src:dst pair
-# Evaluates to the list of the dst files (ie suitable for a dependency list)
 define copy-many-vintf-manifest-files-checked
 $(foreach f, $(1), $(strip \
     $(eval _cmf_tuple := $(subst :, ,$(f))) \
     $(eval _cmf_src := $(word 1,$(_cmf_tuple))) \
     $(eval _cmf_dest := $(word 2,$(_cmf_tuple))) \
-    $(eval $(call copy-vintf-manifest-checked,$(_cmf_src),$(_cmf_dest))) \
-    $(_cmf_dest)))
+    $(eval $(call copy-vintf-manifest-checked,$(_cmf_src),$(_cmf_dest)))))
 endef
 
 # Copy the file only if it's not an ELF file. For use via $(eval).
diff --git a/core/local_current_sdk.mk b/core/local_current_sdk.mk
index ea7da8a..ccdbf77 100644
--- a/core/local_current_sdk.mk
+++ b/core/local_current_sdk.mk
@@ -14,13 +14,24 @@
 # limitations under the License.
 #
 ifdef BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES
-  ifneq (current,$(BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES))
+  _override_to := $(BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES)
+
+  # b/314011075: apks and jars in the vendor or odm partitions cannot use
+  # system SDK 35 and beyond. In order not to suddenly break those vendor
+  # modules using current or system_current as their LOCAL_SDK_VERSION,
+  # override it to 34, which is the maximum API level allowed for them.
+  ifneq (,$(filter JAVA_LIBRARIES APPS,$(LOCAL_MODULE_CLASS)))
+    _override_to := 34
+  endif
+
+  ifneq (current,$(_override_to))
     ifneq (,$(filter true,$(LOCAL_VENDOR_MODULE) $(LOCAL_ODM_MODULE) $(LOCAL_PROPRIETARY_MODULE)))
       ifeq (current,$(LOCAL_SDK_VERSION))
-        LOCAL_SDK_VERSION := $(BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES)
+        LOCAL_SDK_VERSION := $(_override_to)
       else ifeq (system_current,$(LOCAL_SDK_VERSION))
-        LOCAL_SDK_VERSION := system_$(BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES)
+        LOCAL_SDK_VERSION := system_$(_override_to)
       endif
     endif
   endif
+  _override_to :=
 endif
diff --git a/core/local_systemsdk.mk b/core/local_systemsdk.mk
index 460073d..3307e72 100644
--- a/core/local_systemsdk.mk
+++ b/core/local_systemsdk.mk
@@ -33,6 +33,9 @@
           # Runtime resource overlays are exempted from building against System SDK.
           # TODO(b/155027019): remove this, after no product/vendor apps rely on this behavior.
           LOCAL_SDK_VERSION := system_current
+          # We have run below again since LOCAL_SDK_VERSION is newly set and the "_current"
+          # may have to be updated
+          include $(BUILD_SYSTEM)/local_current_sdk.mk
         endif
       endif
     endif
@@ -54,10 +57,35 @@
     # If not, vendor apks are treated equally to system apps
     _supported_systemsdk_versions := $(PLATFORM_SYSTEMSDK_VERSIONS)
   endif
+
+  # b/314011075: apks and jars in the vendor or odm partitions cannot use system SDK 35 and beyond.
+  # This is to discourage the use of Java APIs in the partitions, which hasn't been supported since
+  # the beginning of the project Treble back in Android 10. Ultimately, we'd like to completely
+  # disallow any Java API in the partitions, but it shall be done progressively.
+  ifneq (,$(filter true,$(LOCAL_VENDOR_MODULE) $(LOCAL_ODM_MODULE) $(LOCAL_PROPRIETARY_MODULE)))
+    # 28 is the API level when BOARD_SYSTEMSDK_VERSIONS was introduced. So, it's the oldset API
+    # we allow.
+    _supported_systemsdk_versions := $(call int_range_list, 28, 34)
+  endif
+
+  # Extract version number from LOCAL_SDK_VERSION (ex: system_34 -> 34)
   _system_sdk_version := $(call get-numeric-sdk-version,$(LOCAL_SDK_VERSION))
+  # However, the extraction may fail if it doesn't have any number (i.e. current, core_current,
+  # system_current, or similar) Then use the latest platform SDK version number or the actual
+  # codename.
+  ifeq (,$(_system_sdk_version)
+    ifeq (REL,$(PLATFORM_VERSION_CODENAME))
+      _system_sdk_version := $(PLATFORM_SDK_VERSION)
+    else
+      _system_sdk_version := $(PLATFORM_VERSION_CODENAME)
+    endif
+  endif
+
   ifneq ($(_system_sdk_version),$(filter $(_system_sdk_version),$(_supported_systemsdk_versions)))
-    $(call pretty-error,Incompatible LOCAL_SDK_VERSION '$(LOCAL_SDK_VERSION)'. \
-           System SDK version '$(_system_sdk_version)' is not supported. Supported versions are: $(_supported_systemsdk_versions))
+    ifneq (true,$(BUILD_BROKEN_DONT_CHECK_SYSTEMSDK)
+      $(call pretty-error,Incompatible LOCAL_SDK_VERSION '$(LOCAL_SDK_VERSION)'. \
+             System SDK version '$(_system_sdk_version)' is not supported. Supported versions are: $(_supported_systemsdk_versions))
+    endif
   endif
   _system_sdk_version :=
   _supported_systemsdk_versions :=
diff --git a/core/main.mk b/core/main.mk
index 348a964..649c75c 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -1721,10 +1721,8 @@
 # dist_files only for putting your library into the dist directory with a full build.
 .PHONY: dist_files
 
-ifeq ($(SOONG_COLLECT_JAVA_DEPS), true)
-  $(call dist-for-goals, dist_files, $(SOONG_OUT_DIR)/module_bp_java_deps.json)
-  $(call dist-for-goals, dist_files, $(PRODUCT_OUT)/module-info.json)
-endif
+$(call dist-for-goals, dist_files, $(SOONG_OUT_DIR)/module_bp_java_deps.json)
+$(call dist-for-goals, dist_files, $(PRODUCT_OUT)/module-info.json)
 
 .PHONY: apps_only
 ifeq ($(HOST_OS),darwin)
diff --git a/core/soong_config.mk b/core/soong_config.mk
index 193ac18..b6ce2a7 100644
--- a/core/soong_config.mk
+++ b/core/soong_config.mk
@@ -299,6 +299,7 @@
 $(call add_json_bool, BuildBrokenVendorPropertyNamespace,  $(filter true,$(BUILD_BROKEN_VENDOR_PROPERTY_NAMESPACE)))
 $(call add_json_bool, BuildBrokenIncorrectPartitionImages, $(filter true,$(BUILD_BROKEN_INCORRECT_PARTITION_IMAGES)))
 $(call add_json_list, BuildBrokenInputDirModules,          $(BUILD_BROKEN_INPUT_DIR_MODULES))
+$(call add_json_bool, BuildBrokenDontCheckSystemSdk,       $(filter true,$(BUILD_BROKEN_DONT_CHECK_SYSTEMSDK)))
 
 $(call add_json_list, BuildWarningBadOptionalUsesLibsAllowlist,    $(BUILD_WARNING_BAD_OPTIONAL_USES_LIBS_ALLOWLIST))
 
diff --git a/core/tasks/module-info.mk b/core/tasks/module-info.mk
index eb5c63c..8546828 100644
--- a/core/tasks/module-info.mk
+++ b/core/tasks/module-info.mk
@@ -13,10 +13,15 @@
 $(if $(strip $(2)),'$(COMMA)$(strip $(1)): "$(strip $(2))"')
 endef
 
-$(MODULE_INFO_JSON):
+SOONG_MODULE_INFO := $(SOONG_OUT_DIR)/module-info-$(TARGET_PRODUCT).json
+
+$(MODULE_INFO_JSON): PRIVATE_SOONG_MODULE_INFO := $(SOONG_MODULE_INFO)
+$(MODULE_INFO_JSON): PRIVATE_MERGE_JSON_OBJECTS := $(HOST_OUT_EXECUTABLES)/merge_module_info_json
+$(MODULE_INFO_JSON): $(HOST_OUT_EXECUTABLES)/merge_module_info_json
+$(MODULE_INFO_JSON): $(SOONG_MODULE_INFO)
 	@echo Generating $@
-	$(hide) echo -ne '{\n ' > $@
-	$(hide) echo -ne $(KATI_foreach_sep m,$(COMMA)$(_NEWLINE), $(sort $(ALL_MODULES)),\
+	$(hide) echo -ne '{\n ' > $@.tmp
+	$(hide) echo -ne $(KATI_foreach_sep m,$(COMMA)$(_NEWLINE), $(sort $(ALL_MAKE_MODULE_INFO_JSON_MODULES)),\
 		'"$(m)": {' \
 			'"module_name": "$(ALL_MODULES.$(m).MODULE_NAME)"' \
 			$(call write-optional-json-list, "class", $(sort $(ALL_MODULES.$(m).CLASS))) \
@@ -43,7 +48,9 @@
 			$(call write-optional-json-list, "supported_variants", $(sort $(ALL_MODULES.$(m).SUPPORTED_VARIANTS))) \
 			$(call write-optional-json-list, "host_dependencies", $(sort $(ALL_MODULES.$(m).HOST_REQUIRED_FROM_TARGET))) \
 			$(call write-optional-json-list, "target_dependencies", $(sort $(ALL_MODULES.$(m).TARGET_REQUIRED_FROM_HOST))) \
-		'}')'\n}\n' >> $@
+		'}')'\n}\n' >> $@.tmp
+	$(PRIVATE_MERGE_JSON_OBJECTS) -o $@ $(PRIVATE_SOONG_MODULE_INFO) $@.tmp
+	rm $@.tmp
 
 
 droidcore-unbundled: $(MODULE_INFO_JSON)
diff --git a/envsetup.sh b/envsetup.sh
index cc808d2..5aa11c7 100644
--- a/envsetup.sh
+++ b/envsetup.sh
@@ -62,7 +62,7 @@
               invocations of 'm' etc.
 - tapas:      tapas [<App1> <App2> ...] [arm|x86|arm64|x86_64] [eng|userdebug|user]
               Sets up the build environment for building unbundled apps (APKs).
-- banchan:    banchan <module1> [<module2> ...] \
+- banchan:    banchan <module1> [<module2> ...] \\
                       [arm|x86|arm64|riscv64|x86_64|arm64_only|x86_64only] [eng|userdebug|user]
               Sets up the build environment for building unbundled modules (APEXes).
 - croot:      Changes directory to the top of the tree, or a subdirectory thereof.
@@ -775,7 +775,7 @@
         answer=$1
     else
         print_lunch_menu
-        echo "Which would you like? [aosp_arm-trunk_staging-eng]"
+        echo "Which would you like? [aosp_cf_x86_64_phone-trunk_staging-eng]"
         echo -n "Pick from common choices above (e.g. 13) or specify your own (e.g. aosp_barbet-trunk_staging-eng): "
         read answer
         used_lunch_menu=1
@@ -785,10 +785,10 @@
 
     if [ -z "$answer" ]
     then
-        selection=aosp_arm-trunk_staging-eng
+        selection=aosp_cf_x86_64_phone-trunk_staging-eng
     elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
     then
-        local choices=($(TARGET_BUILD_APPS= get_build_var COMMON_LUNCH_CHOICES))
+        local choices=($(TARGET_BUILD_APPS= TARGET_PRODUCT= TARGET_RELEASE= TARGET_BUILD_VARIANT= get_build_var COMMON_LUNCH_CHOICES 2>/dev/null))
         if [ $answer -le ${#choices[@]} ]
         then
             # array in zsh starts from 1 instead of 0.
diff --git a/target/product/OWNERS b/target/product/OWNERS
index 008e4a2..48d3f2a 100644
--- a/target/product/OWNERS
+++ b/target/product/OWNERS
@@ -1,4 +1,4 @@
-per-file runtime_libart.mk = calin@google.com, mast@google.com, ngeoffray@google.com, oth@google.com, rpl@google.com, vmarko@google.com
+per-file runtime_libart.mk = mast@google.com, ngeoffray@google.com, rpl@google.com, vmarko@google.com
 
 # GSI
 per-file gsi_release.mk = file:/target/product/gsi/OWNERS
@@ -7,4 +7,4 @@
 # Android Go
 per-file go_defaults.mk = gkaiser@google.com, kushg@google.com, rajekumar@google.com
 per-file go_defaults_512.mk = gkaiser@google.com, kushg@google.com, rajekumar@google.com
-per-file go_defaults_common.mk = gkaiser@google.com, kushg@google.com, rajekumar@google.com
\ No newline at end of file
+per-file go_defaults_common.mk = gkaiser@google.com, kushg@google.com, rajekumar@google.com
diff --git a/target/product/base_system.mk b/target/product/base_system.mk
index 098ed27..6a101da9 100644
--- a/target/product/base_system.mk
+++ b/target/product/base_system.mk
@@ -70,7 +70,7 @@
     com.android.scheduling \
     com.android.sdkext \
     com.android.tethering \
-    com.android.tzdata \
+    $(RELEASE_PACKAGE_TZDATA_MODULE) \
     com.android.uwb \
     com.android.virt \
     com.android.wifi \
diff --git a/target/product/gsi/current.txt b/target/product/gsi/current.txt
index 53c9e0c..80aecb7 100644
--- a/target/product/gsi/current.txt
+++ b/target/product/gsi/current.txt
@@ -18,6 +18,7 @@
 LLNDK: libneuralnetworks.so
 LLNDK: libselinux.so
 LLNDK: libsync.so
+LLNDK: libvendorsupport.so
 LLNDK: libvndksupport.so
 LLNDK: libvulkan.so
 VNDK-SP: android.hardware.common-V2-ndk.so
diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp
index 82bfa7e..d5b5b8f 100644
--- a/tools/aconfig/Android.bp
+++ b/tools/aconfig/Android.bp
@@ -44,7 +44,6 @@
     protos: ["protos/aconfig.proto"],
     crate_name: "aconfig_protos",
     source_stem: "aconfig_protos",
-    use_protobuf3: true,
     host_supported: true,
 }
 
@@ -97,6 +96,12 @@
     srcs: ["tests/test_exported.aconfig"],
 }
 
+aconfig_declarations {
+    name: "aconfig.test.forcereadonly.flags",
+    package: "com.android.aconfig.test.forcereadonly",
+    srcs: ["tests/test_force_read_only.aconfig"],
+}
+
 aconfig_values {
     name: "aconfig.test.flag.values",
     package: "com.android.aconfig.test",
@@ -126,6 +131,12 @@
     mode: "exported",
 }
 
+java_aconfig_library {
+    name: "aconfig_test_java_library_forcereadonly",
+    aconfig_declarations: "aconfig.test.forcereadonly.flags",
+    mode: "force-read-only",
+}
+
 android_test {
     name: "aconfig.test.java",
     srcs: [
@@ -136,6 +147,7 @@
     static_libs: [
         "aconfig_test_java_library",
         "aconfig_test_java_library_exported",
+        "aconfig_test_java_library_forcereadonly",
         "androidx.test.rules",
         "testng",
     ],
@@ -174,6 +186,18 @@
     mode: "test",
 }
 
+cc_aconfig_library {
+    name: "aconfig_test_cpp_library_exported_variant",
+    aconfig_declarations: "aconfig.test.flags",
+    mode: "exported",
+}
+
+cc_aconfig_library {
+    name: "aconfig_test_cpp_library_force_read_only_variant",
+    aconfig_declarations: "aconfig.test.flags",
+    mode: "force-read-only",
+}
+
 cc_test {
     name: "aconfig.test.cpp",
     srcs: [
@@ -204,6 +228,36 @@
     test_suites: ["general-tests"],
 }
 
+cc_test {
+    name: "aconfig.test.cpp.exported_mode",
+    srcs: [
+        "tests/aconfig_exported_mode_test.cpp",
+    ],
+    static_libs: [
+        "aconfig_test_cpp_library_exported_variant",
+        "libgmock",
+    ],
+    shared_libs: [
+        "server_configurable_flags",
+    ],
+    test_suites: ["general-tests"],
+}
+
+cc_test {
+    name: "aconfig.test.cpp.force_read_only_mode",
+    srcs: [
+        "tests/aconfig_force_read_only_mode_test.cpp",
+    ],
+    static_libs: [
+        "aconfig_test_cpp_library_force_read_only_variant",
+        "libgmock",
+    ],
+    shared_libs: [
+        "server_configurable_flags",
+    ],
+    test_suites: ["general-tests"],
+}
+
 rust_aconfig_library {
     name: "libaconfig_test_rust_library",
     crate_name: "aconfig_test_rust_library",
@@ -238,3 +292,39 @@
     ],
     test_suites: ["general-tests"],
 }
+
+rust_aconfig_library {
+    name: "libaconfig_test_rust_library_with_exported_mode",
+    crate_name: "aconfig_test_rust_library",
+    aconfig_declarations: "aconfig.test.flags",
+    mode: "exported",
+}
+
+rust_test {
+    name: "aconfig.exported_mode.test.rust",
+    srcs: [
+        "tests/aconfig_exported_mode_test.rs"
+    ],
+    rustlibs: [
+        "libaconfig_test_rust_library_with_exported_mode",
+    ],
+    test_suites: ["general-tests"],
+}
+
+rust_aconfig_library {
+    name: "libaconfig_test_rust_library_with_force_read_only_mode",
+    crate_name: "aconfig_test_rust_library",
+    aconfig_declarations: "aconfig.test.flags",
+    mode: "force-read-only",
+}
+
+rust_test {
+    name: "aconfig.force_read_only_mode.test.rust",
+    srcs: [
+        "tests/aconfig_force_read_only_mode_test.rs"
+    ],
+    rustlibs: [
+        "libaconfig_test_rust_library_with_force_read_only_mode",
+    ],
+    test_suites: ["general-tests"],
+}
diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml
index 2edf4b8..7b58e94 100644
--- a/tools/aconfig/Cargo.toml
+++ b/tools/aconfig/Cargo.toml
@@ -20,6 +20,3 @@
 
 [build-dependencies]
 protobuf-codegen = "3.2.0"
-
-[dev-dependencies]
-itertools = "0.10.5"
diff --git a/tools/aconfig/TEST_MAPPING b/tools/aconfig/TEST_MAPPING
index e29918f..de8d932 100644
--- a/tools/aconfig/TEST_MAPPING
+++ b/tools/aconfig/TEST_MAPPING
@@ -24,6 +24,10 @@
       "name": "aconfig.test"
     },
     {
+      // aconfig Java integration tests (host)
+      "name": "AconfigJavaHostTest"
+    },
+    {
       // aconfig Java integration tests
       "name": "aconfig.test.java"
     },
@@ -36,14 +40,22 @@
       "name": "aconfig.test.cpp.test_mode"
     },
     {
-      // aconfig C++ integration tests (production mode auto-generated code)
+      // aconfig C++ integration tests (exported mode auto-generated code)
+      "name": "aconfig.test.cpp.exported_mode"
+    },
+    {
+      // aconfig Rust integration tests (production mode auto-generated code)
       "name": "aconfig.prod_mode.test.rust"
     },
     {
-      // aconfig C++ integration tests (test mode auto-generated code)
+      // aconfig Rust integration tests (test mode auto-generated code)
       "name": "aconfig.test_mode.test.rust"
     },
     {
+      // aconfig Rust integration tests (exported mode auto-generated code)
+      "name": "aconfig.exported_mode.test.rust"
+    },
+    {
       // printflags unit tests
       "name": "printflags.test"
     }
diff --git a/tools/aconfig/src/codegen/cpp.rs b/tools/aconfig/src/codegen/cpp.rs
index 000581b..1279d8e 100644
--- a/tools/aconfig/src/codegen/cpp.rs
+++ b/tools/aconfig/src/codegen/cpp.rs
@@ -20,20 +20,21 @@
 use tinytemplate::TinyTemplate;
 
 use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
 use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
 
-pub fn generate_cpp_code<'a, I>(
+pub fn generate_cpp_code<I>(
     package: &str,
     parsed_flags_iter: I,
     codegen_mode: CodegenMode,
 ) -> Result<Vec<OutputFile>>
 where
-    I: Iterator<Item = &'a ProtoParsedFlag>,
+    I: Iterator<Item = ProtoParsedFlag>,
 {
     let mut readwrite_count = 0;
     let class_elements: Vec<ClassElement> = parsed_flags_iter
-        .map(|pf| create_class_element(package, pf, &mut readwrite_count))
+        .map(|pf| create_class_element(package, &pf, &mut readwrite_count))
         .collect();
     let readwrite = readwrite_count > 0;
     let has_fixed_read_only = class_elements.iter().any(|item| item.is_fixed_read_only);
@@ -49,7 +50,7 @@
         has_fixed_read_only,
         readwrite,
         readwrite_count,
-        for_test: codegen_mode == CodegenMode::Test,
+        is_test_mode: codegen_mode == CodegenMode::Test,
         class_elements,
     };
 
@@ -92,7 +93,7 @@
     pub has_fixed_read_only: bool,
     pub readwrite: bool,
     pub readwrite_count: i32,
-    pub for_test: bool,
+    pub is_test_mode: bool,
     pub class_elements: Vec<ClassElement>,
 }
 
@@ -135,6 +136,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::protos::ProtoParsedFlags;
     use std::collections::HashMap;
 
     const EXPORTED_PROD_HEADER_EXPECTED: &str = r#"
@@ -148,6 +150,10 @@
 #define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
 #endif
 
+#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED
+#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED true
+#endif
+
 #ifdef __cplusplus
 
 #include <memory>
@@ -168,6 +174,8 @@
 
     virtual bool enabled_fixed_ro() = 0;
 
+    virtual bool enabled_fixed_ro_exported() = 0;
+
     virtual bool enabled_ro() = 0;
 
     virtual bool enabled_ro_exported() = 0;
@@ -197,6 +205,10 @@
     return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
 }
 
+inline bool enabled_fixed_ro_exported() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+}
+
 inline bool enabled_ro() {
     return true;
 }
@@ -224,6 +236,8 @@
 
 bool com_android_aconfig_test_enabled_fixed_ro();
 
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
 bool com_android_aconfig_test_enabled_ro();
 
 bool com_android_aconfig_test_enabled_ro_exported();
@@ -269,6 +283,10 @@
 
     virtual void enabled_fixed_ro(bool val) = 0;
 
+    virtual bool enabled_fixed_ro_exported() = 0;
+
+    virtual void enabled_fixed_ro_exported(bool val) = 0;
+
     virtual bool enabled_ro() = 0;
 
     virtual void enabled_ro(bool val) = 0;
@@ -326,6 +344,14 @@
     provider_->enabled_fixed_ro(val);
 }
 
+inline bool enabled_fixed_ro_exported() {
+    return provider_->enabled_fixed_ro_exported();
+}
+
+inline void enabled_fixed_ro_exported(bool val) {
+    provider_->enabled_fixed_ro_exported(val);
+}
+
 inline bool enabled_ro() {
     return provider_->enabled_ro();
 }
@@ -379,6 +405,10 @@
 
 void set_com_android_aconfig_test_enabled_fixed_ro(bool val);
 
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
+void set_com_android_aconfig_test_enabled_fixed_ro_exported(bool val);
+
 bool com_android_aconfig_test_enabled_ro();
 
 void set_com_android_aconfig_test_enabled_ro(bool val);
@@ -401,6 +431,138 @@
 
 "#;
 
+    const EXPORTED_EXPORTED_HEADER_EXPECTED: &str = r#"
+#pragma once
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace com::android::aconfig::test {
+
+class flag_provider_interface {
+public:
+    virtual ~flag_provider_interface() = default;
+
+    virtual bool disabled_rw_exported() = 0;
+
+    virtual bool enabled_fixed_ro_exported() = 0;
+
+    virtual bool enabled_ro_exported() = 0;
+};
+
+extern std::unique_ptr<flag_provider_interface> provider_;
+
+inline bool disabled_rw_exported() {
+    return provider_->disabled_rw_exported();
+}
+
+inline bool enabled_fixed_ro_exported() {
+    return provider_->enabled_fixed_ro_exported();
+}
+
+inline bool enabled_ro_exported() {
+    return provider_->enabled_ro_exported();
+}
+
+}
+
+extern "C" {
+#endif // __cplusplus
+
+bool com_android_aconfig_test_disabled_rw_exported();
+
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
+bool com_android_aconfig_test_enabled_ro_exported();
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+"#;
+
+    const EXPORTED_FORCE_READ_ONLY_HEADER_EXPECTED: &str = r#"
+#pragma once
+
+#ifndef COM_ANDROID_ACONFIG_TEST
+#define COM_ANDROID_ACONFIG_TEST(FLAG) COM_ANDROID_ACONFIG_TEST_##FLAG
+#endif
+
+#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO
+#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
+#endif
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace com::android::aconfig::test {
+
+class flag_provider_interface {
+public:
+    virtual ~flag_provider_interface() = default;
+
+    virtual bool disabled_ro() = 0;
+
+    virtual bool disabled_rw() = 0;
+
+    virtual bool disabled_rw_in_other_namespace() = 0;
+
+    virtual bool enabled_fixed_ro() = 0;
+
+    virtual bool enabled_ro() = 0;
+
+    virtual bool enabled_rw() = 0;
+};
+
+extern std::unique_ptr<flag_provider_interface> provider_;
+
+inline bool disabled_ro() {
+    return false;
+}
+
+inline bool disabled_rw() {
+    return false;
+}
+
+inline bool disabled_rw_in_other_namespace() {
+    return false;
+}
+
+inline bool enabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+inline bool enabled_ro() {
+    return true;
+}
+
+inline bool enabled_rw() {
+    return true;
+}
+
+}
+
+extern "C" {
+#endif // __cplusplus
+
+bool com_android_aconfig_test_disabled_ro();
+
+bool com_android_aconfig_test_disabled_rw();
+
+bool com_android_aconfig_test_disabled_rw_in_other_namespace();
+
+bool com_android_aconfig_test_enabled_fixed_ro();
+
+bool com_android_aconfig_test_enabled_ro();
+
+bool com_android_aconfig_test_enabled_rw();
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+"#;
+
     const PROD_SOURCE_FILE_EXPECTED: &str = r#"
 #include "com_android_aconfig_test.h"
 #include <server_configurable_flags/get_flags.h>
@@ -449,6 +611,10 @@
                 return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
             }
 
+            virtual bool enabled_fixed_ro_exported() override {
+                return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+            }
+
             virtual bool enabled_ro() override {
                 return true;
             }
@@ -495,6 +661,10 @@
     return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
 }
 
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+}
+
 bool com_android_aconfig_test_enabled_ro() {
     return true;
 }
@@ -600,6 +770,19 @@
                 overrides_["enabled_fixed_ro"] = val;
             }
 
+            virtual bool enabled_fixed_ro_exported() override {
+                auto it = overrides_.find("enabled_fixed_ro_exported");
+                  if (it != overrides_.end()) {
+                      return it->second;
+                } else {
+                  return true;
+                }
+            }
+
+            virtual void enabled_fixed_ro_exported(bool val) override {
+                overrides_["enabled_fixed_ro_exported"] = val;
+            }
+
             virtual bool enabled_ro() override {
                 auto it = overrides_.find("enabled_ro");
                   if (it != overrides_.end()) {
@@ -696,6 +879,13 @@
     com::android::aconfig::test::enabled_fixed_ro(val);
 }
 
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+    return com::android::aconfig::test::enabled_fixed_ro_exported();
+}
+
+void set_com_android_aconfig_test_enabled_fixed_ro_exported(bool val) {
+    com::android::aconfig::test::enabled_fixed_ro_exported(val);
+}
 
 bool com_android_aconfig_test_enabled_ro() {
     return com::android::aconfig::test::enabled_ro();
@@ -732,10 +922,256 @@
 
 "#;
 
-    fn test_generate_cpp_code(mode: CodegenMode) {
-        let parsed_flags = crate::test::parse_test_flags();
+    const EXPORTED_SOURCE_FILE_EXPECTED: &str = r#"
+#include "com_android_aconfig_test.h"
+#include <server_configurable_flags/get_flags.h>
+#include <vector>
+
+namespace com::android::aconfig::test {
+
+    class flag_provider : public flag_provider_interface {
+        public:
+            virtual bool disabled_rw_exported() override {
+                if (cache_[0] == -1) {
+                    cache_[0] = server_configurable_flags::GetServerConfigurableFlag(
+                        "aconfig_flags.aconfig_test",
+                        "com.android.aconfig.test.disabled_rw_exported",
+                        "false") == "true";
+                }
+                return cache_[0];
+            }
+
+            virtual bool enabled_fixed_ro_exported() override {
+                if (cache_[1] == -1) {
+                    cache_[1] = server_configurable_flags::GetServerConfigurableFlag(
+                        "aconfig_flags.aconfig_test",
+                        "com.android.aconfig.test.enabled_fixed_ro_exported",
+                        "false") == "true";
+                }
+                return cache_[1];
+            }
+
+            virtual bool enabled_ro_exported() override {
+                if (cache_[2] == -1) {
+                    cache_[2] = server_configurable_flags::GetServerConfigurableFlag(
+                        "aconfig_flags.aconfig_test",
+                        "com.android.aconfig.test.enabled_ro_exported",
+                        "false") == "true";
+                }
+                return cache_[2];
+            }
+
+    private:
+        std::vector<int8_t> cache_ = std::vector<int8_t>(3, -1);
+    };
+
+    std::unique_ptr<flag_provider_interface> provider_ =
+        std::make_unique<flag_provider>();
+}
+
+bool com_android_aconfig_test_disabled_rw_exported() {
+    return com::android::aconfig::test::disabled_rw_exported();
+}
+
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+    return com::android::aconfig::test::enabled_fixed_ro_exported();
+}
+
+bool com_android_aconfig_test_enabled_ro_exported() {
+    return com::android::aconfig::test::enabled_ro_exported();
+}
+
+
+"#;
+
+    const FORCE_READ_ONLY_SOURCE_FILE_EXPECTED: &str = r#"
+#include "com_android_aconfig_test.h"
+
+namespace com::android::aconfig::test {
+
+    class flag_provider : public flag_provider_interface {
+        public:
+
+            virtual bool disabled_ro() override {
+                return false;
+            }
+
+            virtual bool disabled_rw() override {
+                return false;
+            }
+
+            virtual bool disabled_rw_in_other_namespace() override {
+                return false;
+            }
+
+            virtual bool enabled_fixed_ro() override {
+                return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+            }
+
+            virtual bool enabled_ro() override {
+                return true;
+            }
+
+            virtual bool enabled_rw() override {
+                return true;
+            }
+    };
+
+    std::unique_ptr<flag_provider_interface> provider_ =
+        std::make_unique<flag_provider>();
+}
+
+bool com_android_aconfig_test_disabled_ro() {
+    return false;
+}
+
+bool com_android_aconfig_test_disabled_rw() {
+    return false;
+}
+
+bool com_android_aconfig_test_disabled_rw_in_other_namespace() {
+    return false;
+}
+
+bool com_android_aconfig_test_enabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+bool com_android_aconfig_test_enabled_ro() {
+    return true;
+}
+
+bool com_android_aconfig_test_enabled_rw() {
+    return true;
+}
+
+"#;
+
+    const READ_ONLY_EXPORTED_PROD_HEADER_EXPECTED: &str = r#"
+#pragma once
+
+#ifndef COM_ANDROID_ACONFIG_TEST
+#define COM_ANDROID_ACONFIG_TEST(FLAG) COM_ANDROID_ACONFIG_TEST_##FLAG
+#endif
+
+#ifndef COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO
+#define COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO false
+#endif
+
+#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO
+#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
+#endif
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace com::android::aconfig::test {
+
+class flag_provider_interface {
+public:
+    virtual ~flag_provider_interface() = default;
+
+    virtual bool disabled_fixed_ro() = 0;
+
+    virtual bool disabled_ro() = 0;
+
+    virtual bool enabled_fixed_ro() = 0;
+
+    virtual bool enabled_ro() = 0;
+};
+
+extern std::unique_ptr<flag_provider_interface> provider_;
+
+inline bool disabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+}
+
+inline bool disabled_ro() {
+    return false;
+}
+
+inline bool enabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+inline bool enabled_ro() {
+    return true;
+}
+}
+
+extern "C" {
+#endif // __cplusplus
+
+bool com_android_aconfig_test_disabled_fixed_ro();
+
+bool com_android_aconfig_test_disabled_ro();
+
+bool com_android_aconfig_test_enabled_fixed_ro();
+
+bool com_android_aconfig_test_enabled_ro();
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+"#;
+
+    const READ_ONLY_PROD_SOURCE_FILE_EXPECTED: &str = r#"
+#include "com_android_aconfig_test.h"
+
+namespace com::android::aconfig::test {
+
+    class flag_provider : public flag_provider_interface {
+        public:
+
+            virtual bool disabled_fixed_ro() override {
+                return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+            }
+
+            virtual bool disabled_ro() override {
+                return false;
+            }
+
+            virtual bool enabled_fixed_ro() override {
+                return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+            }
+
+            virtual bool enabled_ro() override {
+                return true;
+            }
+    };
+
+    std::unique_ptr<flag_provider_interface> provider_ =
+        std::make_unique<flag_provider>();
+}
+
+bool com_android_aconfig_test_disabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+}
+
+bool com_android_aconfig_test_disabled_ro() {
+    return false;
+}
+
+bool com_android_aconfig_test_enabled_fixed_ro() {
+    return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+bool com_android_aconfig_test_enabled_ro() {
+    return true;
+}
+"#;
+
+    fn test_generate_cpp_code(
+        parsed_flags: ProtoParsedFlags,
+        mode: CodegenMode,
+        expected_header: &str,
+        expected_src: &str,
+    ) {
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
         let generated =
-            generate_cpp_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter(), mode)
+            generate_cpp_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
                 .unwrap();
         let mut generated_files_map = HashMap::new();
         for file in generated {
@@ -750,12 +1186,7 @@
         assert_eq!(
             None,
             crate::test::first_significant_code_diff(
-                match mode {
-                    CodegenMode::Production => EXPORTED_PROD_HEADER_EXPECTED,
-                    CodegenMode::Test => EXPORTED_TEST_HEADER_EXPECTED,
-                    CodegenMode::Exported =>
-                        todo!("exported mode not yet supported for cpp, see b/313894653."),
-                },
+                expected_header,
                 generated_files_map.get(&target_file_path).unwrap()
             )
         );
@@ -765,12 +1196,7 @@
         assert_eq!(
             None,
             crate::test::first_significant_code_diff(
-                match mode {
-                    CodegenMode::Production => PROD_SOURCE_FILE_EXPECTED,
-                    CodegenMode::Test => TEST_SOURCE_FILE_EXPECTED,
-                    CodegenMode::Exported =>
-                        todo!("exported mode not yet supported for cpp, see b/313894653."),
-                },
+                expected_src,
                 generated_files_map.get(&target_file_path).unwrap()
             )
         );
@@ -778,11 +1204,56 @@
 
     #[test]
     fn test_generate_cpp_code_for_prod() {
-        test_generate_cpp_code(CodegenMode::Production);
+        let parsed_flags = crate::test::parse_test_flags();
+        test_generate_cpp_code(
+            parsed_flags,
+            CodegenMode::Production,
+            EXPORTED_PROD_HEADER_EXPECTED,
+            PROD_SOURCE_FILE_EXPECTED,
+        );
     }
 
     #[test]
     fn test_generate_cpp_code_for_test() {
-        test_generate_cpp_code(CodegenMode::Test);
+        let parsed_flags = crate::test::parse_test_flags();
+        test_generate_cpp_code(
+            parsed_flags,
+            CodegenMode::Test,
+            EXPORTED_TEST_HEADER_EXPECTED,
+            TEST_SOURCE_FILE_EXPECTED,
+        );
+    }
+
+    #[test]
+    fn test_generate_cpp_code_for_exported() {
+        let parsed_flags = crate::test::parse_test_flags();
+        test_generate_cpp_code(
+            parsed_flags,
+            CodegenMode::Exported,
+            EXPORTED_EXPORTED_HEADER_EXPECTED,
+            EXPORTED_SOURCE_FILE_EXPECTED,
+        );
+    }
+
+    #[test]
+    fn test_generate_cpp_code_for_force_read_only() {
+        let parsed_flags = crate::test::parse_test_flags();
+        test_generate_cpp_code(
+            parsed_flags,
+            CodegenMode::ForceReadOnly,
+            EXPORTED_FORCE_READ_ONLY_HEADER_EXPECTED,
+            FORCE_READ_ONLY_SOURCE_FILE_EXPECTED,
+        );
+    }
+
+    #[test]
+    fn test_generate_cpp_code_for_read_only_prod() {
+        let parsed_flags = crate::test::parse_read_only_test_flags();
+        test_generate_cpp_code(
+            parsed_flags,
+            CodegenMode::Production,
+            READ_ONLY_EXPORTED_PROD_HEADER_EXPECTED,
+            READ_ONLY_PROD_SOURCE_FILE_EXPECTED,
+        );
     }
 }
diff --git a/tools/aconfig/src/codegen/java.rs b/tools/aconfig/src/codegen/java.rs
index ae3f274..78e892b 100644
--- a/tools/aconfig/src/codegen/java.rs
+++ b/tools/aconfig/src/codegen/java.rs
@@ -14,28 +14,27 @@
  * limitations under the License.
  */
 
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use serde::Serialize;
 use std::collections::{BTreeMap, BTreeSet};
 use std::path::PathBuf;
 use tinytemplate::TinyTemplate;
 
 use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
 use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
 
-pub fn generate_java_code<'a, I>(
+pub fn generate_java_code<I>(
     package: &str,
     parsed_flags_iter: I,
     codegen_mode: CodegenMode,
 ) -> Result<Vec<OutputFile>>
 where
-    I: Iterator<Item = &'a ProtoParsedFlag>,
+    I: Iterator<Item = ProtoParsedFlag>,
 {
     let flag_elements: Vec<FlagElement> =
-        parsed_flags_iter.map(|pf| create_flag_element(package, pf)).collect();
-    let exported_flag_elements: Vec<FlagElement> =
-        flag_elements.iter().filter(|elem| elem.exported).cloned().collect();
+        parsed_flags_iter.map(|pf| create_flag_element(package, &pf)).collect();
     let namespace_flags = gen_flags_by_namespace(&flag_elements);
     let properties_set: BTreeSet<String> =
         flag_elements.iter().map(|fe| format_property_name(&fe.device_config_namespace)).collect();
@@ -44,13 +43,8 @@
     let runtime_lookup_required =
         flag_elements.iter().any(|elem| elem.is_read_write) || library_exported;
 
-    if library_exported && exported_flag_elements.is_empty() {
-        return Err(anyhow!("exported library contains no exported flags"));
-    }
-
     let context = Context {
         flag_elements,
-        exported_flag_elements,
         namespace_flags,
         is_test_mode,
         runtime_lookup_required,
@@ -109,7 +103,6 @@
 #[derive(Serialize)]
 struct Context {
     pub flag_elements: Vec<FlagElement>,
-    pub exported_flag_elements: Vec<FlagElement>,
     pub namespace_flags: Vec<NamespaceFlags>,
     pub is_test_mode: bool,
     pub runtime_lookup_required: bool,
@@ -133,7 +126,6 @@
     pub is_read_write: bool,
     pub method_name: String,
     pub properties: String,
-    pub exported: bool,
 }
 
 fn create_flag_element(package: &str, pf: &ProtoParsedFlag) -> FlagElement {
@@ -147,7 +139,6 @@
         is_read_write: pf.permission() == ProtoFlagPermission::READ_WRITE,
         method_name: format_java_method_name(pf.name()),
         properties: format_property_name(pf.namespace()),
-        exported: pf.is_exported.unwrap_or(false),
     }
 }
 
@@ -202,6 +193,9 @@
         boolean enabledFixedRo();
         @com.android.aconfig.annotations.AssumeTrueForR8
         @UnsupportedAppUsage
+        boolean enabledFixedRoExported();
+        @com.android.aconfig.annotations.AssumeTrueForR8
+        @UnsupportedAppUsage
         boolean enabledRo();
         @com.android.aconfig.annotations.AssumeTrueForR8
         @UnsupportedAppUsage
@@ -228,6 +222,8 @@
         /** @hide */
         public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro";
         /** @hide */
+        public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
+        /** @hide */
         public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro";
         /** @hide */
         public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
@@ -258,6 +254,11 @@
         }
         @com.android.aconfig.annotations.AssumeTrueForR8
         @UnsupportedAppUsage
+        public static boolean enabledFixedRoExported() {
+            return FEATURE_FLAGS.enabledFixedRoExported();
+        }
+        @com.android.aconfig.annotations.AssumeTrueForR8
+        @UnsupportedAppUsage
         public static boolean enabledRo() {
             return FEATURE_FLAGS.enabledRo();
         }
@@ -310,6 +311,11 @@
         }
         @Override
         @UnsupportedAppUsage
+        public boolean enabledFixedRoExported() {
+            return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED);
+        }
+        @Override
+        @UnsupportedAppUsage
         public boolean enabledRo() {
             return getValue(Flags.FLAG_ENABLED_RO);
         }
@@ -348,6 +354,7 @@
                 Map.entry(Flags.FLAG_DISABLED_RW_EXPORTED, false),
                 Map.entry(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, false),
                 Map.entry(Flags.FLAG_ENABLED_FIXED_RO, false),
+                Map.entry(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, false),
                 Map.entry(Flags.FLAG_ENABLED_RO, false),
                 Map.entry(Flags.FLAG_ENABLED_RO_EXPORTED, false),
                 Map.entry(Flags.FLAG_ENABLED_RW, false)
@@ -359,12 +366,12 @@
     #[test]
     fn test_generate_java_code_production() {
         let parsed_flags = crate::test::parse_test_flags();
-        let generated_files = generate_java_code(
-            crate::test::TEST_PACKAGE,
-            parsed_flags.parsed_flag.iter(),
-            CodegenMode::Production,
-        )
-        .unwrap();
+        let mode = CodegenMode::Production;
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+        let generated_files =
+            generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+                .unwrap();
         let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
             + r#"
             private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
@@ -463,6 +470,11 @@
             }
             @Override
             @UnsupportedAppUsage
+            public boolean enabledFixedRoExported() {
+                return true;
+            }
+            @Override
+            @UnsupportedAppUsage
             public boolean enabledRo() {
                 return true;
             }
@@ -512,12 +524,12 @@
     #[test]
     fn test_generate_java_code_exported() {
         let parsed_flags = crate::test::parse_test_flags();
-        let generated_files = generate_java_code(
-            crate::test::TEST_PACKAGE,
-            parsed_flags.parsed_flag.iter(),
-            CodegenMode::Exported,
-        )
-        .unwrap();
+        let mode = CodegenMode::Exported;
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+        let generated_files =
+            generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+                .unwrap();
 
         let expect_flags_content = r#"
         package com.android.aconfig.test;
@@ -528,6 +540,8 @@
             /** @hide */
             public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported";
             /** @hide */
+            public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
+            /** @hide */
             public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
 
             @UnsupportedAppUsage
@@ -535,6 +549,10 @@
                 return FEATURE_FLAGS.disabledRwExported();
             }
             @UnsupportedAppUsage
+            public static boolean enabledFixedRoExported() {
+                return FEATURE_FLAGS.enabledFixedRoExported();
+            }
+            @UnsupportedAppUsage
             public static boolean enabledRoExported() {
                 return FEATURE_FLAGS.enabledRoExported();
             }
@@ -551,6 +569,8 @@
             @UnsupportedAppUsage
             boolean disabledRwExported();
             @UnsupportedAppUsage
+            boolean enabledFixedRoExported();
+            @UnsupportedAppUsage
             boolean enabledRoExported();
         }
         "#;
@@ -564,8 +584,8 @@
         /** @hide */
         public final class FeatureFlagsImpl implements FeatureFlags {
             private static boolean aconfig_test_is_cached = false;
-            private static boolean other_namespace_is_cached = false;
             private static boolean disabledRwExported = false;
+            private static boolean enabledFixedRoExported = false;
             private static boolean enabledRoExported = false;
 
 
@@ -574,6 +594,8 @@
                     Properties properties = DeviceConfig.getProperties("aconfig_test");
                     disabledRwExported =
                         properties.getBoolean("com.android.aconfig.test.disabled_rw_exported", false);
+                    enabledFixedRoExported =
+                        properties.getBoolean("com.android.aconfig.test.enabled_fixed_ro_exported", false);
                     enabledRoExported =
                         properties.getBoolean("com.android.aconfig.test.enabled_ro_exported", false);
                 } catch (NullPointerException e) {
@@ -589,22 +611,6 @@
                 aconfig_test_is_cached = true;
             }
 
-            private void load_overrides_other_namespace() {
-                try {
-                    Properties properties = DeviceConfig.getProperties("other_namespace");
-                } catch (NullPointerException e) {
-                    throw new RuntimeException(
-                        "Cannot read value from namespace other_namespace "
-                        + "from DeviceConfig. It could be that the code using flag "
-                        + "executed before SettingsProvider initialization. Please use "
-                        + "fixed read-only flag by adding is_fixed_read_only: true in "
-                        + "flag declaration.",
-                        e
-                    );
-                }
-                other_namespace_is_cached = true;
-            }
-
             @Override
             @UnsupportedAppUsage
             public boolean disabledRwExported() {
@@ -616,6 +622,15 @@
 
             @Override
             @UnsupportedAppUsage
+            public boolean enabledFixedRoExported() {
+                if (!aconfig_test_is_cached) {
+                    load_overrides_aconfig_test();
+                }
+                return enabledFixedRoExported;
+            }
+
+            @Override
+            @UnsupportedAppUsage
             public boolean enabledRoExported() {
                 if (!aconfig_test_is_cached) {
                     load_overrides_aconfig_test();
@@ -642,6 +657,11 @@
             }
             @Override
             @UnsupportedAppUsage
+            public boolean enabledFixedRoExported() {
+                return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED);
+            }
+            @Override
+            @UnsupportedAppUsage
             public boolean enabledRoExported() {
                 return getValue(Flags.FLAG_ENABLED_RO_EXPORTED);
             }
@@ -666,6 +686,7 @@
             private Map<String, Boolean> mFlagMap = new HashMap<>(
                 Map.ofEntries(
                     Map.entry(Flags.FLAG_DISABLED_RW_EXPORTED, false),
+                    Map.entry(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, false),
                     Map.entry(Flags.FLAG_ENABLED_RO_EXPORTED, false)
                 )
             );
@@ -703,12 +724,12 @@
     #[test]
     fn test_generate_java_code_test() {
         let parsed_flags = crate::test::parse_test_flags();
-        let generated_files = generate_java_code(
-            crate::test::TEST_PACKAGE,
-            parsed_flags.parsed_flag.iter(),
-            CodegenMode::Test,
-        )
-        .unwrap();
+        let mode = CodegenMode::Test;
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+        let generated_files =
+            generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+                .unwrap();
 
         let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
             + r#"
@@ -759,6 +780,12 @@
             }
             @Override
             @UnsupportedAppUsage
+            public boolean enabledFixedRoExported() {
+                throw new UnsupportedOperationException(
+                    "Method is not implemented.");
+            }
+            @Override
+            @UnsupportedAppUsage
             public boolean enabledRo() {
                 throw new UnsupportedOperationException(
                     "Method is not implemented.");
@@ -807,6 +834,228 @@
     }
 
     #[test]
+    fn test_generate_java_code_force_read_only() {
+        let parsed_flags = crate::test::parse_test_flags();
+        let mode = CodegenMode::ForceReadOnly;
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+        let generated_files =
+            generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+                .unwrap();
+        let expect_featureflags_content = r#"
+        package com.android.aconfig.test;
+        // TODO(b/303773055): Remove the annotation after access issue is resolved.
+        import android.compat.annotation.UnsupportedAppUsage;
+        /** @hide */
+        public interface FeatureFlags {
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            boolean disabledRo();
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            boolean disabledRw();
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            boolean disabledRwInOtherNamespace();
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            boolean enabledFixedRo();
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            boolean enabledRo();
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            boolean enabledRw();
+        }"#;
+
+        let expect_featureflagsimpl_content = r#"
+        package com.android.aconfig.test;
+        // TODO(b/303773055): Remove the annotation after access issue is resolved.
+        import android.compat.annotation.UnsupportedAppUsage;
+        /** @hide */
+        public final class FeatureFlagsImpl implements FeatureFlags {
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRo() {
+                return false;
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRw() {
+                return false;
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRwInOtherNamespace() {
+                return false;
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledFixedRo() {
+                return true;
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledRo() {
+                return true;
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledRw() {
+                return true;
+            }
+        }
+        "#;
+
+        let expect_flags_content = r#"
+        package com.android.aconfig.test;
+        // TODO(b/303773055): Remove the annotation after access issue is resolved.
+        import android.compat.annotation.UnsupportedAppUsage;
+        /** @hide */
+        public final class Flags {
+            /** @hide */
+            public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro";
+            /** @hide */
+            public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw";
+            /** @hide */
+            public static final String FLAG_DISABLED_RW_IN_OTHER_NAMESPACE = "com.android.aconfig.test.disabled_rw_in_other_namespace";
+            /** @hide */
+            public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro";
+            /** @hide */
+            public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro";
+            /** @hide */
+            public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw";
+
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            public static boolean disabledRo() {
+                return FEATURE_FLAGS.disabledRo();
+            }
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            public static boolean disabledRw() {
+                return FEATURE_FLAGS.disabledRw();
+            }
+            @com.android.aconfig.annotations.AssumeFalseForR8
+            @UnsupportedAppUsage
+            public static boolean disabledRwInOtherNamespace() {
+                return FEATURE_FLAGS.disabledRwInOtherNamespace();
+            }
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            public static boolean enabledFixedRo() {
+                return FEATURE_FLAGS.enabledFixedRo();
+            }
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            public static boolean enabledRo() {
+                return FEATURE_FLAGS.enabledRo();
+            }
+            @com.android.aconfig.annotations.AssumeTrueForR8
+            @UnsupportedAppUsage
+            public static boolean enabledRw() {
+                return FEATURE_FLAGS.enabledRw();
+            }
+            private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
+        }"#;
+
+        let expect_fakefeatureflags_content = r#"
+        package com.android.aconfig.test;
+        // TODO(b/303773055): Remove the annotation after access issue is resolved.
+        import android.compat.annotation.UnsupportedAppUsage;
+        import java.util.HashMap;
+        import java.util.Map;
+        /** @hide */
+        public class FakeFeatureFlagsImpl implements FeatureFlags {
+            public FakeFeatureFlagsImpl() {
+                resetAll();
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRo() {
+                return getValue(Flags.FLAG_DISABLED_RO);
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRw() {
+                return getValue(Flags.FLAG_DISABLED_RW);
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean disabledRwInOtherNamespace() {
+                return getValue(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE);
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledFixedRo() {
+                return getValue(Flags.FLAG_ENABLED_FIXED_RO);
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledRo() {
+                return getValue(Flags.FLAG_ENABLED_RO);
+            }
+            @Override
+            @UnsupportedAppUsage
+            public boolean enabledRw() {
+                return getValue(Flags.FLAG_ENABLED_RW);
+            }
+            public void setFlag(String flagName, boolean value) {
+                if (!this.mFlagMap.containsKey(flagName)) {
+                    throw new IllegalArgumentException("no such flag " + flagName);
+                }
+                this.mFlagMap.put(flagName, value);
+            }
+            public void resetAll() {
+                for (Map.Entry entry : mFlagMap.entrySet()) {
+                    entry.setValue(null);
+                }
+            }
+            private boolean getValue(String flagName) {
+                Boolean value = this.mFlagMap.get(flagName);
+                if (value == null) {
+                    throw new IllegalArgumentException(flagName + " is not set");
+                }
+                return value;
+            }
+            private Map<String, Boolean> mFlagMap = new HashMap<>(
+                Map.ofEntries(
+                    Map.entry(Flags.FLAG_DISABLED_RO, false),
+                    Map.entry(Flags.FLAG_DISABLED_RW, false),
+                    Map.entry(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, false),
+                    Map.entry(Flags.FLAG_ENABLED_FIXED_RO, false),
+                    Map.entry(Flags.FLAG_ENABLED_RO, false),
+                    Map.entry(Flags.FLAG_ENABLED_RW, false)
+                )
+            );
+        }
+        "#;
+        let mut file_set = HashMap::from([
+            ("com/android/aconfig/test/Flags.java", expect_flags_content),
+            ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content),
+            ("com/android/aconfig/test/FeatureFlags.java", expect_featureflags_content),
+            ("com/android/aconfig/test/FakeFeatureFlagsImpl.java", expect_fakefeatureflags_content),
+        ]);
+
+        for file in generated_files {
+            let file_path = file.path.to_str().unwrap();
+            assert!(file_set.contains_key(file_path), "Cannot find {}", file_path);
+            assert_eq!(
+                None,
+                crate::test::first_significant_code_diff(
+                    file_set.get(file_path).unwrap(),
+                    &String::from_utf8(file.contents).unwrap()
+                ),
+                "File {} content is not correct",
+                file_path
+            );
+            file_set.remove(file_path);
+        }
+
+        assert!(file_set.is_empty());
+    }
+
+    #[test]
     fn test_format_java_method_name() {
         let expected = "someSnakeName";
         let input = "____some_snake___name____";
diff --git a/tools/aconfig/src/codegen/mod.rs b/tools/aconfig/src/codegen/mod.rs
index abc27c6..64ffa8b 100644
--- a/tools/aconfig/src/codegen/mod.rs
+++ b/tools/aconfig/src/codegen/mod.rs
@@ -19,6 +19,7 @@
 pub mod rust;
 
 use anyhow::{ensure, Result};
+use clap::ValueEnum;
 
 pub fn is_valid_name_ident(s: &str) -> bool {
     // Identifiers must match [a-z][a-z0-9_]*, except consecutive underscores are not allowed
@@ -43,7 +44,7 @@
 }
 
 pub fn is_valid_container_ident(s: &str) -> bool {
-    is_valid_name_ident(s) || s.split('.').all(is_valid_name_ident)
+    s.split('.').all(is_valid_name_ident)
 }
 
 pub fn create_device_config_ident(package: &str, flag_name: &str) -> Result<String> {
@@ -52,6 +53,25 @@
     Ok(format!("{}.{}", package, flag_name))
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
+pub enum CodegenMode {
+    Exported,
+    ForceReadOnly,
+    Production,
+    Test,
+}
+
+impl std::fmt::Display for CodegenMode {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            CodegenMode::Exported => write!(f, "exported"),
+            CodegenMode::ForceReadOnly => write!(f, "force-read-only"),
+            CodegenMode::Production => write!(f, "production"),
+            CodegenMode::Test => write!(f, "test"),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/tools/aconfig/src/codegen/rust.rs b/tools/aconfig/src/codegen/rust.rs
index 04be93b..8a88ffe 100644
--- a/tools/aconfig/src/codegen/rust.rs
+++ b/tools/aconfig/src/codegen/rust.rs
@@ -19,19 +19,20 @@
 use tinytemplate::TinyTemplate;
 
 use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
 use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
 
-pub fn generate_rust_code<'a, I>(
+pub fn generate_rust_code<I>(
     package: &str,
     parsed_flags_iter: I,
     codegen_mode: CodegenMode,
 ) -> Result<OutputFile>
 where
-    I: Iterator<Item = &'a ProtoParsedFlag>,
+    I: Iterator<Item = ProtoParsedFlag>,
 {
     let template_flags: Vec<TemplateParsedFlag> =
-        parsed_flags_iter.map(|pf| TemplateParsedFlag::new(package, pf)).collect();
+        parsed_flags_iter.map(|pf| TemplateParsedFlag::new(package, &pf)).collect();
     let has_readwrite = template_flags.iter().any(|item| item.readwrite);
     let context = TemplateContext {
         package: package.to_string(),
@@ -43,10 +44,9 @@
     template.add_template(
         "rust_code_gen",
         match codegen_mode {
-            CodegenMode::Production => include_str!("../../templates/rust_prod.template"),
             CodegenMode::Test => include_str!("../../templates/rust_test.template"),
-            CodegenMode::Exported => {
-                todo!("exported mode not yet supported for rust, see b/313894653.")
+            CodegenMode::Exported | CodegenMode::ForceReadOnly | CodegenMode::Production => {
+                include_str!("../../templates/rust.template")
             }
         },
     )?;
@@ -153,6 +153,11 @@
         true
     }
 
+    /// query flag enabled_fixed_ro_exported
+    pub fn enabled_fixed_ro_exported(&self) -> bool {
+        true
+    }
+
     /// query flag enabled_ro
     pub fn enabled_ro(&self) -> bool {
         true
@@ -202,6 +207,12 @@
     true
 }
 
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+    true
+}
+
 /// query flag enabled_ro
 #[inline(always)]
 pub fn enabled_ro() -> bool {
@@ -302,6 +313,18 @@
         self.overrides.insert("enabled_fixed_ro", val);
     }
 
+    /// query flag enabled_fixed_ro_exported
+    pub fn enabled_fixed_ro_exported(&self) -> bool {
+        self.overrides.get("enabled_fixed_ro_exported").copied().unwrap_or(
+            true
+        )
+    }
+
+    /// set flag enabled_fixed_ro_exported
+    pub fn set_enabled_fixed_ro_exported(&mut self, val: bool) {
+        self.overrides.insert("enabled_fixed_ro_exported", val);
+    }
+
     /// query flag enabled_ro
     pub fn enabled_ro(&self) -> bool {
         self.overrides.get("enabled_ro").copied().unwrap_or(
@@ -412,6 +435,18 @@
     PROVIDER.lock().unwrap().set_enabled_fixed_ro(val);
 }
 
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+    PROVIDER.lock().unwrap().enabled_fixed_ro_exported()
+}
+
+/// set flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn set_enabled_fixed_ro_exported(val: bool) {
+    PROVIDER.lock().unwrap().set_enabled_fixed_ro_exported(val);
+}
+
 /// query flag enabled_ro
 #[inline(always)]
 pub fn enabled_ro() -> bool {
@@ -454,10 +489,156 @@
 }
 "#;
 
+    const EXPORTED_EXPECTED: &str = r#"
+//! codegenerated rust flag lib
+
+/// flag provider
+pub struct FlagProvider;
+
+lazy_static::lazy_static! {
+    /// flag value cache for disabled_rw_exported
+    static ref CACHED_disabled_rw_exported: bool = flags_rust::GetServerConfigurableFlag(
+        "aconfig_flags.aconfig_test",
+        "com.android.aconfig.test.disabled_rw_exported",
+        "false") == "true";
+
+    /// flag value cache for enabled_fixed_ro_exported
+    static ref CACHED_enabled_fixed_ro_exported: bool = flags_rust::GetServerConfigurableFlag(
+        "aconfig_flags.aconfig_test",
+        "com.android.aconfig.test.enabled_fixed_ro_exported",
+        "false") == "true";
+
+    /// flag value cache for enabled_ro_exported
+    static ref CACHED_enabled_ro_exported: bool = flags_rust::GetServerConfigurableFlag(
+        "aconfig_flags.aconfig_test",
+        "com.android.aconfig.test.enabled_ro_exported",
+        "false") == "true";
+
+}
+
+impl FlagProvider {
+    /// query flag disabled_rw_exported
+    pub fn disabled_rw_exported(&self) -> bool {
+        *CACHED_disabled_rw_exported
+    }
+
+    /// query flag enabled_fixed_ro_exported
+    pub fn enabled_fixed_ro_exported(&self) -> bool {
+        *CACHED_enabled_fixed_ro_exported
+    }
+
+    /// query flag enabled_ro_exported
+    pub fn enabled_ro_exported(&self) -> bool {
+        *CACHED_enabled_ro_exported
+    }
+}
+
+/// flag provider
+pub static PROVIDER: FlagProvider = FlagProvider;
+
+/// query flag disabled_rw_exported
+#[inline(always)]
+pub fn disabled_rw_exported() -> bool {
+    PROVIDER.disabled_rw_exported()
+}
+
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+    PROVIDER.enabled_fixed_ro_exported()
+}
+
+/// query flag enabled_ro_exported
+#[inline(always)]
+pub fn enabled_ro_exported() -> bool {
+    PROVIDER.enabled_ro_exported()
+}
+"#;
+
+    const FORCE_READ_ONLY_EXPECTED: &str = r#"
+//! codegenerated rust flag lib
+
+/// flag provider
+pub struct FlagProvider;
+
+impl FlagProvider {
+    /// query flag disabled_ro
+    pub fn disabled_ro(&self) -> bool {
+        false
+    }
+
+    /// query flag disabled_rw
+    pub fn disabled_rw(&self) -> bool {
+        false
+    }
+
+    /// query flag disabled_rw_in_other_namespace
+    pub fn disabled_rw_in_other_namespace(&self) -> bool {
+        false
+    }
+
+    /// query flag enabled_fixed_ro
+    pub fn enabled_fixed_ro(&self) -> bool {
+        true
+    }
+
+    /// query flag enabled_ro
+    pub fn enabled_ro(&self) -> bool {
+        true
+    }
+
+    /// query flag enabled_rw
+    pub fn enabled_rw(&self) -> bool {
+        true
+    }
+}
+
+/// flag provider
+pub static PROVIDER: FlagProvider = FlagProvider;
+
+/// query flag disabled_ro
+#[inline(always)]
+pub fn disabled_ro() -> bool {
+    false
+}
+
+/// query flag disabled_rw
+#[inline(always)]
+pub fn disabled_rw() -> bool {
+    false
+}
+
+/// query flag disabled_rw_in_other_namespace
+#[inline(always)]
+pub fn disabled_rw_in_other_namespace() -> bool {
+    false
+}
+
+/// query flag enabled_fixed_ro
+#[inline(always)]
+pub fn enabled_fixed_ro() -> bool {
+    true
+}
+
+/// query flag enabled_ro
+#[inline(always)]
+pub fn enabled_ro() -> bool {
+    true
+}
+
+/// query flag enabled_rw
+#[inline(always)]
+pub fn enabled_rw() -> bool {
+    true
+}
+"#;
+
     fn test_generate_rust_code(mode: CodegenMode) {
         let parsed_flags = crate::test::parse_test_flags();
+        let modified_parsed_flags =
+            crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
         let generated =
-            generate_rust_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter(), mode)
+            generate_rust_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
                 .unwrap();
         assert_eq!("src/lib.rs", format!("{}", generated.path.display()));
         assert_eq!(
@@ -466,8 +647,8 @@
                 match mode {
                     CodegenMode::Production => PROD_EXPECTED,
                     CodegenMode::Test => TEST_EXPECTED,
-                    CodegenMode::Exported =>
-                        todo!("exported mode not yet supported for rust, see b/313894653."),
+                    CodegenMode::Exported => EXPORTED_EXPECTED,
+                    CodegenMode::ForceReadOnly => FORCE_READ_ONLY_EXPECTED,
                 },
                 &String::from_utf8(generated.contents).unwrap()
             )
@@ -483,4 +664,14 @@
     fn test_generate_rust_code_for_test() {
         test_generate_rust_code(CodegenMode::Test);
     }
+
+    #[test]
+    fn test_generate_rust_code_for_exported() {
+        test_generate_rust_code(CodegenMode::Exported);
+    }
+
+    #[test]
+    fn test_generate_rust_code_for_force_read_only() {
+        test_generate_rust_code(CodegenMode::ForceReadOnly);
+    }
 }
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
index e437c02..f7a6417 100644
--- a/tools/aconfig/src/commands.rs
+++ b/tools/aconfig/src/commands.rs
@@ -15,20 +15,22 @@
  */
 
 use anyhow::{bail, ensure, Context, Result};
-use clap::ValueEnum;
+use itertools::Itertools;
 use protobuf::Message;
+use std::collections::HashMap;
 use std::io::Read;
 use std::path::PathBuf;
 
 use crate::codegen::cpp::generate_cpp_code;
 use crate::codegen::java::generate_java_code;
 use crate::codegen::rust::generate_rust_code;
-use crate::storage::generate_storage_files;
-
+use crate::codegen::CodegenMode;
+use crate::dump::{DumpFormat, DumpPredicate};
 use crate::protos::{
     ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag,
     ProtoParsedFlags, ProtoTracepoint,
 };
+use crate::storage::generate_storage_files;
 
 pub struct Input {
     pub source: String,
@@ -188,38 +190,37 @@
     Ok(output)
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
-pub enum CodegenMode {
-    Production,
-    Test,
-    Exported,
-}
-
 pub fn create_java_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<Vec<OutputFile>> {
     let parsed_flags = input.try_parse_flags()?;
-    let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
-    let Some(package) = find_unique_package(&filtered_parsed_flags) else {
+    let modified_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, codegen_mode)?;
+    let Some(package) = find_unique_package(&modified_parsed_flags) else {
         bail!("no parsed flags, or the parsed flags use different packages");
     };
-    generate_java_code(package, filtered_parsed_flags.iter(), codegen_mode)
+    let package = package.to_string();
+    let _flag_ids = assign_flag_ids(&package, modified_parsed_flags.iter())?;
+    generate_java_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
 }
 
 pub fn create_cpp_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<Vec<OutputFile>> {
     let parsed_flags = input.try_parse_flags()?;
-    let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
-    let Some(package) = find_unique_package(&filtered_parsed_flags) else {
+    let modified_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, codegen_mode)?;
+    let Some(package) = find_unique_package(&modified_parsed_flags) else {
         bail!("no parsed flags, or the parsed flags use different packages");
     };
-    generate_cpp_code(package, filtered_parsed_flags.iter(), codegen_mode)
+    let package = package.to_string();
+    let _flag_ids = assign_flag_ids(&package, modified_parsed_flags.iter())?;
+    generate_cpp_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
 }
 
 pub fn create_rust_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<OutputFile> {
     let parsed_flags = input.try_parse_flags()?;
-    let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
-    let Some(package) = find_unique_package(&filtered_parsed_flags) else {
+    let modified_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, codegen_mode)?;
+    let Some(package) = find_unique_package(&modified_parsed_flags) else {
         bail!("no parsed flags, or the parsed flags use different packages");
     };
-    generate_rust_code(package, filtered_parsed_flags.iter(), codegen_mode)
+    let package = package.to_string();
+    let _flag_ids = assign_flag_ids(&package, modified_parsed_flags.iter())?;
+    generate_rust_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
 }
 
 pub fn create_storage(caches: Vec<Input>, container: &str) -> Result<Vec<OutputFile>> {
@@ -276,73 +277,28 @@
     Ok(output)
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
-pub enum DumpFormat {
-    Text,
-    Verbose,
-    Protobuf,
-    Textproto,
-    Bool,
-}
-
 pub fn dump_parsed_flags(
     mut input: Vec<Input>,
     format: DumpFormat,
+    filters: &[&str],
     dedup: bool,
 ) -> Result<Vec<u8>> {
     let individually_parsed_flags: Result<Vec<ProtoParsedFlags>> =
         input.iter_mut().map(|i| i.try_parse_flags()).collect();
     let parsed_flags: ProtoParsedFlags =
         crate::protos::parsed_flags::merge(individually_parsed_flags?, dedup)?;
-
-    let mut output = Vec::new();
-    match format {
-        DumpFormat::Text => {
-            for parsed_flag in parsed_flags.parsed_flag.into_iter() {
-                let line = format!(
-                    "{} [{}]: {:?} + {:?}\n",
-                    parsed_flag.fully_qualified_name(),
-                    parsed_flag.container(),
-                    parsed_flag.permission(),
-                    parsed_flag.state()
-                );
-                output.extend_from_slice(line.as_bytes());
-            }
-        }
-        DumpFormat::Verbose => {
-            for parsed_flag in parsed_flags.parsed_flag.into_iter() {
-                let sources: Vec<_> =
-                    parsed_flag.trace.iter().map(|tracepoint| tracepoint.source()).collect();
-                let line = format!(
-                    "{} [{}]: {:?} + {:?} ({})\n",
-                    parsed_flag.fully_qualified_name(),
-                    parsed_flag.container(),
-                    parsed_flag.permission(),
-                    parsed_flag.state(),
-                    sources.join(", ")
-                );
-                output.extend_from_slice(line.as_bytes());
-            }
-        }
-        DumpFormat::Protobuf => {
-            parsed_flags.write_to_vec(&mut output)?;
-        }
-        DumpFormat::Textproto => {
-            let s = protobuf::text_format::print_to_string_pretty(&parsed_flags);
-            output.extend_from_slice(s.as_bytes());
-        }
-        DumpFormat::Bool => {
-            for parsed_flag in parsed_flags.parsed_flag.into_iter() {
-                let line = format!(
-                    "{}={:?}\n",
-                    parsed_flag.fully_qualified_name(),
-                    parsed_flag.state() == ProtoFlagState::ENABLED
-                );
-                output.extend_from_slice(line.as_bytes());
-            }
-        }
-    }
-    Ok(output)
+    let filters: Vec<Box<DumpPredicate>> = if filters.is_empty() {
+        vec![Box::new(|_| true)]
+    } else {
+        filters
+            .iter()
+            .map(|f| crate::dump::create_filter_predicate(f))
+            .collect::<Result<Vec<_>>>()?
+    };
+    crate::dump::dump_parsed_flags(
+        parsed_flags.parsed_flag.into_iter().filter(|flag| filters.iter().any(|p| p(flag))),
+        format,
+    )
 }
 
 fn find_unique_package(parsed_flags: &[ProtoParsedFlag]) -> Option<&str> {
@@ -365,16 +321,65 @@
     Some(container)
 }
 
-fn filter_parsed_flags(
+pub fn modify_parsed_flags_based_on_mode(
     parsed_flags: ProtoParsedFlags,
     codegen_mode: CodegenMode,
-) -> Vec<ProtoParsedFlag> {
-    match codegen_mode {
-        CodegenMode::Exported => {
-            parsed_flags.parsed_flag.into_iter().filter(|pf| pf.is_exported()).collect()
-        }
-        _ => parsed_flags.parsed_flag,
+) -> Result<Vec<ProtoParsedFlag>> {
+    fn exported_mode_flag_modifier(mut parsed_flag: ProtoParsedFlag) -> ProtoParsedFlag {
+        parsed_flag.set_state(ProtoFlagState::DISABLED);
+        parsed_flag.set_permission(ProtoFlagPermission::READ_WRITE);
+        parsed_flag.set_is_fixed_read_only(false);
+        parsed_flag
     }
+
+    fn force_read_only_mode_flag_modifier(mut parsed_flag: ProtoParsedFlag) -> ProtoParsedFlag {
+        parsed_flag.set_permission(ProtoFlagPermission::READ_ONLY);
+        parsed_flag
+    }
+
+    let modified_parsed_flags: Vec<_> = match codegen_mode {
+        CodegenMode::Exported => parsed_flags
+            .parsed_flag
+            .into_iter()
+            .filter(|pf| pf.is_exported())
+            .map(exported_mode_flag_modifier)
+            .collect(),
+        CodegenMode::ForceReadOnly => parsed_flags
+            .parsed_flag
+            .into_iter()
+            .filter(|pf| !pf.is_exported())
+            .map(force_read_only_mode_flag_modifier)
+            .collect(),
+        CodegenMode::Production | CodegenMode::Test => {
+            parsed_flags.parsed_flag.into_iter().collect()
+        }
+    };
+    if modified_parsed_flags.is_empty() {
+        bail!("{codegen_mode} library contains no {codegen_mode} flags");
+    }
+
+    Ok(modified_parsed_flags)
+}
+
+pub fn assign_flag_ids<'a, I>(package: &str, parsed_flags_iter: I) -> Result<HashMap<String, u16>>
+where
+    I: Iterator<Item = &'a ProtoParsedFlag> + Clone,
+{
+    assert!(parsed_flags_iter.clone().tuple_windows().all(|(a, b)| a.name() <= b.name()));
+    let mut flag_ids = HashMap::new();
+    for (id_to_assign, pf) in (0_u32..).zip(parsed_flags_iter) {
+        if package != pf.package() {
+            return Err(anyhow::anyhow!("encountered a flag not in current package"));
+        }
+
+        // put a cap on how many flags a package can contain to 65535
+        if id_to_assign > u16::MAX as u32 {
+            return Err(anyhow::anyhow!("the number of flags in a package cannot exceed 65535"));
+        }
+
+        flag_ids.insert(pf.name().to_string(), id_to_assign as u16);
+    }
+    Ok(flag_ids)
 }
 
 #[cfg(test)]
@@ -408,9 +413,9 @@
         assert_eq!(ProtoFlagState::ENABLED, enabled_ro.trace[2].state());
         assert_eq!(ProtoFlagPermission::READ_ONLY, enabled_ro.trace[2].permission());
 
-        assert_eq!(8, parsed_flags.parsed_flag.len());
+        assert_eq!(9, parsed_flags.parsed_flag.len());
         for pf in parsed_flags.parsed_flag.iter() {
-            if pf.name() == "enabled_fixed_ro" {
+            if pf.name().starts_with("enabled_fixed_ro") {
                 continue;
             }
             let first = pf.trace.first().unwrap();
@@ -619,62 +624,33 @@
     }
 
     #[test]
-    fn test_dump_text_format() {
+    fn test_dump() {
         let input = parse_test_flags_as_input();
-        let bytes = dump_parsed_flags(vec![input], DumpFormat::Text, false).unwrap();
-        let text = std::str::from_utf8(&bytes).unwrap();
-        assert!(
-            text.contains("com.android.aconfig.test.disabled_ro [system]: READ_ONLY + DISABLED")
-        );
-    }
-
-    #[test]
-    fn test_dump_protobuf_format() {
-        let expected = protobuf::text_format::parse_from_str::<ProtoParsedFlags>(
-            crate::test::TEST_FLAGS_TEXTPROTO,
+        let bytes = dump_parsed_flags(
+            vec![input],
+            DumpFormat::Custom("{fully_qualified_name}".to_string()),
+            &[],
+            false,
         )
-        .unwrap()
-        .write_to_bytes()
         .unwrap();
-
-        let input = parse_test_flags_as_input();
-        let actual = dump_parsed_flags(vec![input], DumpFormat::Protobuf, false).unwrap();
-
-        assert_eq!(expected, actual);
-    }
-
-    #[test]
-    fn test_dump_textproto_format() {
-        let input = parse_test_flags_as_input();
-        let bytes = dump_parsed_flags(vec![input], DumpFormat::Textproto, false).unwrap();
         let text = std::str::from_utf8(&bytes).unwrap();
-        assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
+        assert!(text.contains("com.android.aconfig.test.disabled_ro"));
     }
 
     #[test]
     fn test_dump_textproto_format_dedup() {
         let input = parse_test_flags_as_input();
         let input2 = parse_test_flags_as_input();
-        let bytes = dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, true).unwrap();
+        let bytes =
+            dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, &[], true).unwrap();
         let text = std::str::from_utf8(&bytes).unwrap();
-        assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
-    }
-
-    #[test]
-    fn test_filter_parsed_flags() {
-        let mut input = parse_test_flags_as_input();
-        let parsed_flags = input.try_parse_flags().unwrap();
-
-        let filtered_parsed_flags =
-            filter_parsed_flags(parsed_flags.clone(), CodegenMode::Exported);
-        assert_eq!(2, filtered_parsed_flags.len());
-
-        let filtered_parsed_flags =
-            filter_parsed_flags(parsed_flags.clone(), CodegenMode::Production);
-        assert_eq!(8, filtered_parsed_flags.len());
-
-        let filtered_parsed_flags = filter_parsed_flags(parsed_flags.clone(), CodegenMode::Test);
-        assert_eq!(8, filtered_parsed_flags.len());
+        assert_eq!(
+            None,
+            crate::test::first_significant_code_diff(
+                crate::test::TEST_FLAGS_TEXTPROTO.trim(),
+                text.trim()
+            )
+        );
     }
 
     fn parse_test_flags_as_input() -> Input {
@@ -684,4 +660,76 @@
         let reader = Box::new(cursor);
         Input { source: "test.data".to_string(), reader }
     }
+
+    #[test]
+    fn test_modify_parsed_flags_based_on_mode_prod() {
+        let parsed_flags = crate::test::parse_test_flags();
+        let p_parsed_flags =
+            modify_parsed_flags_based_on_mode(parsed_flags.clone(), CodegenMode::Production)
+                .unwrap();
+        assert_eq!(parsed_flags.parsed_flag.len(), p_parsed_flags.len());
+        for (i, item) in p_parsed_flags.iter().enumerate() {
+            assert!(parsed_flags.parsed_flag[i].eq(item));
+        }
+    }
+
+    #[test]
+    fn test_modify_parsed_flags_based_on_mode_exported() {
+        let parsed_flags = crate::test::parse_test_flags();
+        let p_parsed_flags =
+            modify_parsed_flags_based_on_mode(parsed_flags, CodegenMode::Exported).unwrap();
+        assert_eq!(3, p_parsed_flags.len());
+        for flag in p_parsed_flags.iter() {
+            assert_eq!(ProtoFlagState::DISABLED, flag.state());
+            assert_eq!(ProtoFlagPermission::READ_WRITE, flag.permission());
+            assert!(!flag.is_fixed_read_only());
+            assert!(flag.is_exported());
+        }
+
+        let mut parsed_flags = crate::test::parse_test_flags();
+        parsed_flags.parsed_flag.retain(|pf| !pf.is_exported());
+        let error =
+            modify_parsed_flags_based_on_mode(parsed_flags, CodegenMode::Exported).unwrap_err();
+        assert_eq!("exported library contains no exported flags", format!("{:?}", error));
+    }
+
+    #[test]
+    fn test_assign_flag_ids() {
+        let parsed_flags = crate::test::parse_test_flags();
+        let package = find_unique_package(&parsed_flags.parsed_flag).unwrap().to_string();
+        let flag_ids = assign_flag_ids(&package, parsed_flags.parsed_flag.iter()).unwrap();
+        let expected_flag_ids = HashMap::from([
+            (String::from("disabled_ro"), 0_u16),
+            (String::from("disabled_rw"), 1_u16),
+            (String::from("disabled_rw_exported"), 2_u16),
+            (String::from("disabled_rw_in_other_namespace"), 3_u16),
+            (String::from("enabled_fixed_ro"), 4_u16),
+            (String::from("enabled_fixed_ro_exported"), 5_u16),
+            (String::from("enabled_ro"), 6_u16),
+            (String::from("enabled_ro_exported"), 7_u16),
+            (String::from("enabled_rw"), 8_u16),
+        ]);
+        assert_eq!(flag_ids, expected_flag_ids);
+    }
+
+    #[test]
+    fn test_modify_parsed_flags_based_on_mode_force_read_only() {
+        let parsed_flags = crate::test::parse_test_flags();
+        let p_parsed_flags =
+            modify_parsed_flags_based_on_mode(parsed_flags.clone(), CodegenMode::ForceReadOnly)
+                .unwrap();
+        assert_eq!(6, p_parsed_flags.len());
+        for pf in p_parsed_flags {
+            assert_eq!(ProtoFlagPermission::READ_ONLY, pf.permission());
+        }
+
+        let mut parsed_flags = crate::test::parse_test_flags();
+        parsed_flags.parsed_flag.retain_mut(|pf| pf.is_exported());
+        let error = modify_parsed_flags_based_on_mode(parsed_flags, CodegenMode::ForceReadOnly)
+            .unwrap_err();
+        assert_eq!(
+            "force-read-only library contains no force-read-only flags",
+            format!("{:?}", error)
+        );
+    }
 }
diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs
new file mode 100644
index 0000000..37368ee
--- /dev/null
+++ b/tools/aconfig/src/dump.rs
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+use crate::protos::{
+    ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoTracepoint,
+};
+use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
+use anyhow::{anyhow, bail, Context, Result};
+use protobuf::Message;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DumpFormat {
+    Protobuf,
+    Textproto,
+    Custom(String),
+}
+
+impl TryFrom<&str> for DumpFormat {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
+        match value {
+            // protobuf formats
+            "protobuf" => Ok(Self::Protobuf),
+            "textproto" => Ok(Self::Textproto),
+            // custom format
+            _ => Ok(Self::Custom(value.to_owned())),
+        }
+    }
+}
+
+pub fn dump_parsed_flags<I>(parsed_flags_iter: I, format: DumpFormat) -> Result<Vec<u8>>
+where
+    I: Iterator<Item = ProtoParsedFlag>,
+{
+    let mut output = Vec::new();
+    match format {
+        DumpFormat::Protobuf => {
+            let parsed_flags =
+                ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
+            parsed_flags.write_to_vec(&mut output)?;
+        }
+        DumpFormat::Textproto => {
+            let parsed_flags =
+                ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
+            let s = protobuf::text_format::print_to_string_pretty(&parsed_flags);
+            output.extend_from_slice(s.as_bytes());
+        }
+        DumpFormat::Custom(format) => {
+            for flag in parsed_flags_iter {
+                dump_custom_format(&flag, &format, &mut output);
+            }
+        }
+    }
+    Ok(output)
+}
+
+fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec<u8>) {
+    fn format_trace(trace: &[ProtoTracepoint]) -> String {
+        trace
+            .iter()
+            .map(|tracepoint| {
+                format!(
+                    "{}: {:?} + {:?}",
+                    tracepoint.source(),
+                    tracepoint.permission(),
+                    tracepoint.state()
+                )
+            })
+            .collect::<Vec<_>>()
+            .join(", ")
+    }
+
+    fn format_trace_paths(trace: &[ProtoTracepoint]) -> String {
+        trace.iter().map(|tracepoint| tracepoint.source()).collect::<Vec<_>>().join(", ")
+    }
+
+    fn format_metadata(metadata: &ProtoFlagMetadata) -> String {
+        format!("{:?}", metadata.purpose())
+    }
+
+    let mut str = format
+        // ProtoParsedFlag fields
+        .replace("{package}", flag.package())
+        .replace("{name}", flag.name())
+        .replace("{namespace}", flag.namespace())
+        .replace("{description}", flag.description())
+        .replace("{bug}", &flag.bug.join(", "))
+        .replace("{state}", &format!("{:?}", flag.state()))
+        .replace("{state:bool}", &format!("{}", flag.state() == ProtoFlagState::ENABLED))
+        .replace("{permission}", &format!("{:?}", flag.permission()))
+        .replace("{trace}", &format_trace(&flag.trace))
+        .replace("{trace:paths}", &format_trace_paths(&flag.trace))
+        .replace("{is_fixed_read_only}", &format!("{}", flag.is_fixed_read_only()))
+        .replace("{is_exported}", &format!("{}", flag.is_exported()))
+        .replace("{container}", flag.container())
+        .replace("{metadata}", &format_metadata(&flag.metadata))
+        // ParsedFlagExt functions
+        .replace("{fully_qualified_name}", &flag.fully_qualified_name());
+    str.push('\n');
+    output.extend_from_slice(str.as_bytes());
+}
+
+pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool;
+
+pub fn create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>> {
+    let predicates = filter
+        .split('+')
+        .map(|sub_filter| create_filter_predicate_single(sub_filter))
+        .collect::<Result<Vec<_>>>()?;
+    Ok(Box::new(move |flag| predicates.iter().all(|p| p(flag))))
+}
+
+fn create_filter_predicate_single(filter: &str) -> Result<Box<DumpPredicate>> {
+    fn enum_from_str<T>(expected: &[T], s: &str) -> Result<T>
+    where
+        T: std::fmt::Debug + Copy,
+    {
+        for candidate in expected.iter() {
+            if s == format!("{:?}", candidate) {
+                return Ok(*candidate);
+            }
+        }
+        let expected =
+            expected.iter().map(|state| format!("{:?}", state)).collect::<Vec<_>>().join(", ");
+        bail!("\"{s}\": not a valid flag state, expected one of {expected}");
+    }
+
+    let error_msg = format!("\"{filter}\": filter syntax error");
+    let (what, arg) = filter.split_once(':').ok_or_else(|| anyhow!(error_msg.clone()))?;
+    match what {
+        "package" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.package() == expected))
+        }
+        "name" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.name() == expected))
+        }
+        "namespace" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.namespace() == expected))
+        }
+        // description: not supported yet
+        "bug" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.bug.join(", ") == expected))
+        }
+        "state" => {
+            let expected = enum_from_str(&[ProtoFlagState::ENABLED, ProtoFlagState::DISABLED], arg)
+                .context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.state() == expected))
+        }
+        "permission" => {
+            let expected = enum_from_str(
+                &[ProtoFlagPermission::READ_ONLY, ProtoFlagPermission::READ_WRITE],
+                arg,
+            )
+            .context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.permission() == expected))
+        }
+        // trace: not supported yet
+        "is_fixed_read_only" => {
+            let expected: bool = arg.parse().context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_fixed_read_only() == expected))
+        }
+        "is_exported" => {
+            let expected: bool = arg.parse().context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_exported() == expected))
+        }
+        "container" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.container() == expected))
+        }
+        // metadata: not supported yet
+        "fully_qualified_name" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.fully_qualified_name() == expected))
+        }
+        _ => Err(anyhow!(error_msg)),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::protos::ProtoParsedFlags;
+    use crate::test::parse_test_flags;
+    use protobuf::Message;
+
+    fn parse_enabled_ro_flag() -> ProtoParsedFlag {
+        parse_test_flags().parsed_flag.into_iter().find(|pf| pf.name() == "enabled_ro").unwrap()
+    }
+
+    #[test]
+    fn test_dumpformat_from_str() {
+        // supported format types
+        assert_eq!(DumpFormat::try_from("protobuf").unwrap(), DumpFormat::Protobuf);
+        assert_eq!(DumpFormat::try_from("textproto").unwrap(), DumpFormat::Textproto);
+        assert_eq!(
+            DumpFormat::try_from("foobar").unwrap(),
+            DumpFormat::Custom("foobar".to_owned())
+        );
+    }
+
+    #[test]
+    fn test_dump_parsed_flags_protobuf_format() {
+        let expected = protobuf::text_format::parse_from_str::<ProtoParsedFlags>(
+            crate::test::TEST_FLAGS_TEXTPROTO,
+        )
+        .unwrap()
+        .write_to_bytes()
+        .unwrap();
+        let parsed_flags = parse_test_flags();
+        let actual =
+            dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Protobuf).unwrap();
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_dump_parsed_flags_textproto_format() {
+        let parsed_flags = parse_test_flags();
+        let bytes =
+            dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Textproto).unwrap();
+        let text = std::str::from_utf8(&bytes).unwrap();
+        assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
+    }
+
+    #[test]
+    fn test_dump_parsed_flags_custom_format() {
+        macro_rules! assert_dump_parsed_flags_custom_format_contains {
+            ($format:expr, $expected:expr) => {
+                let parsed_flags = parse_test_flags();
+                let bytes = dump_parsed_flags(
+                    parsed_flags.parsed_flag.into_iter(),
+                    $format.try_into().unwrap(),
+                )
+                .unwrap();
+                let text = std::str::from_utf8(&bytes).unwrap();
+                assert!(text.contains($expected));
+            };
+        }
+
+        // custom format
+        assert_dump_parsed_flags_custom_format_contains!(
+            "{fully_qualified_name}={permission} + {state}",
+            "com.android.aconfig.test.enabled_ro=READ_ONLY + ENABLED"
+        );
+    }
+
+    #[test]
+    fn test_dump_custom_format() {
+        macro_rules! assert_custom_format {
+            ($format:expr, $expected:expr) => {
+                let flag = parse_enabled_ro_flag();
+                let mut bytes = vec![];
+                dump_custom_format(&flag, $format, &mut bytes);
+                let text = std::str::from_utf8(&bytes).unwrap();
+                assert_eq!(text, $expected);
+            };
+        }
+
+        assert_custom_format!("{package}", "com.android.aconfig.test\n");
+        assert_custom_format!("{name}", "enabled_ro\n");
+        assert_custom_format!("{namespace}", "aconfig_test\n");
+        assert_custom_format!("{description}", "This flag is ENABLED + READ_ONLY\n");
+        assert_custom_format!("{bug}", "abc\n");
+        assert_custom_format!("{state}", "ENABLED\n");
+        assert_custom_format!("{state:bool}", "true\n");
+        assert_custom_format!("{permission}", "READ_ONLY\n");
+        assert_custom_format!("{trace}", "tests/test.aconfig: READ_WRITE + DISABLED, tests/first.values: READ_WRITE + DISABLED, tests/second.values: READ_ONLY + ENABLED\n");
+        assert_custom_format!(
+            "{trace:paths}",
+            "tests/test.aconfig, tests/first.values, tests/second.values\n"
+        );
+        assert_custom_format!("{is_fixed_read_only}", "false\n");
+        assert_custom_format!("{is_exported}", "false\n");
+        assert_custom_format!("{container}", "system\n");
+        assert_custom_format!("{metadata}", "PURPOSE_BUGFIX\n");
+
+        assert_custom_format!("name={name}|state={state}", "name=enabled_ro|state=ENABLED\n");
+        assert_custom_format!("{state}{state}{state}", "ENABLEDENABLEDENABLED\n");
+    }
+
+    #[test]
+    fn test_create_filter_predicate() {
+        macro_rules! assert_create_filter_predicate {
+            ($filter:expr, $expected:expr) => {
+                let parsed_flags = parse_test_flags();
+                let predicate = create_filter_predicate($filter).unwrap();
+                let mut filtered_flags: Vec<String> = parsed_flags
+                    .parsed_flag
+                    .into_iter()
+                    .filter(predicate)
+                    .map(|flag| flag.fully_qualified_name())
+                    .collect();
+                filtered_flags.sort();
+                assert_eq!(&filtered_flags, $expected);
+            };
+        }
+
+        assert_create_filter_predicate!(
+            "package:com.android.aconfig.test",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.disabled_rw",
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.disabled_rw_in_other_namespace",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "name:disabled_rw",
+            &["com.android.aconfig.test.disabled_rw"]
+        );
+        assert_create_filter_predicate!(
+            "namespace:other_namespace",
+            &["com.android.aconfig.test.disabled_rw_in_other_namespace"]
+        );
+        // description: not supported yet
+        assert_create_filter_predicate!("bug:123", &["com.android.aconfig.test.disabled_ro",]);
+        assert_create_filter_predicate!(
+            "state:ENABLED",
+            &[
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "permission:READ_ONLY",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+        // trace: not supported yet
+        assert_create_filter_predicate!(
+            "is_fixed_read_only:true",
+            &[
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "is_exported:true",
+            &[
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "container:system",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.disabled_rw",
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.disabled_rw_in_other_namespace",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        // metadata: not supported yet
+
+        // synthesized fields
+        assert_create_filter_predicate!(
+            "fully_qualified_name:com.android.aconfig.test.disabled_rw",
+            &["com.android.aconfig.test.disabled_rw"]
+        );
+
+        // multiple sub filters
+        assert_create_filter_predicate!(
+            "permission:READ_ONLY+state:ENABLED",
+            &[
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_fixed_ro_exported",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+    }
+}
diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs
index 63a50c8..6c4e241 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -26,13 +26,22 @@
 
 mod codegen;
 mod commands;
+mod dump;
 mod protos;
 mod storage;
 
+use codegen::CodegenMode;
+use dump::DumpFormat;
+
 #[cfg(test)]
 mod test;
 
-use commands::{CodegenMode, DumpFormat, Input, OutputFile};
+use commands::{Input, OutputFile};
+
+const HELP_DUMP_FILTER: &str = r#"
+Limit which flags to output. If multiple --filter arguments are provided, the output will be
+limited to flags that match any of the filters.
+"#;
 
 fn cli() -> Command {
     Command::new("aconfig")
@@ -61,7 +70,7 @@
                 .arg(
                     Arg::new("mode")
                         .long("mode")
-                        .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+                        .value_parser(EnumValueParser::<CodegenMode>::new())
                         .default_value("production"),
                 ),
         )
@@ -72,7 +81,7 @@
                 .arg(
                     Arg::new("mode")
                         .long("mode")
-                        .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+                        .value_parser(EnumValueParser::<CodegenMode>::new())
                         .default_value("production"),
                 ),
         )
@@ -83,7 +92,7 @@
                 .arg(
                     Arg::new("mode")
                         .long("mode")
-                        .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+                        .value_parser(EnumValueParser::<CodegenMode>::new())
                         .default_value("production"),
                 ),
         )
@@ -98,13 +107,22 @@
                 .arg(Arg::new("out").long("out").default_value("-")),
         )
         .subcommand(
-            Command::new("dump")
+            Command::new("dump-cache")
+                .alias("dump")
                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append))
                 .arg(
                     Arg::new("format")
                         .long("format")
-                        .value_parser(EnumValueParser::<commands::DumpFormat>::new())
-                        .default_value("text"),
+                        .value_parser(|s: &str| DumpFormat::try_from(s))
+                        .default_value(
+                            "{fully_qualified_name} [{container}]: {permission} + {state}",
+                        ),
+                )
+                .arg(
+                    Arg::new("filter")
+                        .long("filter")
+                        .action(ArgAction::Append)
+                        .help(HELP_DUMP_FILTER.trim()),
                 )
                 .arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue))
                 .arg(Arg::new("out").long("out").default_value("-")),
@@ -245,12 +263,17 @@
             let path = get_required_arg::<String>(sub_matches, "out")?;
             write_output_to_file_or_stdout(path, &output)?;
         }
-        Some(("dump", sub_matches)) => {
+        Some(("dump-cache", sub_matches)) => {
             let input = open_zero_or_more_files(sub_matches, "cache")?;
             let format = get_required_arg::<DumpFormat>(sub_matches, "format")
                 .context("failed to dump previously parsed flags")?;
+            let filters = sub_matches
+                .get_many::<String>("filter")
+                .unwrap_or_default()
+                .map(String::as_ref)
+                .collect::<Vec<_>>();
             let dedup = get_required_arg::<bool>(sub_matches, "dedup")?;
-            let output = commands::dump_parsed_flags(input, *format, *dedup)?;
+            let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?;
             let path = get_required_arg::<String>(sub_matches, "out")?;
             write_output_to_file_or_stdout(path, &output)?;
         }
diff --git a/tools/aconfig/src/storage/flag_table.rs b/tools/aconfig/src/storage/flag_table.rs
new file mode 100644
index 0000000..595217e
--- /dev/null
+++ b/tools/aconfig/src/storage/flag_table.rs
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+use crate::commands::assign_flag_ids;
+use crate::storage::{self, FlagPackage};
+use anyhow::{anyhow, Result};
+
+#[derive(PartialEq, Debug)]
+pub struct FlagTableHeader {
+    pub version: u32,
+    pub container: String,
+    pub file_size: u32,
+    pub num_flags: u32,
+    pub bucket_offset: u32,
+    pub node_offset: u32,
+}
+
+impl FlagTableHeader {
+    fn new(container: &str, num_flags: u32) -> Self {
+        Self {
+            version: storage::FILE_VERSION,
+            container: String::from(container),
+            file_size: 0,
+            num_flags,
+            bucket_offset: 0,
+            node_offset: 0,
+        }
+    }
+
+    fn as_bytes(&self) -> Vec<u8> {
+        let mut result = Vec::new();
+        result.extend_from_slice(&self.version.to_le_bytes());
+        let container_bytes = self.container.as_bytes();
+        result.extend_from_slice(&(container_bytes.len() as u32).to_le_bytes());
+        result.extend_from_slice(container_bytes);
+        result.extend_from_slice(&self.file_size.to_le_bytes());
+        result.extend_from_slice(&self.num_flags.to_le_bytes());
+        result.extend_from_slice(&self.bucket_offset.to_le_bytes());
+        result.extend_from_slice(&self.node_offset.to_le_bytes());
+        result
+    }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub struct FlagTableNode {
+    pub package_id: u32,
+    pub flag_name: String,
+    pub flag_type: u16,
+    pub flag_id: u16,
+    pub next_offset: Option<u32>,
+    pub bucket_index: u32,
+}
+
+impl FlagTableNode {
+    fn new(
+        package_id: u32,
+        flag_name: &str,
+        flag_type: u16,
+        flag_id: u16,
+        num_buckets: u32,
+    ) -> Self {
+        let full_flag_name = package_id.to_string() + "/" + flag_name;
+        let bucket_index = storage::get_bucket_index(&full_flag_name, num_buckets);
+        Self {
+            package_id,
+            flag_name: flag_name.to_string(),
+            flag_type,
+            flag_id,
+            next_offset: None,
+            bucket_index,
+        }
+    }
+
+    fn as_bytes(&self) -> Vec<u8> {
+        let mut result = Vec::new();
+        result.extend_from_slice(&self.package_id.to_le_bytes());
+        let name_bytes = self.flag_name.as_bytes();
+        result.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
+        result.extend_from_slice(name_bytes);
+        result.extend_from_slice(&self.flag_type.to_le_bytes());
+        result.extend_from_slice(&self.flag_id.to_le_bytes());
+        result.extend_from_slice(&self.next_offset.unwrap_or(0).to_le_bytes());
+        result
+    }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct FlagTable {
+    pub header: FlagTableHeader,
+    pub buckets: Vec<Option<u32>>,
+    pub nodes: Vec<FlagTableNode>,
+}
+
+impl FlagTable {
+    fn create_nodes(package: &FlagPackage, num_buckets: u32) -> Result<Vec<FlagTableNode>> {
+        let flag_ids =
+            assign_flag_ids(package.package_name, package.boolean_flags.iter().copied())?;
+        package
+            .boolean_flags
+            .iter()
+            .map(|&pf| {
+                let fid = flag_ids
+                    .get(pf.name())
+                    .ok_or(anyhow!(format!("missing flag id for {}", pf.name())))?;
+                // all flags are boolean value at the moment, thus using the last bit. When more
+                // flag value types are supported, flag value type information should come from the
+                // parsed flag, and we will set the flag_type bit mask properly.
+                let flag_type = 1;
+                Ok(FlagTableNode::new(package.package_id, pf.name(), flag_type, *fid, num_buckets))
+            })
+            .collect::<Result<Vec<_>>>()
+    }
+
+    pub fn new(container: &str, packages: &[FlagPackage]) -> Result<Self> {
+        // create table
+        let num_flags = packages.iter().map(|pkg| pkg.boolean_flags.len() as u32).sum();
+        let num_buckets = storage::get_table_size(num_flags)?;
+
+        let mut table = Self {
+            header: FlagTableHeader::new(container, num_flags),
+            buckets: vec![None; num_buckets as usize],
+            nodes: packages
+                .iter()
+                .map(|pkg| FlagTable::create_nodes(pkg, num_buckets))
+                .collect::<Result<Vec<_>>>()?
+                .concat(),
+        };
+
+        // initialize all header fields
+        table.header.bucket_offset = table.header.as_bytes().len() as u32;
+        table.header.node_offset = table.header.bucket_offset + num_buckets * 4;
+        table.header.file_size = table.header.node_offset
+            + table.nodes.iter().map(|x| x.as_bytes().len()).sum::<usize>() as u32;
+
+        // sort nodes by bucket index for efficiency
+        table.nodes.sort_by(|a, b| a.bucket_index.cmp(&b.bucket_index));
+
+        // fill all node offset
+        let mut offset = table.header.node_offset;
+        for i in 0..table.nodes.len() {
+            let node_bucket_idx = table.nodes[i].bucket_index;
+            let next_node_bucket_idx = if i + 1 < table.nodes.len() {
+                Some(table.nodes[i + 1].bucket_index)
+            } else {
+                None
+            };
+
+            if table.buckets[node_bucket_idx as usize].is_none() {
+                table.buckets[node_bucket_idx as usize] = Some(offset);
+            }
+            offset += table.nodes[i].as_bytes().len() as u32;
+
+            if let Some(index) = next_node_bucket_idx {
+                if index == node_bucket_idx {
+                    table.nodes[i].next_offset = Some(offset);
+                }
+            }
+        }
+
+        Ok(table)
+    }
+
+    pub fn as_bytes(&self) -> Vec<u8> {
+        [
+            self.header.as_bytes(),
+            self.buckets.iter().map(|v| v.unwrap_or(0).to_le_bytes()).collect::<Vec<_>>().concat(),
+            self.nodes.iter().map(|v| v.as_bytes()).collect::<Vec<_>>().concat(),
+        ]
+        .concat()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::storage::{
+        group_flags_by_package, tests::parse_all_test_flags, tests::read_str_from_bytes,
+        tests::read_u16_from_bytes, tests::read_u32_from_bytes,
+    };
+
+    impl FlagTableHeader {
+        // test only method to deserialize back into the header struct
+        fn from_bytes(bytes: &[u8]) -> Result<Self> {
+            let mut head = 0;
+            Ok(Self {
+                version: read_u32_from_bytes(bytes, &mut head)?,
+                container: read_str_from_bytes(bytes, &mut head)?,
+                file_size: read_u32_from_bytes(bytes, &mut head)?,
+                num_flags: read_u32_from_bytes(bytes, &mut head)?,
+                bucket_offset: read_u32_from_bytes(bytes, &mut head)?,
+                node_offset: read_u32_from_bytes(bytes, &mut head)?,
+            })
+        }
+    }
+
+    impl FlagTableNode {
+        // test only method to deserialize back into the node struct
+        fn from_bytes(bytes: &[u8], num_buckets: u32) -> Result<Self> {
+            let mut head = 0;
+            let mut node = Self {
+                package_id: read_u32_from_bytes(bytes, &mut head)?,
+                flag_name: read_str_from_bytes(bytes, &mut head)?,
+                flag_type: read_u16_from_bytes(bytes, &mut head)?,
+                flag_id: read_u16_from_bytes(bytes, &mut head)?,
+                next_offset: match read_u32_from_bytes(bytes, &mut head)? {
+                    0 => None,
+                    val => Some(val),
+                },
+                bucket_index: 0,
+            };
+            let full_flag_name = node.package_id.to_string() + "/" + &node.flag_name;
+            node.bucket_index = storage::get_bucket_index(&full_flag_name, num_buckets);
+            Ok(node)
+        }
+
+        // create test baseline, syntactic sugar
+        fn new_expected(
+            package_id: u32,
+            flag_name: &str,
+            flag_type: u16,
+            flag_id: u16,
+            next_offset: Option<u32>,
+            bucket_index: u32,
+        ) -> Self {
+            Self {
+                package_id,
+                flag_name: flag_name.to_string(),
+                flag_type,
+                flag_id,
+                next_offset,
+                bucket_index,
+            }
+        }
+    }
+
+    impl FlagTable {
+        // test only method to deserialize back into the table struct
+        fn from_bytes(bytes: &[u8]) -> Result<Self> {
+            let header = FlagTableHeader::from_bytes(bytes)?;
+            let num_flags = header.num_flags;
+            let num_buckets = storage::get_table_size(num_flags)?;
+            let mut head = header.as_bytes().len();
+            let buckets = (0..num_buckets)
+                .map(|_| match read_u32_from_bytes(bytes, &mut head).unwrap() {
+                    0 => None,
+                    val => Some(val),
+                })
+                .collect();
+            let nodes = (0..num_flags)
+                .map(|_| {
+                    let node = FlagTableNode::from_bytes(&bytes[head..], num_buckets).unwrap();
+                    head += node.as_bytes().len();
+                    node
+                })
+                .collect();
+
+            let table = Self { header, buckets, nodes };
+            Ok(table)
+        }
+    }
+
+    pub fn create_test_flag_table() -> Result<FlagTable> {
+        let caches = parse_all_test_flags();
+        let packages = group_flags_by_package(caches.iter());
+        FlagTable::new("system", &packages)
+    }
+
+    #[test]
+    // this test point locks down the table creation and each field
+    fn test_table_contents() {
+        let flag_table = create_test_flag_table();
+        assert!(flag_table.is_ok());
+
+        let header: &FlagTableHeader = &flag_table.as_ref().unwrap().header;
+        let expected_header = FlagTableHeader {
+            version: storage::FILE_VERSION,
+            container: String::from("system"),
+            file_size: 320,
+            num_flags: 8,
+            bucket_offset: 30,
+            node_offset: 98,
+        };
+        assert_eq!(header, &expected_header);
+
+        println!("{:?}", &flag_table.as_ref().unwrap().nodes);
+
+        let buckets: &Vec<Option<u32>> = &flag_table.as_ref().unwrap().buckets;
+        let expected_bucket: Vec<Option<u32>> = vec![
+            Some(98),
+            Some(124),
+            None,
+            None,
+            None,
+            Some(177),
+            None,
+            Some(203),
+            None,
+            Some(261),
+            None,
+            None,
+            None,
+            None,
+            None,
+            Some(293),
+            None,
+        ];
+        assert_eq!(buckets, &expected_bucket);
+
+        let nodes: &Vec<FlagTableNode> = &flag_table.as_ref().unwrap().nodes;
+        assert_eq!(nodes.len(), 8);
+
+        assert_eq!(nodes[0], FlagTableNode::new_expected(0, "enabled_ro", 1, 1, None, 0));
+        assert_eq!(nodes[1], FlagTableNode::new_expected(0, "enabled_rw", 1, 2, Some(150), 1));
+        assert_eq!(nodes[2], FlagTableNode::new_expected(1, "disabled_ro", 1, 0, None, 1));
+        assert_eq!(nodes[3], FlagTableNode::new_expected(2, "enabled_ro", 1, 1, None, 5));
+        assert_eq!(
+            nodes[4],
+            FlagTableNode::new_expected(1, "enabled_fixed_ro", 1, 1, Some(235), 7)
+        );
+        assert_eq!(nodes[5], FlagTableNode::new_expected(1, "enabled_ro", 1, 2, None, 7));
+        assert_eq!(nodes[6], FlagTableNode::new_expected(2, "enabled_fixed_ro", 1, 0, None, 9));
+        assert_eq!(nodes[7], FlagTableNode::new_expected(0, "disabled_rw", 1, 0, None, 15));
+    }
+
+    #[test]
+    // this test point locks down the table serialization
+    fn test_serialization() {
+        let flag_table = create_test_flag_table();
+        assert!(flag_table.is_ok());
+        let flag_table = flag_table.unwrap();
+
+        let header: &FlagTableHeader = &flag_table.header;
+        let reinterpreted_header = FlagTableHeader::from_bytes(&header.as_bytes());
+        assert!(reinterpreted_header.is_ok());
+        assert_eq!(header, &reinterpreted_header.unwrap());
+
+        let nodes: &Vec<FlagTableNode> = &flag_table.nodes;
+        let num_buckets = storage::get_table_size(header.num_flags).unwrap();
+        for node in nodes.iter() {
+            let reinterpreted_node = FlagTableNode::from_bytes(&node.as_bytes(), num_buckets);
+            assert!(reinterpreted_node.is_ok());
+            assert_eq!(node, &reinterpreted_node.unwrap());
+        }
+
+        let reinterpreted_table = FlagTable::from_bytes(&flag_table.as_bytes());
+        assert!(reinterpreted_table.is_ok());
+        assert_eq!(&flag_table, &reinterpreted_table.unwrap());
+    }
+}
diff --git a/tools/aconfig/src/storage/mod.rs b/tools/aconfig/src/storage/mod.rs
index 90e05f5..a28fccd 100644
--- a/tools/aconfig/src/storage/mod.rs
+++ b/tools/aconfig/src/storage/mod.rs
@@ -14,17 +14,50 @@
  * limitations under the License.
  */
 
-use anyhow::Result;
-use std::collections::{HashMap, HashSet};
+pub mod flag_table;
+pub mod package_table;
+
+use anyhow::{anyhow, Result};
+use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
+use std::hash::{Hash, Hasher};
+use std::path::PathBuf;
 
 use crate::commands::OutputFile;
 use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
+use crate::storage::{flag_table::FlagTable, package_table::PackageTable};
+
+pub const FILE_VERSION: u32 = 1;
+
+pub const HASH_PRIMES: [u32; 29] = [
+    7, 17, 29, 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241,
+    786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
+    402653189, 805306457, 1610612741,
+];
+
+/// Get the right hash table size given number of entries in the table. Use a
+/// load factor of 0.5 for performance.
+pub fn get_table_size(entries: u32) -> Result<u32> {
+    HASH_PRIMES
+        .iter()
+        .find(|&&num| num >= 2 * entries)
+        .copied()
+        .ok_or(anyhow!("Number of packages is too large"))
+}
+
+/// Get the corresponding bucket index given the key and number of buckets
+pub fn get_bucket_index<T: Hash>(val: &T, num_buckets: u32) -> u32 {
+    let mut s = DefaultHasher::new();
+    val.hash(&mut s);
+    (s.finish() % num_buckets as u64) as u32
+}
 
 pub struct FlagPackage<'a> {
     pub package_name: &'a str,
     pub package_id: u32,
     pub flag_names: HashSet<&'a str>,
     pub boolean_flags: Vec<&'a ProtoParsedFlag>,
+    // offset of the first boolean flag in this flag package with respect to the start of
+    // boolean flag value array in the flag value file
     pub boolean_offset: u32,
 }
 
@@ -52,7 +85,7 @@
 {
     // group flags by package
     let mut packages: Vec<FlagPackage<'a>> = Vec::new();
-    let mut package_index: HashMap<&'a str, usize> = HashMap::new();
+    let mut package_index: HashMap<&str, usize> = HashMap::new();
     for parsed_flags in parsed_flags_vec_iter {
         for parsed_flag in parsed_flags.parsed_flag.iter() {
             let index = *(package_index.entry(parsed_flag.package()).or_insert(packages.len()));
@@ -64,26 +97,38 @@
     }
 
     // calculate package flag value start offset, in flag value file, each boolean
-    // is stored as two bytes, the first byte will be the flag value. the second
-    // byte is flag info byte, which is a bitmask to indicate the status of a flag
+    // is stored as a single byte
     let mut boolean_offset = 0;
     for p in packages.iter_mut() {
         p.boolean_offset = boolean_offset;
-        boolean_offset += 2 * p.boolean_flags.len() as u32;
+        boolean_offset += p.boolean_flags.len() as u32;
     }
 
     packages
 }
 
 pub fn generate_storage_files<'a, I>(
-    _containser: &str,
+    container: &str,
     parsed_flags_vec_iter: I,
 ) -> Result<Vec<OutputFile>>
 where
     I: Iterator<Item = &'a ProtoParsedFlags>,
 {
-    let _packages = group_flags_by_package(parsed_flags_vec_iter);
-    Ok(vec![])
+    let packages = group_flags_by_package(parsed_flags_vec_iter);
+
+    // create and serialize package map
+    let package_table = PackageTable::new(container, &packages)?;
+    let package_table_file_path = PathBuf::from("package.map");
+    let package_table_file =
+        OutputFile { contents: package_table.as_bytes(), path: package_table_file_path };
+
+    // create and serialize flag map
+    let flag_table = FlagTable::new(container, &packages)?;
+    let flag_table_file_path = PathBuf::from("flag.map");
+    let flag_table_file =
+        OutputFile { contents: flag_table.as_bytes(), path: flag_table_file_path };
+
+    Ok(vec![package_table_file, flag_table_file])
 }
 
 #[cfg(test)]
@@ -91,23 +136,45 @@
     use super::*;
     use crate::Input;
 
+    /// Read and parse bytes as u16
+    pub fn read_u16_from_bytes(buf: &[u8], head: &mut usize) -> Result<u16> {
+        let val = u16::from_le_bytes(buf[*head..*head + 2].try_into()?);
+        *head += 2;
+        Ok(val)
+    }
+
+    /// Read and parse bytes as u32
+    pub fn read_u32_from_bytes(buf: &[u8], head: &mut usize) -> Result<u32> {
+        let val = u32::from_le_bytes(buf[*head..*head + 4].try_into()?);
+        *head += 4;
+        Ok(val)
+    }
+
+    /// Read and parse bytes as string
+    pub fn read_str_from_bytes(buf: &[u8], head: &mut usize) -> Result<String> {
+        let num_bytes = read_u32_from_bytes(buf, head)? as usize;
+        let val = String::from_utf8(buf[*head..*head + num_bytes].to_vec())?;
+        *head += num_bytes;
+        Ok(val)
+    }
+
     pub fn parse_all_test_flags() -> Vec<ProtoParsedFlags> {
         let aconfig_files = [
             (
                 "com.android.aconfig.storage.test_1",
-                "storage_test_1_part_1.aconfig",
-                include_bytes!("../../tests/storage_test_1_part_1.aconfig").as_slice(),
-            ),
-            (
-                "com.android.aconfig.storage.test_1",
-                "storage_test_1_part_2.aconfig",
-                include_bytes!("../../tests/storage_test_1_part_2.aconfig").as_slice(),
+                "storage_test_1.aconfig",
+                include_bytes!("../../tests/storage_test_1.aconfig").as_slice(),
             ),
             (
                 "com.android.aconfig.storage.test_2",
                 "storage_test_2.aconfig",
                 include_bytes!("../../tests/storage_test_2.aconfig").as_slice(),
             ),
+            (
+                "com.android.aconfig.storage.test_4",
+                "storage_test_4.aconfig",
+                include_bytes!("../../tests/storage_test_4.aconfig").as_slice(),
+            ),
         ];
 
         aconfig_files
@@ -143,16 +210,14 @@
             }
         }
 
-        assert_eq!(packages.len(), 2);
+        assert_eq!(packages.len(), 3);
 
         assert_eq!(packages[0].package_name, "com.android.aconfig.storage.test_1");
         assert_eq!(packages[0].package_id, 0);
-        assert_eq!(packages[0].flag_names.len(), 5);
+        assert_eq!(packages[0].flag_names.len(), 3);
         assert!(packages[0].flag_names.contains("enabled_rw"));
         assert!(packages[0].flag_names.contains("disabled_rw"));
         assert!(packages[0].flag_names.contains("enabled_ro"));
-        assert!(packages[0].flag_names.contains("disabled_ro"));
-        assert!(packages[0].flag_names.contains("enabled_fixed_ro"));
         assert_eq!(packages[0].boolean_offset, 0);
 
         assert_eq!(packages[1].package_name, "com.android.aconfig.storage.test_2");
@@ -161,6 +226,13 @@
         assert!(packages[1].flag_names.contains("enabled_ro"));
         assert!(packages[1].flag_names.contains("disabled_ro"));
         assert!(packages[1].flag_names.contains("enabled_fixed_ro"));
-        assert_eq!(packages[1].boolean_offset, 10);
+        assert_eq!(packages[1].boolean_offset, 3);
+
+        assert_eq!(packages[2].package_name, "com.android.aconfig.storage.test_4");
+        assert_eq!(packages[2].package_id, 2);
+        assert_eq!(packages[2].flag_names.len(), 2);
+        assert!(packages[2].flag_names.contains("enabled_ro"));
+        assert!(packages[2].flag_names.contains("enabled_fixed_ro"));
+        assert_eq!(packages[2].boolean_offset, 6);
     }
 }
diff --git a/tools/aconfig/src/storage/package_table.rs b/tools/aconfig/src/storage/package_table.rs
new file mode 100644
index 0000000..0ce1349
--- /dev/null
+++ b/tools/aconfig/src/storage/package_table.rs
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+use crate::storage::{self, FlagPackage};
+use anyhow::Result;
+
+#[derive(PartialEq, Debug)]
+pub struct PackageTableHeader {
+    pub version: u32,
+    pub container: String,
+    pub file_size: u32,
+    pub num_packages: u32,
+    pub bucket_offset: u32,
+    pub node_offset: u32,
+}
+
+impl PackageTableHeader {
+    fn new(container: &str, num_packages: u32) -> Self {
+        Self {
+            version: storage::FILE_VERSION,
+            container: String::from(container),
+            file_size: 0,
+            num_packages,
+            bucket_offset: 0,
+            node_offset: 0,
+        }
+    }
+
+    fn as_bytes(&self) -> Vec<u8> {
+        let mut result = Vec::new();
+        result.extend_from_slice(&self.version.to_le_bytes());
+        let container_bytes = self.container.as_bytes();
+        result.extend_from_slice(&(container_bytes.len() as u32).to_le_bytes());
+        result.extend_from_slice(container_bytes);
+        result.extend_from_slice(&self.file_size.to_le_bytes());
+        result.extend_from_slice(&self.num_packages.to_le_bytes());
+        result.extend_from_slice(&self.bucket_offset.to_le_bytes());
+        result.extend_from_slice(&self.node_offset.to_le_bytes());
+        result
+    }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct PackageTableNode {
+    pub package_name: String,
+    pub package_id: u32,
+    // offset of the first boolean flag in this flag package with respect to the start of
+    // boolean flag value array in the flag value file
+    pub boolean_offset: u32,
+    pub next_offset: Option<u32>,
+    pub bucket_index: u32,
+}
+
+impl PackageTableNode {
+    fn new(package: &FlagPackage, num_buckets: u32) -> Self {
+        let bucket_index =
+            storage::get_bucket_index(&package.package_name.to_string(), num_buckets);
+        Self {
+            package_name: String::from(package.package_name),
+            package_id: package.package_id,
+            boolean_offset: package.boolean_offset,
+            next_offset: None,
+            bucket_index,
+        }
+    }
+
+    fn as_bytes(&self) -> Vec<u8> {
+        let mut result = Vec::new();
+        let name_bytes = self.package_name.as_bytes();
+        result.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
+        result.extend_from_slice(name_bytes);
+        result.extend_from_slice(&self.package_id.to_le_bytes());
+        result.extend_from_slice(&self.boolean_offset.to_le_bytes());
+        result.extend_from_slice(&self.next_offset.unwrap_or(0).to_le_bytes());
+        result
+    }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct PackageTable {
+    pub header: PackageTableHeader,
+    pub buckets: Vec<Option<u32>>,
+    pub nodes: Vec<PackageTableNode>,
+}
+
+impl PackageTable {
+    pub fn new(container: &str, packages: &[FlagPackage]) -> Result<Self> {
+        // create table
+        let num_packages = packages.len() as u32;
+        let num_buckets = storage::get_table_size(num_packages)?;
+        let mut table = Self {
+            header: PackageTableHeader::new(container, num_packages),
+            buckets: vec![None; num_buckets as usize],
+            nodes: packages.iter().map(|pkg| PackageTableNode::new(pkg, num_buckets)).collect(),
+        };
+
+        // initialize all header fields
+        table.header.bucket_offset = table.header.as_bytes().len() as u32;
+        table.header.node_offset = table.header.bucket_offset + num_buckets * 4;
+        table.header.file_size = table.header.node_offset
+            + table.nodes.iter().map(|x| x.as_bytes().len()).sum::<usize>() as u32;
+
+        // sort nodes by bucket index for efficiency
+        table.nodes.sort_by(|a, b| a.bucket_index.cmp(&b.bucket_index));
+
+        // fill all node offset
+        let mut offset = table.header.node_offset;
+        for i in 0..table.nodes.len() {
+            let node_bucket_idx = table.nodes[i].bucket_index;
+            let next_node_bucket_idx = if i + 1 < table.nodes.len() {
+                Some(table.nodes[i + 1].bucket_index)
+            } else {
+                None
+            };
+
+            if table.buckets[node_bucket_idx as usize].is_none() {
+                table.buckets[node_bucket_idx as usize] = Some(offset);
+            }
+            offset += table.nodes[i].as_bytes().len() as u32;
+
+            if let Some(index) = next_node_bucket_idx {
+                if index == node_bucket_idx {
+                    table.nodes[i].next_offset = Some(offset);
+                }
+            }
+        }
+
+        Ok(table)
+    }
+
+    pub fn as_bytes(&self) -> Vec<u8> {
+        [
+            self.header.as_bytes(),
+            self.buckets.iter().map(|v| v.unwrap_or(0).to_le_bytes()).collect::<Vec<_>>().concat(),
+            self.nodes.iter().map(|v| v.as_bytes()).collect::<Vec<_>>().concat(),
+        ]
+        .concat()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::storage::{
+        group_flags_by_package, tests::parse_all_test_flags, tests::read_str_from_bytes,
+        tests::read_u32_from_bytes,
+    };
+
+    impl PackageTableHeader {
+        // test only method to deserialize back into the header struct
+        fn from_bytes(bytes: &[u8]) -> Result<Self> {
+            let mut head = 0;
+            Ok(Self {
+                version: read_u32_from_bytes(bytes, &mut head)?,
+                container: read_str_from_bytes(bytes, &mut head)?,
+                file_size: read_u32_from_bytes(bytes, &mut head)?,
+                num_packages: read_u32_from_bytes(bytes, &mut head)?,
+                bucket_offset: read_u32_from_bytes(bytes, &mut head)?,
+                node_offset: read_u32_from_bytes(bytes, &mut head)?,
+            })
+        }
+    }
+
+    impl PackageTableNode {
+        // test only method to deserialize back into the node struct
+        fn from_bytes(bytes: &[u8], num_buckets: u32) -> Result<Self> {
+            let mut head = 0;
+            let mut node = Self {
+                package_name: read_str_from_bytes(bytes, &mut head)?,
+                package_id: read_u32_from_bytes(bytes, &mut head)?,
+                boolean_offset: read_u32_from_bytes(bytes, &mut head)?,
+                next_offset: match read_u32_from_bytes(bytes, &mut head)? {
+                    0 => None,
+                    val => Some(val),
+                },
+                bucket_index: 0,
+            };
+            node.bucket_index = storage::get_bucket_index(&node.package_name, num_buckets);
+            Ok(node)
+        }
+    }
+
+    impl PackageTable {
+        // test only method to deserialize back into the table struct
+        fn from_bytes(bytes: &[u8]) -> Result<Self> {
+            let header = PackageTableHeader::from_bytes(bytes)?;
+            let num_packages = header.num_packages;
+            let num_buckets = storage::get_table_size(num_packages)?;
+            let mut head = header.as_bytes().len();
+            let buckets = (0..num_buckets)
+                .map(|_| match read_u32_from_bytes(bytes, &mut head).unwrap() {
+                    0 => None,
+                    val => Some(val),
+                })
+                .collect();
+            let nodes = (0..num_packages)
+                .map(|_| {
+                    let node = PackageTableNode::from_bytes(&bytes[head..], num_buckets).unwrap();
+                    head += node.as_bytes().len();
+                    node
+                })
+                .collect();
+
+            let table = Self { header, buckets, nodes };
+            Ok(table)
+        }
+    }
+
+    pub fn create_test_package_table() -> Result<PackageTable> {
+        let caches = parse_all_test_flags();
+        let packages = group_flags_by_package(caches.iter());
+        PackageTable::new("system", &packages)
+    }
+
+    #[test]
+    // this test point locks down the table creation and each field
+    fn test_table_contents() {
+        let package_table = create_test_package_table();
+        assert!(package_table.is_ok());
+
+        let header: &PackageTableHeader = &package_table.as_ref().unwrap().header;
+        let expected_header = PackageTableHeader {
+            version: storage::FILE_VERSION,
+            container: String::from("system"),
+            file_size: 208,
+            num_packages: 3,
+            bucket_offset: 30,
+            node_offset: 58,
+        };
+        assert_eq!(header, &expected_header);
+
+        let buckets: &Vec<Option<u32>> = &package_table.as_ref().unwrap().buckets;
+        let expected: Vec<Option<u32>> = vec![Some(58), None, None, Some(108), None, None, None];
+        assert_eq!(buckets, &expected);
+
+        let nodes: &Vec<PackageTableNode> = &package_table.as_ref().unwrap().nodes;
+        assert_eq!(nodes.len(), 3);
+        let first_node_expected = PackageTableNode {
+            package_name: String::from("com.android.aconfig.storage.test_2"),
+            package_id: 1,
+            boolean_offset: 3,
+            next_offset: None,
+            bucket_index: 0,
+        };
+        assert_eq!(nodes[0], first_node_expected);
+        let second_node_expected = PackageTableNode {
+            package_name: String::from("com.android.aconfig.storage.test_1"),
+            package_id: 0,
+            boolean_offset: 0,
+            next_offset: Some(158),
+            bucket_index: 3,
+        };
+        assert_eq!(nodes[1], second_node_expected);
+        let third_node_expected = PackageTableNode {
+            package_name: String::from("com.android.aconfig.storage.test_4"),
+            package_id: 2,
+            boolean_offset: 6,
+            next_offset: None,
+            bucket_index: 3,
+        };
+        assert_eq!(nodes[2], third_node_expected);
+    }
+
+    #[test]
+    // this test point locks down the table serialization
+    fn test_serialization() {
+        let package_table = create_test_package_table();
+        assert!(package_table.is_ok());
+        let package_table = package_table.unwrap();
+
+        let header: &PackageTableHeader = &package_table.header;
+        let reinterpreted_header = PackageTableHeader::from_bytes(&header.as_bytes());
+        assert!(reinterpreted_header.is_ok());
+        assert_eq!(header, &reinterpreted_header.unwrap());
+
+        let nodes: &Vec<PackageTableNode> = &package_table.nodes;
+        let num_buckets = storage::get_table_size(header.num_packages).unwrap();
+        for node in nodes.iter() {
+            let reinterpreted_node = PackageTableNode::from_bytes(&node.as_bytes(), num_buckets);
+            assert!(reinterpreted_node.is_ok());
+            assert_eq!(node, &reinterpreted_node.unwrap());
+        }
+
+        let reinterpreted_table = PackageTable::from_bytes(&package_table.as_bytes());
+        assert!(reinterpreted_table.is_ok());
+        assert_eq!(&package_table, &reinterpreted_table.unwrap());
+    }
+}
diff --git a/tools/aconfig/src/test.rs b/tools/aconfig/src/test.rs
index 71de57e..cbb95b8 100644
--- a/tools/aconfig/src/test.rs
+++ b/tools/aconfig/src/test.rs
@@ -145,6 +145,31 @@
 }
 parsed_flag {
   package: "com.android.aconfig.test"
+  name: "enabled_fixed_ro_exported"
+  namespace: "aconfig_test"
+  description: "This flag is fixed ENABLED + READ_ONLY and exported"
+  bug: "111"
+  state: ENABLED
+  permission: READ_ONLY
+  trace {
+    source: "tests/test.aconfig"
+    state: DISABLED
+    permission: READ_ONLY
+  }
+  trace {
+    source: "tests/first.values"
+    state: ENABLED
+    permission: READ_ONLY
+  }
+  is_fixed_read_only: true
+  is_exported: true
+  container: "system"
+  metadata {
+    purpose: PURPOSE_UNSPECIFIED
+  }
+}
+parsed_flag {
+  package: "com.android.aconfig.test"
   name: "enabled_ro"
   namespace: "aconfig_test"
   description: "This flag is ENABLED + READ_ONLY"
@@ -225,6 +250,24 @@
 }
 "#;
 
+    pub fn parse_read_only_test_flags() -> ProtoParsedFlags {
+        let bytes = crate::commands::parse_flags(
+            "com.android.aconfig.test",
+            Some("system"),
+            vec![Input {
+                source: "tests/read_only_test.aconfig".to_string(),
+                reader: Box::new(include_bytes!("../tests/read_only_test.aconfig").as_slice()),
+            }],
+            vec![Input {
+                source: "tests/read_only_test.values".to_string(),
+                reader: Box::new(include_bytes!("../tests/read_only_test.values").as_slice()),
+            }],
+            crate::commands::DEFAULT_FLAG_PERMISSION,
+        )
+        .unwrap();
+        crate::protos::parsed_flags::try_from_binary_proto(&bytes).unwrap()
+    }
+
     pub fn parse_test_flags() -> ProtoParsedFlags {
         let bytes = crate::commands::parse_flags(
             "com.android.aconfig.test",
diff --git a/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template b/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
index 8010b88..933d6a7 100644
--- a/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
+++ b/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
@@ -12,23 +12,11 @@
     }
 
 {{ for item in flag_elements}}
-{{ if library_exported }}
-
-{{ if item.exported }}
     @Override
     @UnsupportedAppUsage
     public boolean {item.method_name}() \{
         return getValue(Flags.FLAG_{item.flag_name_constant_suffix});
     }
-{{ endif }}
-
-{{ else }}
-    @Override
-    @UnsupportedAppUsage
-    public boolean {item.method_name}() \{
-        return getValue(Flags.FLAG_{item.flag_name_constant_suffix});
-    }
-{{ endif }}
 {{ endfor}}
     public void setFlag(String flagName, boolean value) \{
         if (!this.mFlagMap.containsKey(flagName)) \{
@@ -52,20 +40,11 @@
     }
 
     private Map<String, Boolean> mFlagMap = new HashMap<>(
-        {{ if library_exported }}
-        Map.ofEntries(
-            {{-for item in exported_flag_elements}}
-            Map.entry(Flags.FLAG_{item.flag_name_constant_suffix}, false)
-            {{ -if not @last }},{{ endif }}
-            {{ -endfor }}
-        )
-        {{ else }}
         Map.ofEntries(
             {{-for item in flag_elements}}
             Map.entry(Flags.FLAG_{item.flag_name_constant_suffix}, false)
             {{ -if not @last }},{{ endif }}
             {{ -endfor }}
         )
-        {{ endif }}
     );
 }
diff --git a/tools/aconfig/templates/FeatureFlags.java.template b/tools/aconfig/templates/FeatureFlags.java.template
index 180f882..5e67b13 100644
--- a/tools/aconfig/templates/FeatureFlags.java.template
+++ b/tools/aconfig/templates/FeatureFlags.java.template
@@ -5,15 +5,10 @@
 /** @hide */
 public interface FeatureFlags \{
 {{ for item in flag_elements }}
-{{ if library_exported }}
-
-{{ if item.exported }}
+{{ -if library_exported }}
     @UnsupportedAppUsage
     boolean {item.method_name}();
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
 {{ -if not item.is_read_write }}
 {{ -if item.default_value }}
     @com.android.aconfig.annotations.AssumeTrueForR8
@@ -23,7 +18,6 @@
 {{ endif }}
     @UnsupportedAppUsage
     boolean {item.method_name}();
-
 {{ endif }}
-{{ endfor }}
+{{ -endfor }}
 }
diff --git a/tools/aconfig/templates/FeatureFlagsImpl.java.template b/tools/aconfig/templates/FeatureFlagsImpl.java.template
index 7a52ceb..28baa41 100644
--- a/tools/aconfig/templates/FeatureFlagsImpl.java.template
+++ b/tools/aconfig/templates/FeatureFlagsImpl.java.template
@@ -1,54 +1,42 @@
 package {package_name};
 // TODO(b/303773055): Remove the annotation after access issue is resolved.
 import android.compat.annotation.UnsupportedAppUsage;
-{{ if not is_test_mode }}
-{{ if runtime_lookup_required- }}
+{{ -if not is_test_mode }}
+{{ -if runtime_lookup_required }}
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
 {{ endif }}
 /** @hide */
 public final class FeatureFlagsImpl implements FeatureFlags \{
-{{- if runtime_lookup_required }}
-{{- for namespace_with_flags in namespace_flags }}
+{{ -if runtime_lookup_required }}
+{{ -for namespace_with_flags in namespace_flags }}
     private static boolean {namespace_with_flags.namespace}_is_cached = false;
-{{- endfor- }}
+{{ -endfor- }}
 
 {{ for flag in flag_elements }}
-{{ if library_exported }}
-{{ if flag.exported }}
+{{ -if library_exported }}
     private static boolean {flag.method_name} = false;
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
 {{- if flag.is_read_write }}
     private static boolean {flag.method_name} = {flag.default_value};
 {{- endif- }}
-{{ endif }}
-{{ endfor }}
-
+{{ -endif }}
+{{ -endfor }}
 {{ for namespace_with_flags in namespace_flags }}
     private void load_overrides_{namespace_with_flags.namespace}() \{
         try \{
             Properties properties = DeviceConfig.getProperties("{namespace_with_flags.namespace}");
-
-            {{- for flag in namespace_with_flags.flags }}
-            {{ if library_exported }}
-
-            {{ if flag.exported }}
+{{ -for flag in namespace_with_flags.flags }}
+{{ -if library_exported }}
             {flag.method_name} =
                 properties.getBoolean("{flag.device_config_flag}", false);
-            {{ endif }}
-
-            {{ else }}
-
-            {{ if flag.is_read_write }}
+{{ -else }}
+{{ -if flag.is_read_write }}
             {flag.method_name} =
                 properties.getBoolean("{flag.device_config_flag}", {flag.default_value});
-            {{ endif }}
-
-            {{ endif }}
-            {{ endfor }}
+{{ -endif }}
+{{ -endif }}
+{{ -endfor }}
         } catch (NullPointerException e) \{
             throw new RuntimeException(
                 "Cannot read value from namespace {namespace_with_flags.namespace} "
@@ -62,37 +50,27 @@
         {namespace_with_flags.namespace}_is_cached = true;
     }
 {{ endfor- }}
-{{ endif- }}
-
-{{ for flag in flag_elements }}
-{{ if library_exported }}
-
-{{ if flag.exported }}
+{{ -endif }}{#- end of runtime_lookup_required #}
+{{ -for flag in flag_elements }}
     @Override
     @UnsupportedAppUsage
     public boolean {flag.method_name}() \{
+{{ -if library_exported }}
         if (!{flag.device_config_namespace}_is_cached) \{
             load_overrides_{flag.device_config_namespace}();
         }
         return {flag.method_name};
-    }
-{{ endif }}
-
-{{ else }}
-    @Override
-    @UnsupportedAppUsage
-    public boolean {flag.method_name}() \{
-    {{ -if flag.is_read_write }}
+{{ -else }}
+{{ -if flag.is_read_write }}
         if (!{flag.device_config_namespace}_is_cached) \{
             load_overrides_{flag.device_config_namespace}();
         }
         return {flag.method_name};
-    {{ else }}
+{{ -else }}
         return {flag.default_value};
-    {{ endif- }}
+{{ -endif- }}
+{{ -endif }}
     }
-{{ endif }}
-
 {{ endfor }}
 }
 {{ else }}
diff --git a/tools/aconfig/templates/Flags.java.template b/tools/aconfig/templates/Flags.java.template
index 9f4c52f..34b8189 100644
--- a/tools/aconfig/templates/Flags.java.template
+++ b/tools/aconfig/templates/Flags.java.template
@@ -5,42 +5,30 @@
 
 /** @hide */
 public final class Flags \{
-{{- for item in flag_elements}}
-    {{ if library_exported }}
-    {{ if item.exported }}
+{{ -for item in flag_elements}}
     /** @hide */
     public static final String FLAG_{item.flag_name_constant_suffix} = "{item.device_config_flag}";
-    {{ endif }}
-    {{ else }}
-    /** @hide */
-    public static final String FLAG_{item.flag_name_constant_suffix} = "{item.device_config_flag}";
-    {{ endif }}
 {{- endfor }}
-{{ for item in flag_elements}}
+{{ -for item in flag_elements}}
 {{ if library_exported }}
-
-{{ if item.exported }}
     @UnsupportedAppUsage
     public static boolean {item.method_name}() \{
         return FEATURE_FLAGS.{item.method_name}();
     }
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
 {{ -if not item.is_read_write }}
 {{ -if item.default_value }}
     @com.android.aconfig.annotations.AssumeTrueForR8
 {{ -else }}
     @com.android.aconfig.annotations.AssumeFalseForR8
-{{ -endif- }}
-{{ endif }}
+{{ -endif }}
+{{ -endif }}
     @UnsupportedAppUsage
     public static boolean {item.method_name}() \{
         return FEATURE_FLAGS.{item.method_name}();
     }
-{{ endif }}
-{{ endfor }}
+{{ -endif }}
+{{ -endfor }}
 {{ -if is_test_mode }}
     public static void setFeatureFlags(FeatureFlags featureFlags) \{
         Flags.FEATURE_FLAGS = featureFlags;
@@ -49,7 +37,8 @@
     public static void unsetFeatureFlags() \{
         Flags.FEATURE_FLAGS = null;
     }
-{{ endif }}
+{{ -endif }}
+
     private static FeatureFlags FEATURE_FLAGS{{ -if not is_test_mode }} = new FeatureFlagsImpl(){{ -endif- }};
 
 }
diff --git a/tools/aconfig/templates/cpp_exported_header.template b/tools/aconfig/templates/cpp_exported_header.template
index cc1b18d..0f7853e 100644
--- a/tools/aconfig/templates/cpp_exported_header.template
+++ b/tools/aconfig/templates/cpp_exported_header.template
@@ -1,16 +1,16 @@
 #pragma once
 
-{{ if not for_test- }}
+{{ if not is_test_mode- }}
 {{ if has_fixed_read_only- }}
 #ifndef {package_macro}
 #define {package_macro}(FLAG) {package_macro}_##FLAG
 #endif
-{{ for item in class_elements- }}
-{{ if item.is_fixed_read_only- }}
+{{ for item in class_elements }}
+{{ -if item.is_fixed_read_only }}
 #ifndef {package_macro}_{item.flag_macro}
 #define {package_macro}_{item.flag_macro} {item.default_value}
 #endif
-{{ endif }}
+{{ -endif }}
 {{ -endfor }}
 {{ -endif }}
 {{ -endif }}
@@ -24,15 +24,15 @@
 class flag_provider_interface \{
 public:
     virtual ~flag_provider_interface() = default;
-    {{ for item in class_elements}}
+    {{ -for item in class_elements}}
     virtual bool {item.flag_name}() = 0;
 
-    {{ if for_test }}
+    {{ -if is_test_mode }}
     virtual void {item.flag_name}(bool val) = 0;
     {{ -endif }}
     {{ -endfor }}
 
-    {{ if for_test }}
+    {{ -if is_test_mode }}
     virtual void reset_flags() \{}
     {{ -endif }}
 };
@@ -41,29 +41,29 @@
 
 {{ for item in class_elements}}
 inline bool {item.flag_name}() \{
-    {{ if for_test }}
+    {{ -if is_test_mode }}
     return provider_->{item.flag_name}();
-    {{ -else- }}
-    {{ if item.readwrite- }}
+    {{ -else }}
+    {{ -if item.readwrite }}
     return provider_->{item.flag_name}();
-    {{ -else- }}
-    {{ if item.is_fixed_read_only }}
+    {{ -else }}
+    {{ -if item.is_fixed_read_only }}
     return {package_macro}_{item.flag_macro};
-    {{ -else- }}
+    {{ -else }}
     return {item.default_value};
     {{ -endif }}
     {{ -endif }}
     {{ -endif }}
 }
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 inline void {item.flag_name}(bool val) \{
     provider_->{item.flag_name}(val);
 }
 {{ -endif }}
 {{ -endfor }}
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 inline void reset_flags() \{
     return provider_->reset_flags();
 }
@@ -77,12 +77,12 @@
 {{ for item in class_elements }}
 bool {header}_{item.flag_name}();
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 void set_{header}_{item.flag_name}(bool val);
 {{ -endif }}
 {{ -endfor }}
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 void {header}_reset_flags();
 {{ -endif }}
 
diff --git a/tools/aconfig/templates/cpp_source_file.template b/tools/aconfig/templates/cpp_source_file.template
index 1bfa4b6..4bcd1b7 100644
--- a/tools/aconfig/templates/cpp_source_file.template
+++ b/tools/aconfig/templates/cpp_source_file.template
@@ -1,17 +1,20 @@
 #include "{header}.h"
-{{ if readwrite }}
+
+{{ if readwrite- }}
 #include <server_configurable_flags/get_flags.h>
 {{ endif }}
-{{ if for_test }}
+{{ if is_test_mode }}
 #include <unordered_map>
 #include <string>
 {{ -else- }}
+{{ if readwrite- }}
 #include <vector>
-{{ endif }}
+{{ -endif }}
+{{ -endif }}
 
 namespace {cpp_namespace} \{
 
-{{ if for_test }}
+{{ if is_test_mode }}
     class flag_provider : public flag_provider_interface \{
     private:
         std::unordered_map<std::string, bool> overrides_;
@@ -21,7 +24,7 @@
             : overrides_()
         \{}
 
-        {{ for item in class_elements}}
+{{ for item in class_elements }}
         virtual bool {item.flag_name}() override \{
             auto it = overrides_.find("{item.flag_name}");
               if (it != overrides_.end()) \{
@@ -32,7 +35,7 @@
                   "aconfig_flags.{item.device_config_namespace}",
                   "{item.device_config_flag}",
                   "{item.default_value}") == "true";
-              {{ -else- }}
+              {{ -else }}
                   return {item.default_value};
               {{ -endif }}
             }
@@ -41,7 +44,7 @@
         virtual void {item.flag_name}(bool val) override \{
             overrides_["{item.flag_name}"] = val;
         }
-        {{ endfor }}
+{{ endfor }}
 
         virtual void reset_flags() override \{
             overrides_.clear();
@@ -52,9 +55,11 @@
 
     class flag_provider : public flag_provider_interface \{
     public:
-        {{ for item in class_elements}}
+
+        {{ -for item in class_elements }}
+
         virtual bool {item.flag_name}() override \{
-            {{ if item.readwrite- }}
+            {{ -if item.readwrite }}
             if (cache_[{item.readwrite_idx}] == -1) \{
                 cache_[{item.readwrite_idx}] = server_configurable_flags::GetServerConfigurableFlag(
                     "aconfig_flags.{item.device_config_namespace}",
@@ -62,17 +67,19 @@
                     "{item.default_value}") == "true";
             }
             return cache_[{item.readwrite_idx}];
-            {{ -else- }}
-            {{ if item.is_fixed_read_only }}
+            {{ -else }}
+            {{ -if item.is_fixed_read_only }}
             return {package_macro}_{item.flag_macro};
-            {{ -else- }}
+            {{ -else }}
             return {item.default_value};
             {{ -endif }}
             {{ -endif }}
         }
-        {{ endfor }}
+        {{ -endfor }}
+    {{ if readwrite- }}
     private:
         std::vector<int8_t> cache_ = std::vector<int8_t>({readwrite_count}, -1);
+    {{ -endif }}
     };
 
 
@@ -82,32 +89,31 @@
     std::make_unique<flag_provider>();
 }
 
-
-{{ for item in class_elements}}
+{{ for item in class_elements }}
 bool {header}_{item.flag_name}() \{
-    {{ if for_test }}
+    {{ -if is_test_mode }}
     return {cpp_namespace}::{item.flag_name}();
-    {{ -else- }}
-    {{ if item.readwrite- }}
+    {{ -else }}
+    {{ -if item.readwrite }}
     return {cpp_namespace}::{item.flag_name}();
-    {{ -else- }}
-    {{ if item.is_fixed_read_only }}
+    {{ -else }}
+    {{ -if item.is_fixed_read_only }}
     return {package_macro}_{item.flag_macro};
-    {{ -else- }}
+    {{ -else }}
     return {item.default_value};
     {{ -endif }}
     {{ -endif }}
     {{ -endif }}
 }
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 void set_{header}_{item.flag_name}(bool val) \{
     {cpp_namespace}::{item.flag_name}(val);
 }
 {{ -endif }}
-{{ endfor -}}
+{{ endfor }}
 
-{{ if for_test }}
+{{ -if is_test_mode }}
 void {header}_reset_flags() \{
      {cpp_namespace}::reset_flags();
 }
diff --git a/tools/aconfig/templates/rust_prod.template b/tools/aconfig/templates/rust.template
similarity index 77%
rename from tools/aconfig/templates/rust_prod.template
rename to tools/aconfig/templates/rust.template
index 30ea646..f9a2829 100644
--- a/tools/aconfig/templates/rust_prod.template
+++ b/tools/aconfig/templates/rust.template
@@ -3,32 +3,32 @@
 /// flag provider
 pub struct FlagProvider;
 
-{{ if has_readwrite - }}
+{{ if has_readwrite- }}
 lazy_static::lazy_static! \{
-    {{ for flag in template_flags }}
-    {{ if flag.readwrite -}}
+{{ -for flag in template_flags }}
+    {{ -if flag.readwrite }}
     /// flag value cache for {flag.name}
     static ref CACHED_{flag.name}: bool = flags_rust::GetServerConfigurableFlag(
         "aconfig_flags.{flag.device_config_namespace}",
         "{flag.device_config_flag}",
         "{flag.default_value}") == "true";
     {{ -endif }}
-    {{ endfor }}
+{{ -endfor }}
 }
 {{ -endif }}
 
 impl FlagProvider \{
 
-    {{ for flag in template_flags }}
+{{ for flag in template_flags }}
     /// query flag {flag.name}
     pub fn {flag.name}(&self) -> bool \{
-    {{ if flag.readwrite -}}
+    {{ -if flag.readwrite }}
         *CACHED_{flag.name}
-    {{ -else- }}
+    {{ -else }}
         {flag.default_value}
     {{ -endif }}
     }
-    {{ endfor }}
+{{ endfor }}
 
 }
 
@@ -38,10 +38,10 @@
 {{ for flag in template_flags }}
 /// query flag {flag.name}
 #[inline(always)]
-{{ if flag.readwrite -}}
+{{ -if flag.readwrite }}
 pub fn {flag.name}() -> bool \{
     PROVIDER.{flag.name}()
-{{ -else- }}
+{{ -else }}
 pub fn {flag.name}() -> bool \{
     {flag.default_value}
 {{ -endif }}
diff --git a/tools/aconfig/templates/rust_test.template b/tools/aconfig/templates/rust_test.template
index fd1229b..d01f40a 100644
--- a/tools/aconfig/templates/rust_test.template
+++ b/tools/aconfig/templates/rust_test.template
@@ -9,7 +9,7 @@
 }
 
 impl FlagProvider \{
-    {{ for flag in template_flags }}
+{{ for flag in template_flags }}
     /// query flag {flag.name}
     pub fn {flag.name}(&self) -> bool \{
         self.overrides.get("{flag.name}").copied().unwrap_or(
@@ -28,7 +28,7 @@
     pub fn set_{flag.name}(&mut self, val: bool) \{
         self.overrides.insert("{flag.name}", val);
     }
-    {{ endfor }}
+{{ endfor }}
 
     /// clear all flag overrides
     pub fn reset_flags(&mut self) \{
diff --git a/tools/aconfig/tests/AconfigTest.java b/tools/aconfig/tests/AconfigTest.java
index bb993c4..7e76efb 100644
--- a/tools/aconfig/tests/AconfigTest.java
+++ b/tools/aconfig/tests/AconfigTest.java
@@ -10,6 +10,7 @@
 import static com.android.aconfig.test.Flags.enabledRw;
 import static com.android.aconfig.test.exported.Flags.exportedFlag;
 import static com.android.aconfig.test.exported.Flags.FLAG_EXPORTED_FLAG;
+import static com.android.aconfig.test.forcereadonly.Flags.froRw;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -33,17 +34,13 @@
     @Test
     public void testEnabledReadOnlyFlag() {
         assertEquals("com.android.aconfig.test.disabled_rw", FLAG_DISABLED_RW);
-        // TODO: change to assertTrue(enabledRo()) when the build supports reading tests/*.values
-        // (currently all flags are assigned the default READ_ONLY + DISABLED)
-        assertFalse(enabledRo());
+        assertTrue(enabledRo());
     }
 
     @Test
     public void testEnabledFixedReadOnlyFlag() {
         assertEquals("com.android.aconfig.test.enabled_fixed_ro", FLAG_ENABLED_FIXED_RO);
-        // TODO: change to assertTrue(enabledFixedRo()) when the build supports reading tests/*.values
-        // (currently all flags are assigned the default READ_ONLY + DISABLED)
-        assertFalse(enabledFixedRo());
+        assertTrue(enabledFixedRo());
     }
 
     @Test
@@ -55,9 +52,7 @@
     @Test
     public void testEnabledReadWriteFlag() {
         assertEquals("com.android.aconfig.test.enabled_rw", FLAG_ENABLED_RW);
-        // TODO: change to assertTrue(enabledRw()) when the build supports reading tests/*.values
-        // (currently all flags are assigned the default READ_ONLY + DISABLED)
-        assertFalse(enabledRw());
+        assertTrue(enabledRw());
     }
 
     @Test
@@ -72,4 +67,9 @@
         assertEquals("com.android.aconfig.test.exported.exported_flag", FLAG_EXPORTED_FLAG);
         assertFalse(exportedFlag());
     }
+
+    @Test
+    public void testForceReadOnly() {
+        assertFalse(froRw());
+    }
 }
diff --git a/tools/aconfig/tests/aconfig_exported_mode_test.cpp b/tools/aconfig/tests/aconfig_exported_mode_test.cpp
new file mode 100644
index 0000000..d6eab43
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_exported_mode_test.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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 "com_android_aconfig_test.h"
+#include "gtest/gtest.h"
+
+using namespace com::android::aconfig::test;
+
+TEST(AconfigTest, TestDisabledRwExportedFlag) {
+  ASSERT_FALSE(com_android_aconfig_test_disabled_rw_exported());
+  ASSERT_FALSE(provider_->disabled_rw_exported());
+  ASSERT_FALSE(disabled_rw_exported());
+}
+
+TEST(AconfigTest, TestEnabledFixedRoExportedFlag) {
+  // TODO: change to assertTrue(enabledFixedRoExported()) when the build supports reading tests/*.values
+  ASSERT_FALSE(com_android_aconfig_test_enabled_fixed_ro_exported());
+  ASSERT_FALSE(provider_->enabled_fixed_ro_exported());
+  ASSERT_FALSE(enabled_fixed_ro_exported());
+}
+
+TEST(AconfigTest, TestEnabledRoExportedFlag) {
+  // TODO: change to assertTrue(enabledRoExported()) when the build supports reading tests/*.values
+  ASSERT_FALSE(com_android_aconfig_test_enabled_ro_exported());
+  ASSERT_FALSE(provider_->enabled_ro_exported());
+  ASSERT_FALSE(enabled_ro_exported());
+}
+
+int main(int argc, char** argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
\ No newline at end of file
diff --git a/tools/aconfig/tests/aconfig_exported_mode_test.rs b/tools/aconfig/tests/aconfig_exported_mode_test.rs
new file mode 100644
index 0000000..4b48047
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_exported_mode_test.rs
@@ -0,0 +1,7 @@
+#[cfg(not(feature = "cargo"))]
+#[test]
+fn test_flags() {
+    assert!(!aconfig_test_rust_library::disabled_rw_exported());
+    assert!(!aconfig_test_rust_library::enabled_fixed_ro_exported());
+    assert!(!aconfig_test_rust_library::enabled_ro_exported());
+}
diff --git a/tools/aconfig/tests/aconfig_force_read_only_mode_test.cpp b/tools/aconfig/tests/aconfig_force_read_only_mode_test.cpp
new file mode 100644
index 0000000..0dec481
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_force_read_only_mode_test.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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 "com_android_aconfig_test.h"
+#include "gtest/gtest.h"
+
+using namespace com::android::aconfig::test;
+
+TEST(AconfigTest, TestDisabledReadOnlyFlag) {
+  ASSERT_FALSE(com_android_aconfig_test_disabled_ro());
+  ASSERT_FALSE(provider_->disabled_ro());
+  ASSERT_FALSE(disabled_ro());
+}
+
+TEST(AconfigTest, TestEnabledReadOnlyFlag) {
+  ASSERT_TRUE(com_android_aconfig_test_enabled_ro());
+  ASSERT_TRUE(provider_->enabled_ro());
+  ASSERT_TRUE(enabled_ro());
+}
+
+TEST(AconfigTest, TestDisabledReadWriteFlag) {
+  ASSERT_FALSE(com_android_aconfig_test_disabled_rw());
+  ASSERT_FALSE(provider_->disabled_rw());
+  ASSERT_FALSE(disabled_rw());
+}
+
+TEST(AconfigTest, TestEnabledReadWriteFlag) {
+  ASSERT_TRUE(com_android_aconfig_test_enabled_rw());
+  ASSERT_TRUE(provider_->enabled_rw());
+  ASSERT_TRUE(enabled_rw());
+}
+
+TEST(AconfigTest, TestEnabledFixedReadOnlyFlag) {
+  ASSERT_TRUE(com_android_aconfig_test_enabled_fixed_ro());
+  ASSERT_TRUE(provider_->enabled_fixed_ro());
+  ASSERT_TRUE(enabled_fixed_ro());
+}
+
+int main(int argc, char** argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/tools/aconfig/tests/aconfig_force_read_only_mode_test.rs b/tools/aconfig/tests/aconfig_force_read_only_mode_test.rs
new file mode 100644
index 0000000..4f05e26
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_force_read_only_mode_test.rs
@@ -0,0 +1,10 @@
+#[cfg(not(feature = "cargo"))]
+#[test]
+fn test_flags() {
+    assert!(!aconfig_test_rust_library::disabled_ro());
+    assert!(!aconfig_test_rust_library::disabled_rw());
+    assert!(!aconfig_test_rust_library::disabled_rw_in_other_namespace());
+    assert!(aconfig_test_rust_library::enabled_fixed_ro());
+    assert!(aconfig_test_rust_library::enabled_ro());
+    assert!(aconfig_test_rust_library::enabled_rw());
+}
diff --git a/tools/aconfig/tests/aconfig_prod_mode_test.rs b/tools/aconfig/tests/aconfig_prod_mode_test.rs
index 950c441..e1fb8e5 100644
--- a/tools/aconfig/tests/aconfig_prod_mode_test.rs
+++ b/tools/aconfig/tests/aconfig_prod_mode_test.rs
@@ -3,7 +3,6 @@
 fn test_flags() {
     assert!(!aconfig_test_rust_library::disabled_ro());
     assert!(!aconfig_test_rust_library::disabled_rw());
-    // TODO: Fix template to not default both disabled and enabled to false
-    assert!(!aconfig_test_rust_library::enabled_ro());
-    assert!(!aconfig_test_rust_library::enabled_rw());
+    assert!(aconfig_test_rust_library::enabled_ro());
+    assert!(aconfig_test_rust_library::enabled_rw());
 }
diff --git a/tools/aconfig/tests/aconfig_test.cpp b/tools/aconfig/tests/aconfig_test.cpp
index 52651e4..0dec481 100644
--- a/tools/aconfig/tests/aconfig_test.cpp
+++ b/tools/aconfig/tests/aconfig_test.cpp
@@ -26,11 +26,9 @@
 }
 
 TEST(AconfigTest, TestEnabledReadOnlyFlag) {
-  // TODO: change to assertTrue(enabledRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_ro());
-  ASSERT_FALSE(provider_->enabled_ro());
-  ASSERT_FALSE(enabled_ro());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_ro());
+  ASSERT_TRUE(provider_->enabled_ro());
+  ASSERT_TRUE(enabled_ro());
 }
 
 TEST(AconfigTest, TestDisabledReadWriteFlag) {
@@ -40,19 +38,15 @@
 }
 
 TEST(AconfigTest, TestEnabledReadWriteFlag) {
-  // TODO: change to assertTrue(enabledRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_rw());
-  ASSERT_FALSE(provider_->enabled_rw());
-  ASSERT_FALSE(enabled_rw());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_rw());
+  ASSERT_TRUE(provider_->enabled_rw());
+  ASSERT_TRUE(enabled_rw());
 }
 
 TEST(AconfigTest, TestEnabledFixedReadOnlyFlag) {
-  // TODO: change to assertTrue(enabledFixedRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_fixed_ro());
-  ASSERT_FALSE(provider_->enabled_fixed_ro());
-  ASSERT_FALSE(enabled_fixed_ro());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_fixed_ro());
+  ASSERT_TRUE(provider_->enabled_fixed_ro());
+  ASSERT_TRUE(enabled_fixed_ro());
 }
 
 int main(int argc, char** argv) {
diff --git a/tools/aconfig/tests/aconfig_test_mode_test.rs b/tools/aconfig/tests/aconfig_test_mode_test.rs
index 3f56d2c..a08fbab 100644
--- a/tools/aconfig/tests/aconfig_test_mode_test.rs
+++ b/tools/aconfig/tests/aconfig_test_mode_test.rs
@@ -3,22 +3,21 @@
 fn test_flags() {
     assert!(!aconfig_test_rust_library::disabled_ro());
     assert!(!aconfig_test_rust_library::disabled_rw());
-    // TODO: Fix template to not default both disabled and enabled to false
-    assert!(!aconfig_test_rust_library::enabled_ro());
-    assert!(!aconfig_test_rust_library::enabled_rw());
+    assert!(aconfig_test_rust_library::enabled_ro());
+    assert!(aconfig_test_rust_library::enabled_rw());
 
     aconfig_test_rust_library::set_disabled_ro(true);
     assert!(aconfig_test_rust_library::disabled_ro());
     aconfig_test_rust_library::set_disabled_rw(true);
     assert!(aconfig_test_rust_library::disabled_rw());
-    aconfig_test_rust_library::set_enabled_ro(true);
-    assert!(aconfig_test_rust_library::enabled_ro());
-    aconfig_test_rust_library::set_enabled_rw(true);
-    assert!(aconfig_test_rust_library::enabled_rw());
+    aconfig_test_rust_library::set_enabled_ro(false);
+    assert!(!aconfig_test_rust_library::enabled_ro());
+    aconfig_test_rust_library::set_enabled_rw(false);
+    assert!(!aconfig_test_rust_library::enabled_rw());
 
     aconfig_test_rust_library::reset_flags();
     assert!(!aconfig_test_rust_library::disabled_ro());
     assert!(!aconfig_test_rust_library::disabled_rw());
-    assert!(!aconfig_test_rust_library::enabled_ro());
-    assert!(!aconfig_test_rust_library::enabled_rw());
+    assert!(aconfig_test_rust_library::enabled_ro());
+    assert!(aconfig_test_rust_library::enabled_rw());
 }
diff --git a/tools/aconfig/tests/aconfig_test_test_variant.cpp b/tools/aconfig/tests/aconfig_test_test_variant.cpp
index 8a745c5..b3cf710 100644
--- a/tools/aconfig/tests/aconfig_test_test_variant.cpp
+++ b/tools/aconfig/tests/aconfig_test_test_variant.cpp
@@ -33,11 +33,9 @@
 }
 
 TEST_F(AconfigTest, TestEnabledReadOnlyFlag) {
-  // TODO: change to assertTrue(enabledRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_ro());
-  ASSERT_FALSE(provider_->enabled_ro());
-  ASSERT_FALSE(enabled_ro());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_ro());
+  ASSERT_TRUE(provider_->enabled_ro());
+  ASSERT_TRUE(enabled_ro());
 }
 
 TEST_F(AconfigTest, TestDisabledReadWriteFlag) {
@@ -47,19 +45,15 @@
 }
 
 TEST_F(AconfigTest, TestEnabledReadWriteFlag) {
-  // TODO: change to assertTrue(enabledRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_rw());
-  ASSERT_FALSE(provider_->enabled_rw());
-  ASSERT_FALSE(enabled_rw());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_rw());
+  ASSERT_TRUE(provider_->enabled_rw());
+  ASSERT_TRUE(enabled_rw());
 }
 
 TEST_F(AconfigTest, TestEnabledFixedReadOnlyFlag) {
-  // TODO: change to assertTrue(enabledFixedRo()) when the build supports reading tests/*.values
-  // (currently all flags are assigned the default READ_ONLY + DISABLED)
-  ASSERT_FALSE(com_android_aconfig_test_enabled_fixed_ro());
-  ASSERT_FALSE(provider_->enabled_fixed_ro());
-  ASSERT_FALSE(enabled_fixed_ro());
+  ASSERT_TRUE(com_android_aconfig_test_enabled_fixed_ro());
+  ASSERT_TRUE(provider_->enabled_fixed_ro());
+  ASSERT_TRUE(enabled_fixed_ro());
 }
 
 TEST_F(AconfigTest, OverrideFlagValue) {
@@ -70,14 +64,14 @@
 
 TEST_F(AconfigTest, ResetFlagValue) {
   ASSERT_FALSE(disabled_ro());
-  ASSERT_FALSE(enabled_ro());
-  disabled_ro(true);
-  enabled_ro(true);
-  ASSERT_TRUE(disabled_ro());
   ASSERT_TRUE(enabled_ro());
+  disabled_ro(true);
+  enabled_ro(false);
+  ASSERT_TRUE(disabled_ro());
+  ASSERT_FALSE(enabled_ro());
   reset_flags();
   ASSERT_FALSE(disabled_ro());
-  ASSERT_FALSE(enabled_ro());
+  ASSERT_TRUE(enabled_ro());
 }
 
 int main(int argc, char** argv) {
diff --git a/tools/aconfig/tests/first.values b/tools/aconfig/tests/first.values
index 731ce84..2efb463 100644
--- a/tools/aconfig/tests/first.values
+++ b/tools/aconfig/tests/first.values
@@ -40,3 +40,9 @@
     state: DISABLED
     permission: READ_WRITE
 }
+flag_value {
+    package: "com.android.aconfig.test"
+    name: "enabled_fixed_ro_exported"
+    state: ENABLED
+    permission: READ_ONLY
+}
diff --git a/tools/aconfig/tests/storage_test_1_part_2.aconfig b/tools/aconfig/tests/read_only_test.aconfig
similarity index 68%
rename from tools/aconfig/tests/storage_test_1_part_2.aconfig
rename to tools/aconfig/tests/read_only_test.aconfig
index 5eb0c0c..5eb5056 100644
--- a/tools/aconfig/tests/storage_test_1_part_2.aconfig
+++ b/tools/aconfig/tests/read_only_test.aconfig
@@ -1,4 +1,4 @@
-package: "com.android.aconfig.storage.test_1"
+package: "com.android.aconfig.test"
 container: "system"
 
 flag {
@@ -22,3 +22,11 @@
     bug: ""
     is_fixed_read_only: true
 }
+
+flag {
+    name: "disabled_fixed_ro"
+    namespace: "aconfig_test"
+    description: "This flag is fixed READ_ONLY + DISABLED"
+    bug: ""
+    is_fixed_read_only: true
+}
diff --git a/tools/aconfig/tests/read_only_test.values b/tools/aconfig/tests/read_only_test.values
new file mode 100644
index 0000000..349c7aa
--- /dev/null
+++ b/tools/aconfig/tests/read_only_test.values
@@ -0,0 +1,18 @@
+flag_value {
+    package: "com.android.aconfig.test"
+    name: "disabled_ro"
+    state: DISABLED
+    permission: READ_ONLY
+}
+flag_value {
+    package: "com.android.aconfig.test"
+    name: "enabled_ro"
+    state: ENABLED
+    permission: READ_ONLY
+}
+flag_value {
+    package: "com.android.aconfig.test"
+    name: "enabled_fixed_ro"
+    state: ENABLED
+    permission: READ_ONLY
+}
diff --git a/tools/aconfig/tests/storage_test_1_part_1.aconfig b/tools/aconfig/tests/storage_test_1.aconfig
similarity index 72%
rename from tools/aconfig/tests/storage_test_1_part_1.aconfig
rename to tools/aconfig/tests/storage_test_1.aconfig
index 70462cd..a122c57 100644
--- a/tools/aconfig/tests/storage_test_1_part_1.aconfig
+++ b/tools/aconfig/tests/storage_test_1.aconfig
@@ -15,3 +15,10 @@
     bug: "456"
     is_exported: true
 }
+
+flag {
+    name: "enabled_ro"
+    namespace: "aconfig_test"
+    description: "This flag is ENABLED + READ_ONLY"
+    bug: "abc"
+}
diff --git a/tools/aconfig/tests/storage_test_1_part_2.aconfig b/tools/aconfig/tests/storage_test_4.aconfig
similarity index 64%
copy from tools/aconfig/tests/storage_test_1_part_2.aconfig
copy to tools/aconfig/tests/storage_test_4.aconfig
index 5eb0c0c..333fe09 100644
--- a/tools/aconfig/tests/storage_test_1_part_2.aconfig
+++ b/tools/aconfig/tests/storage_test_4.aconfig
@@ -1,4 +1,4 @@
-package: "com.android.aconfig.storage.test_1"
+package: "com.android.aconfig.storage.test_4"
 container: "system"
 
 flag {
@@ -9,13 +9,6 @@
 }
 
 flag {
-    name: "disabled_ro"
-    namespace: "aconfig_test"
-    description: "This flag is DISABLED + READ_ONLY"
-    bug: "123"
-}
-
-flag {
     name: "enabled_fixed_ro"
     namespace: "aconfig_test"
     description: "This flag is fixed READ_ONLY + ENABLED"
diff --git a/tools/aconfig/tests/test.aconfig b/tools/aconfig/tests/test.aconfig
index 014bced..c11508a 100644
--- a/tools/aconfig/tests/test.aconfig
+++ b/tools/aconfig/tests/test.aconfig
@@ -78,3 +78,12 @@
     bug: "111"
     is_exported: true
 }
+
+flag {
+    name: "enabled_fixed_ro_exported"
+    namespace: "aconfig_test"
+    description: "This flag is fixed ENABLED + READ_ONLY and exported"
+    bug: "111"
+    is_fixed_read_only: true
+    is_exported: true
+}
\ No newline at end of file
diff --git a/tools/aconfig/tests/test_force_read_only.aconfig b/tools/aconfig/tests/test_force_read_only.aconfig
new file mode 100644
index 0000000..05ab0e2
--- /dev/null
+++ b/tools/aconfig/tests/test_force_read_only.aconfig
@@ -0,0 +1,17 @@
+package: "com.android.aconfig.test.forcereadonly"
+container: "system"
+
+flag {
+    name: "fro_exported"
+    namespace: "aconfig_test"
+    description: "This is an exported flag"
+    is_exported: true
+    bug: "888"
+}
+
+flag {
+    name: "fro_rw"
+    namespace: "aconfig_test"
+    description: "This flag is not exported"
+    bug: "777"
+}
diff --git a/tools/compliance/go.mod b/tools/compliance/go.mod
index 1928189..bd04077 100644
--- a/tools/compliance/go.mod
+++ b/tools/compliance/go.mod
@@ -26,4 +26,4 @@
 // Indirect dep from go-cmp
 exclude golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
 
-go 1.18
+go 1.21
diff --git a/tools/metadata/generator.go b/tools/metadata/generator.go
index d328876..b7668be 100644
--- a/tools/metadata/generator.go
+++ b/tools/metadata/generator.go
@@ -77,9 +77,18 @@
 	return string(data)
 }
 
-func writeNewlineToOutputFile(outputFile string) {
+func writeEmptyOutputProto(outputFile string, metadataRule string) {
 	file, err := os.Create(outputFile)
-	data := "\n"
+	if err != nil {
+		log.Fatal(err)
+	}
+	var message proto.Message
+	if metadataRule == "test_spec" {
+		message = &test_spec_proto.TestSpec{}
+	} else if metadataRule == "code_metadata" {
+		message = &code_metadata_proto.CodeMetadata{}
+	}
+	data, err := proto.Marshal(message)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -92,8 +101,8 @@
 }
 
 func processTestSpecProtobuf(
-		filePath string, ownershipMetadataMap *sync.Map, keyLocks *keyToLocksMap,
-		errCh chan error, wg *sync.WaitGroup,
+	filePath string, ownershipMetadataMap *sync.Map, keyLocks *keyToLocksMap,
+	errCh chan error, wg *sync.WaitGroup,
 ) {
 	defer wg.Done()
 
@@ -121,7 +130,7 @@
 				if metadata.GetTrendyTeamId() != existing.GetTrendyTeamId() {
 					errCh <- fmt.Errorf(
 						"Conflicting trendy team IDs found for %s at:\n%s with teamId"+
-								": %s,\n%s with teamId: %s",
+							": %s,\n%s with teamId: %s",
 						key,
 						metadata.GetPath(), metadata.GetTrendyTeamId(), existing.GetPath(),
 						existing.GetTrendyTeamId(),
@@ -147,8 +156,8 @@
 
 // processCodeMetadataProtobuf processes CodeMetadata protobuf files
 func processCodeMetadataProtobuf(
-		filePath string, ownershipMetadataMap *sync.Map, sourceFileMetadataMap *sync.Map, keyLocks *keyToLocksMap,
-		errCh chan error, wg *sync.WaitGroup,
+	filePath string, ownershipMetadataMap *sync.Map, sourceFileMetadataMap *sync.Map, keyLocks *keyToLocksMap,
+	errCh chan error, wg *sync.WaitGroup,
 ) {
 	defer wg.Done()
 
@@ -182,8 +191,8 @@
 				if attributes.TeamID != existing.TeamID && (!attributes.MultiOwnership || !existing.MultiOwnership) {
 					errCh <- fmt.Errorf(
 						"Conflict found for source file %s covered at %s with team ID: %s. Existing team ID: %s and path: %s."+
-								" If multi-ownership is required, multiOwnership should be set to true in all test_spec modules using this target. "+
-								"Multiple-ownership in general is discouraged though as it make infrastructure around android relying on this information pick up a random value when it needs only one.",
+							" If multi-ownership is required, multiOwnership should be set to true in all test_spec modules using this target. "+
+							"Multiple-ownership in general is discouraged though as it make infrastructure around android relying on this information pick up a random value when it needs only one.",
 						srcFile, internalMetadata.GetPath(), attributes.TeamID, existing.TeamID, existing.Path,
 					)
 					srcFileLock.Unlock()
@@ -235,7 +244,7 @@
 	inputFileData := strings.TrimRight(readFileToString(*inputFile), "\n")
 	filePaths := strings.Split(inputFileData, " ")
 	if len(filePaths) == 1 && filePaths[0] == "" {
-		writeNewlineToOutputFile(*outputFile)
+		writeEmptyOutputProto(*outputFile, *rule)
 		return
 	}
 	ownershipMetadataMap := &sync.Map{}
diff --git a/tools/metadata/testdata/generatedEmptyOutputFile.txt b/tools/metadata/testdata/generatedEmptyOutputFile.txt
index 8b13789..e69de29 100644
--- a/tools/metadata/testdata/generatedEmptyOutputFile.txt
+++ b/tools/metadata/testdata/generatedEmptyOutputFile.txt
@@ -1 +0,0 @@
-
diff --git a/tools/perf/benchmarks b/tools/perf/benchmarks
new file mode 100755
index 0000000..e188858
--- /dev/null
+++ b/tools/perf/benchmarks
@@ -0,0 +1,710 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 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.
+
+import sys
+if __name__ == "__main__":
+    sys.dont_write_bytecode = True
+
+import argparse
+import dataclasses
+import datetime
+import json
+import os
+import pathlib
+import random
+import re
+import shutil
+import subprocess
+import time
+import uuid
+
+import pretty
+import utils
+
+
+class FatalError(Exception):
+    def __init__(self):
+        pass
+
+
+class OptionsError(Exception):
+    def __init__(self, message):
+        self.message = message
+
+
+@dataclasses.dataclass(frozen=True)
+class Lunch:
+    "Lunch combination"
+
+    target_product: str
+    "TARGET_PRODUCT"
+
+    target_release: str
+    "TARGET_RELEASE"
+
+    target_build_variant: str
+    "TARGET_BUILD_VARIANT"
+
+    def ToDict(self):
+        return {
+            "TARGET_PRODUCT": self.target_product,
+            "TARGET_RELEASE": self.target_release,
+            "TARGET_BUILD_VARIANT": self.target_build_variant,
+        }
+
+    def Combine(self):
+        return f"{self.target_product}-{self.target_release}-{self.target_build_variant}"
+
+
+@dataclasses.dataclass(frozen=True)
+class Change:
+    "A change that we make to the tree, and how to undo it"
+    label: str
+    "String to print in the log when the change is made"
+
+    change: callable
+    "Function to change the source tree"
+
+    undo: callable
+    "Function to revert the source tree to its previous condition in the most minimal way possible."
+
+
+@dataclasses.dataclass(frozen=True)
+class Benchmark:
+    "Something we measure"
+
+    id: str
+    "Short ID for the benchmark, for the command line"
+
+    title: str
+    "Title for reports"
+
+    change: Change
+    "Source tree modification for the benchmark that will be measured"
+
+    modules: list[str]
+    "Build modules to build on soong command line"
+
+    preroll: int
+    "Number of times to run the build command to stabilize"
+
+    postroll: int
+    "Number of times to run the build command after reverting the action to stabilize"
+
+
+@dataclasses.dataclass(frozen=True)
+class FileSnapshot:
+    "Snapshot of a file's contents."
+
+    filename: str
+    "The file that was snapshottened"
+
+    contents: str
+    "The contents of the file"
+
+    def write(self):
+        "Write the contents back to the file"
+        with open(self.filename, "w") as f:
+            f.write(self.contents)
+
+
+def Snapshot(filename):
+    """Return a FileSnapshot with the file's current contents."""
+    with open(filename) as f:
+        contents = f.read()
+    return FileSnapshot(filename, contents)
+
+
+def Clean():
+    """Remove the out directory."""
+    def remove_out():
+        if os.path.exists("out"):
+            shutil.rmtree("out")
+    return Change(label="Remove out", change=remove_out, undo=lambda: None)
+
+
+def NoChange():
+    """No change to the source tree."""
+    return Change(label="No change", change=lambda: None, undo=lambda: None)
+
+
+def Create(filename):
+    "Create an action to create `filename`. The parent directory must exist."
+    def create():
+        with open(filename, "w") as f:
+            pass
+    def delete():
+        os.remove(filename)
+    return Change(
+                label=f"Create {filename}",
+                change=create,
+                undo=delete,
+            )
+
+
+def Modify(filename, contents, before=None):
+    """Create an action to modify `filename` by appending the result of `contents`
+    before the last instances of `before` in the file.
+
+    Raises an error if `before` doesn't appear in the file.
+    """
+    orig = Snapshot(filename)
+    if before:
+        index = orig.contents.rfind(before)
+        if index < 0:
+            report_error(f"{filename}: Unable to find string '{before}' for modify operation.")
+            raise FatalError()
+    else:
+        index = len(orig.contents)
+    modified = FileSnapshot(filename, orig.contents[:index] + contents() + orig.contents[index:])
+    if False:
+        print(f"Modify: {filename}")
+        x = orig.contents.replace("\n", "\n   ORIG")
+        print(f"   ORIG {x}")
+        x = modified.contents.replace("\n", "\n   MODIFIED")
+        print(f"   MODIFIED {x}")
+
+    return Change(
+            label="Modify " + filename,
+            change=lambda: modified.write(),
+            undo=lambda: orig.write()
+        )
+
+def AddJavaField(filename, prefix):
+    return Modify(filename,
+                  lambda: f"{prefix} static final int BENCHMARK = {random.randint(0, 1000000)};\n",
+                  before="}")
+
+
+def Comment(prefix, suffix=""):
+    return lambda: prefix + " " + str(uuid.uuid4()) + suffix
+
+
+class BenchmarkReport():
+    "Information about a run of the benchmark"
+
+    lunch: Lunch
+    "lunch combo"
+
+    benchmark: Benchmark
+    "The benchmark object."
+
+    iteration: int
+    "Which iteration of the benchmark"
+
+    log_dir: str
+    "Path the the log directory, relative to the root of the reports directory"
+
+    preroll_duration_ns: [int]
+    "Durations of the in nanoseconds."
+
+    duration_ns: int
+    "Duration of the measured portion of the benchmark in nanoseconds."
+
+    postroll_duration_ns: [int]
+    "Durations of the postrolls in nanoseconds."
+
+    complete: bool
+    "Whether the benchmark made it all the way through the postrolls."
+
+    def __init__(self, lunch, benchmark, iteration, log_dir):
+        self.lunch = lunch
+        self.benchmark = benchmark
+        self.iteration = iteration
+        self.log_dir = log_dir
+        self.preroll_duration_ns = []
+        self.duration_ns = -1
+        self.postroll_duration_ns = []
+        self.complete = False
+
+    def ToDict(self):
+        return {
+            "lunch": self.lunch.ToDict(),
+            "id": self.benchmark.id,
+            "title": self.benchmark.title,
+            "modules": self.benchmark.modules,
+            "change": self.benchmark.change.label,
+            "iteration": self.iteration,
+            "log_dir": self.log_dir,
+            "preroll_duration_ns": self.preroll_duration_ns,
+            "duration_ns": self.duration_ns,
+            "postroll_duration_ns": self.postroll_duration_ns,
+            "complete": self.complete,
+        }
+
+class Runner():
+    """Runs the benchmarks."""
+
+    def __init__(self, options):
+        self._options = options
+        self._reports = []
+        self._complete = False
+
+    def Run(self):
+        """Run all of the user-selected benchmarks."""
+        # Clean out the log dir or create it if necessary
+        prepare_log_dir(self._options.LogDir())
+
+        try:
+            for lunch in self._options.Lunches():
+                print(lunch)
+                for benchmark in self._options.Benchmarks():
+                    for iteration in range(self._options.Iterations()):
+                        self._run_benchmark(lunch, benchmark, iteration)
+            self._complete = True
+        finally:
+            self._write_summary()
+
+
+    def _run_benchmark(self, lunch, benchmark, iteration):
+        """Run a single benchmark."""
+        benchmark_log_subdir = self._log_dir(lunch, benchmark, iteration)
+        benchmark_log_dir = self._options.LogDir().joinpath(benchmark_log_subdir)
+
+        sys.stderr.write(f"STARTING BENCHMARK: {benchmark.id}\n")
+        sys.stderr.write(f"             lunch: {lunch.Combine()}\n")
+        sys.stderr.write(f"         iteration: {iteration}\n")
+        sys.stderr.write(f" benchmark_log_dir: {benchmark_log_dir}\n")
+
+        report = BenchmarkReport(lunch, benchmark, iteration, benchmark_log_subdir)
+        self._reports.append(report)
+
+        # Preroll builds
+        for i in range(benchmark.preroll):
+            ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"pre_{i}"), benchmark.modules)
+            report.preroll_duration_ns.append(ns)
+
+        sys.stderr.write(f"PERFORMING CHANGE: {benchmark.change.label}\n")
+        if not self._options.DryRun():
+            benchmark.change.change()
+        try:
+
+            # Measured build
+            ns = self._run_build(lunch, benchmark_log_dir.joinpath("measured"), benchmark.modules)
+            report.duration_ns = ns
+
+            dist_one = self._options.DistOne()
+            if dist_one:
+                # If we're disting just one benchmark, save the logs and we can stop here.
+                self._dist(dist_one)
+            else:
+                # Postroll builds
+                for i in range(benchmark.preroll):
+                    ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"post_{i}"),
+                                         benchmark.modules)
+                    report.postroll_duration_ns.append(ns)
+
+        finally:
+            # Always undo, even if we crashed or the build failed and we stopped.
+            sys.stderr.write(f"UNDOING CHANGE: {benchmark.change.label}\n")
+            if not self._options.DryRun():
+                benchmark.change.undo()
+
+        self._write_summary()
+        sys.stderr.write(f"FINISHED BENCHMARK: {benchmark.id}\n")
+
+    def _log_dir(self, lunch, benchmark, iteration):
+        """Construct the log directory fir a benchmark run."""
+        path = f"{lunch.Combine()}/{benchmark.id}"
+        # Zero pad to the correct length for correct alpha sorting
+        path += ("/%0" + str(len(str(self._options.Iterations()))) + "d") % iteration
+        return path
+
+    def _run_build(self, lunch, build_log_dir, modules):
+        """Builds the modules.  Saves interesting log files to log_dir.  Raises FatalError
+        if the build fails.
+        """
+        sys.stderr.write(f"STARTING BUILD {modules}\n")
+
+        before_ns = time.perf_counter_ns()
+        if not self._options.DryRun():
+            cmd = [
+                "build/soong/soong_ui.bash",
+                "--build-mode",
+                "--all-modules",
+                f"--dir={self._options.root}",
+                "--skip-metrics-upload",
+            ] + modules
+            env = dict(os.environ)
+            env["TARGET_PRODUCT"] = lunch.target_product
+            env["TARGET_RELEASE"] = lunch.target_release
+            env["TARGET_BUILD_VARIANT"] = lunch.target_build_variant
+            returncode = subprocess.call(cmd, env=env)
+            if returncode != 0:
+                report_error(f"Build failed: {' '.join(cmd)}")
+                raise FatalError()
+
+        after_ns = time.perf_counter_ns()
+
+        # TODO: Copy some log files.
+
+        sys.stderr.write(f"FINISHED BUILD {modules}\n")
+
+        return after_ns - before_ns
+
+    def _dist(self, dist_dir):
+        out_dir = pathlib.Path("out")
+        dest_dir = pathlib.Path(dist_dir).joinpath("logs")
+        os.makedirs(dest_dir, exist_ok=True)
+        basenames = [
+            "build.trace.gz",
+            "soong.log",
+            "soong_build_metrics.pb",
+            "soong_metrics",
+        ]
+        for base in basenames:
+            src = out_dir.joinpath(base)
+            if src.exists():
+                sys.stderr.write(f"DIST: copied {src} to {dest_dir}\n")
+                shutil.copy(src, dest_dir)
+
+    def _write_summary(self):
+        # Write the results, even if the build failed or we crashed, including
+        # whether we finished all of the benchmarks.
+        data = {
+            "start_time": self._options.Timestamp().isoformat(),
+            "branch": self._options.Branch(),
+            "tag": self._options.Tag(),
+            "benchmarks": [report.ToDict() for report in self._reports],
+            "complete": self._complete,
+        }
+        with open(self._options.LogDir().joinpath("summary.json"), "w", encoding="utf-8") as f:
+            json.dump(data, f, indent=2, sort_keys=True)
+
+
+def benchmark_table(benchmarks):
+    rows = [("ID", "DESCRIPTION", "REBUILD"),]
+    rows += [(benchmark.id, benchmark.title, " ".join(benchmark.modules)) for benchmark in
+             benchmarks]
+    return rows
+
+
+def prepare_log_dir(directory):
+    if os.path.exists(directory):
+        # If it exists and isn't a directory, fail.
+        if not os.path.isdir(directory):
+            report_error(f"Log directory already exists but isn't a directory: {directory}")
+            raise FatalError()
+        # Make sure the directory is empty. Do this rather than deleting it to handle
+        # symlinks cleanly.
+        for filename in os.listdir(directory):
+            entry = os.path.join(directory, filename)
+            if os.path.isdir(entry):
+                shutil.rmtree(entry)
+            else:
+                os.unlink(entry)
+    else:
+        # Create it
+        os.makedirs(directory)
+
+
+class Options():
+    def __init__(self):
+        self._had_error = False
+
+        # Wall time clock when we started
+        self._timestamp = datetime.datetime.now(datetime.timezone.utc)
+
+        # Move to the root of the tree right away. Everything must happen from there.
+        self.root = utils.get_root()
+        if not self.root:
+            report_error("Unable to find root of tree from cwd.")
+            raise FatalError()
+        os.chdir(self.root)
+
+        # Initialize the Benchmarks. Note that this pre-loads all of the files, etc.
+        # Doing all that here forces us to fail fast if one of them can't load a required
+        # file, at the cost of a small startup speed. Don't make this do something slow
+        # like scan the whole tree.
+        self._init_benchmarks()
+
+        # Argument parsing
+        epilog = f"""
+benchmarks:
+{pretty.FormatTable(benchmark_table(self._benchmarks), prefix="  ")}
+"""
+
+        parser = argparse.ArgumentParser(
+                prog="benchmarks",
+                allow_abbrev=False, # Don't let people write unsupportable scripts.
+                formatter_class=argparse.RawDescriptionHelpFormatter,
+                epilog=epilog,
+                description="Run build system performance benchmarks.")
+        self.parser = parser
+
+        parser.add_argument("--log-dir",
+                            help="Directory for logs. Default is $TOP/../benchmarks/.")
+        parser.add_argument("--dated-logs", action="store_true",
+                            help="Append timestamp to log dir.")
+        parser.add_argument("-n", action="store_true", dest="dry_run",
+                            help="Dry run. Don't run the build commands but do everything else.")
+        parser.add_argument("--tag",
+                            help="Variant of the run, for when there are multiple perf runs.")
+        parser.add_argument("--lunch", nargs="*",
+                            help="Lunch combos to test")
+        parser.add_argument("--iterations", type=int, default=1,
+                            help="Number of iterations of each test to run.")
+        parser.add_argument("--branch", type=str,
+                            help="Specify branch. Otherwise a guess will be made based on repo.")
+        parser.add_argument("--benchmark", nargs="*", default=[b.id for b in self._benchmarks],
+                            metavar="BENCHMARKS",
+                            help="Benchmarks to run.  Default suite will be run if omitted.")
+        parser.add_argument("--dist-one", action="store_true",
+                            help="Copy logs and metrics to the given dist dir. Requires that only"
+                                + " one benchmark be supplied. Postroll steps will be skipped.")
+
+        self._args = parser.parse_args()
+
+        self._branch = self._branch()
+        self._log_dir = self._log_dir()
+        self._lunches = self._lunches()
+
+        # Validate the benchmark ids
+        all_ids = [benchmark.id for benchmark in self._benchmarks]
+        bad_ids = [id for id in self._args.benchmark if id not in all_ids]
+        if bad_ids:
+            for id in bad_ids:
+                self._error(f"Invalid benchmark: {id}")
+
+        # --dist-one requires that only one benchmark be supplied
+        if self._args.dist_one and len(self.Benchmarks()) != 1:
+            self._error("--dist-one requires that exactly one --benchmark.")
+
+        if self._had_error:
+            raise FatalError()
+
+    def Timestamp(self):
+        return self._timestamp
+
+    def _branch(self):
+        """Return the branch, either from the command line or by guessing from repo."""
+        if self._args.branch:
+            return self._args.branch
+        try:
+            branch = subprocess.check_output(f"cd {self.root}/.repo/manifests"
+                        + " && git rev-parse --abbrev-ref --symbolic-full-name @{u}",
+                    shell=True, encoding="utf-8")
+            return branch.strip().split("/")[-1]
+        except subprocess.CalledProcessError as ex:
+            report_error("Can't get branch from .repo dir. Specify --branch argument")
+            report_error(str(ex))
+            raise FatalError()
+
+    def Branch(self):
+        return self._branch
+
+    def _log_dir(self):
+        "The log directory to use, based on the current options"
+        if self._args.log_dir:
+            d = pathlib.Path(self._args.log_dir).resolve().absolute()
+        else:
+            d = self.root.joinpath("..", utils.DEFAULT_REPORT_DIR)
+        if self._args.dated_logs:
+            d = d.joinpath(self._timestamp.strftime('%Y-%m-%d'))
+        d = d.joinpath(self._branch)
+        if self._args.tag:
+            d = d.joinpath(self._args.tag)
+        return d.resolve().absolute()
+
+    def LogDir(self):
+        return self._log_dir
+
+    def Benchmarks(self):
+        return [b for b in self._benchmarks if b.id in self._args.benchmark]
+
+    def Tag(self):
+        return self._args.tag
+
+    def DryRun(self):
+        return self._args.dry_run
+
+    def _lunches(self):
+        def parse_lunch(lunch):
+            parts = lunch.split("-")
+            if len(parts) != 3:
+                raise OptionsError(f"Invalid lunch combo: {lunch}")
+            return Lunch(parts[0], parts[1], parts[2])
+        # If they gave lunch targets on the command line use that
+        if self._args.lunch:
+            result = []
+            # Split into Lunch objects
+            for lunch in self._args.lunch:
+                try:
+                    result.append(parse_lunch(lunch))
+                except OptionsError as ex:
+                    self._error(ex.message)
+            return result
+        # Use whats in the environment
+        product = os.getenv("TARGET_PRODUCT")
+        release = os.getenv("TARGET_RELEASE")
+        variant = os.getenv("TARGET_BUILD_VARIANT")
+        if (not product) or (not release) or (not variant):
+            # If they didn't give us anything, fail rather than guessing. There's no good
+            # default for AOSP.
+            self._error("No lunch combo specified. Either pass --lunch argument or run lunch.")
+            return []
+        return [Lunch(product, release, variant),]
+
+    def Lunches(self):
+        return self._lunches
+
+    def Iterations(self):
+        return self._args.iterations
+
+    def DistOne(self):
+        return self._args.dist_one
+
+    def _init_benchmarks(self):
+        """Initialize the list of benchmarks."""
+        # Assumes that we've already chdired to the root of the tree.
+        self._benchmarks = [
+            Benchmark(id="full",
+                      title="Full build",
+                      change=Clean(),
+                      modules=["droid"],
+                      preroll=0,
+                      postroll=3,
+                      ),
+            Benchmark(id="nochange",
+                      title="No change",
+                      change=NoChange(),
+                      modules=["droid"],
+                      preroll=2,
+                      postroll=3,
+                      ),
+            Benchmark(id="unreferenced",
+                      title="Create unreferenced file",
+                      change=Create("bionic/unreferenced.txt"),
+                      modules=["droid"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="modify_bp",
+                      title="Modify Android.bp",
+                      change=Modify("bionic/libc/Android.bp", Comment("//")),
+                      modules=["droid"],
+                      preroll=1,
+                      postroll=3,
+                      ),
+            Benchmark(id="modify_stdio",
+                      title="Modify stdio.cpp",
+                      change=Modify("bionic/libc/stdio/stdio.cpp", Comment("//")),
+                      modules=["libc"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="modify_adbd",
+                      title="Modify adbd",
+                      change=Modify("packages/modules/adb/daemon/main.cpp", Comment("//")),
+                      modules=["adbd"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="services_private_field",
+                      title="Add private field to ActivityManagerService.java",
+                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+                                          "private"),
+                      modules=["services"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="services_public_field",
+                      title="Add public field to ActivityManagerService.java",
+                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+                                          "/** @hide */ public"),
+                      modules=["services"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="services_api",
+                      title="Add API to ActivityManagerService.javaa",
+                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+                                          "@android.annotation.SuppressLint(\"UnflaggedApi\") public"),
+                      modules=["services"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="framework_private_field",
+                      title="Add private field to Settings.java",
+                      change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+                                          "private"),
+                      modules=["framework-minus-apex"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="framework_public_field",
+                      title="Add public field to Settings.java",
+                      change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+                                          "/** @hide */ public"),
+                      modules=["framework-minus-apex"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="framework_api",
+                      title="Add API to Settings.java",
+                      change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+                                          "@android.annotation.SuppressLint(\"UnflaggedApi\") public"),
+                      modules=["framework-minus-apex"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="modify_framework_resource",
+                      title="Modify framework resource",
+                      change=Modify("frameworks/base/core/res/res/values/config.xml",
+                                    lambda: str(uuid.uuid4()),
+                                    before="</string>"),
+                      modules=["framework-minus-apex"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="add_framework_resource",
+                      title="Add framework resource",
+                      change=Modify("frameworks/base/core/res/res/values/config.xml",
+                                    lambda: f"<string name=\"BENCHMARK\">{uuid.uuid4()}</string>",
+                                    before="</resources>"),
+                      modules=["framework-minus-apex"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+            Benchmark(id="add_systemui_field",
+                      title="Add SystemUI field",
+                      change=AddJavaField("frameworks/base/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java",
+                                    "public"),
+                      modules=["SystemUI"],
+                      preroll=1,
+                      postroll=2,
+                      ),
+        ]
+
+    def _error(self, message):
+        report_error(message)
+        self._had_error = True
+
+
+def report_error(message):
+    sys.stderr.write(f"error: {message}\n")
+
+
+def main(argv):
+    try:
+        options = Options()
+        runner = Runner(options)
+        runner.Run()
+    except FatalError:
+        sys.stderr.write(f"FAILED\n")
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/tools/perf/format_benchmarks b/tools/perf/format_benchmarks
new file mode 100755
index 0000000..845d73f
--- /dev/null
+++ b/tools/perf/format_benchmarks
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 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.
+
+import sys
+if __name__ == "__main__":
+    sys.dont_write_bytecode = True
+
+import argparse
+import dataclasses
+import datetime
+import json
+import os
+import pathlib
+import statistics
+import zoneinfo
+
+import pretty
+import utils
+
+# TODO:
+# - Flag if the last postroll build was more than 15 seconds or something. That's
+#   an indicator that something is amiss.
+# - Add a mode to print all of the values for multi-iteration runs
+# - Add a flag to reorder the tags
+# - Add a flag to reorder the headers in order to show grouping more clearly.
+
+
+def FindSummaries(args):
+    def find_summaries(directory):
+        return [str(p.resolve()) for p in pathlib.Path(directory).glob("**/summary.json")]
+    if not args:
+        # If they didn't give an argument, use the default dir
+        root = utils.get_root()
+        if not root:
+            return []
+        return find_summaries(root.joinpath("..", utils.DEFAULT_REPORT_DIR))
+    results = list()
+    for arg in args:
+        if os.path.isfile(arg):
+            # If it's a file add that
+            results.append(arg)
+        elif os.path.isdir(arg):
+            # If it's a directory, find all of the files there
+            results += find_summaries(arg)
+        else:
+            sys.stderr.write(f"Invalid summary argument: {arg}\n")
+            sys.exit(1)
+    return sorted(list(results))
+
+
+def LoadSummary(filename):
+    with open(filename) as f:
+        return json.load(f)
+
+# Columns:
+#   Date
+#   Branch
+#   Tag
+#   --
+#   Lunch
+# Rows:
+#   Benchmark
+
+def lunch_str(d):
+    "Convert a lunch dict to a string"
+    return f"{d['TARGET_PRODUCT']}-{d['TARGET_RELEASE']}-{d['TARGET_BUILD_VARIANT']}"
+
+def group_by(l, key):
+    "Return a list of tuples, grouped by key, sorted by key"
+    result = {}
+    for item in l:
+        result.setdefault(key(item), []).append(item)
+    return [(k, v) for k, v in result.items()]
+
+
+class Table:
+    def __init__(self):
+        self._data = {}
+        self._rows = []
+        self._cols = []
+
+    def Set(self, column_key, row_key, data):
+        self._data[(column_key, row_key)] = data
+        if not column_key in self._cols:
+            self._cols.append(column_key)
+        if not row_key in self._rows:
+            self._rows.append(row_key)
+
+    def Write(self, out):
+        table = []
+        # Expand the column items
+        for row in zip(*self._cols):
+            if row.count(row[0]) == len(row):
+                continue
+            table.append([""] + [col for col in row])
+        if table:
+            table.append(pretty.SEPARATOR)
+        # Populate the data
+        for row in self._rows:
+            table.append([str(row)] + [str(self._data.get((col, row), "")) for col in self._cols])
+        out.write(pretty.FormatTable(table))
+
+
+def format_duration_sec(ns):
+    "Format a duration in ns to second precision"
+    sec = round(ns / 1000000000)
+    h, sec = divmod(sec, 60*60)
+    m, sec = divmod(sec, 60)
+    result = ""
+    if h > 0:
+        result += f"{h:2d}h "
+    if h > 0 or m > 0:
+        result += f"{m:2d}m "
+    return result + f"{sec:2d}s"
+
+
+def main(argv):
+    parser = argparse.ArgumentParser(
+            prog="format_benchmarks",
+            allow_abbrev=False, # Don't let people write unsupportable scripts.
+            description="Print analysis tables for benchmarks")
+
+    parser.add_argument("--tags", nargs="*",
+                        help="The tags to print, in order.")
+
+    parser.add_argument("summaries", nargs="*",
+                        help="A summary.json file or a directory in which to look for summaries.")
+
+    args = parser.parse_args()
+
+    # Load the summaries
+    summaries = [(s, LoadSummary(s)) for s in FindSummaries(args.summaries)]
+
+    # Convert to MTV time
+    for filename, s in summaries:
+        dt = datetime.datetime.fromisoformat(s["start_time"])
+        dt = dt.astimezone(zoneinfo.ZoneInfo("America/Los_Angeles"))
+        s["datetime"] = dt
+        s["date"] = datetime.date(dt.year, dt.month, dt.day)
+
+    # Filter out tags we don't want
+    if args.tags:
+        summaries = [(f, s) for f, s in summaries if s.get("tag", "") in args.tags]
+
+    # If they supplied tags, sort in that order, otherwise sort by tag
+    if args.tags:
+        tagsort = lambda tag: args.tags.index(tag)
+    else:
+        tagsort = lambda tag: tag
+
+    # Sort the summaries
+    summaries.sort(key=lambda s: (s[1]["date"], s[1]["branch"], tagsort(s[1]["tag"])))
+
+    # group the benchmarks by column and iteration
+    def bm_key(b):
+        return (
+            lunch_str(b["lunch"]),
+        )
+    for filename, summary in summaries:
+        summary["columns"] = [(key, group_by(bms, lambda b: b["id"])) for key, bms
+                              in group_by(summary["benchmarks"], bm_key)]
+
+    # Build the table
+    table = Table()
+    for filename, summary in summaries:
+        for key, column in summary["columns"]:
+            for id, cell in column:
+                duration_ns = statistics.median([b["duration_ns"] for b in cell])
+                table.Set(tuple([summary["date"].strftime("%Y-%m-%d"),
+                                 summary["branch"],
+                                 summary["tag"]]
+                                + list(key)),
+                          cell[0]["title"], format_duration_sec(duration_ns))
+
+    table.Write(sys.stdout)
+
+if __name__ == "__main__":
+    main(sys.argv)
+
diff --git a/tools/perf/pretty.py b/tools/perf/pretty.py
new file mode 100644
index 0000000..1b59098
--- /dev/null
+++ b/tools/perf/pretty.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2023 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.
+
+# Formatting utilities
+
+class Sentinel():
+    pass
+
+SEPARATOR = Sentinel()
+
+def FormatTable(data, prefix=""):
+    """Pretty print a table.
+
+    Prefixes each row with `prefix`.
+    """
+    if not data:
+        return ""
+    widths = [max([len(x) if x else 0 for x in col]) for col
+              in zip(*[d for d in data if not isinstance(d, Sentinel)])]
+    result = ""
+    colsep = "  "
+    for row in data:
+        result += prefix
+        if row == SEPARATOR:
+            for w in widths:
+                result += "-" * w
+                result += colsep
+            result += "\n"
+        else:
+            for i in range(len(row)):
+                cell = row[i] if row[i] else ""
+                if i != 0:
+                    result += " " * (widths[i] - len(cell))
+                result += cell
+                if i == 0:
+                    result += " " * (widths[i] - len(cell))
+                result += colsep
+            result += "\n"
+    return result
+
+
diff --git a/tools/perf/utils.py b/tools/perf/utils.py
new file mode 100644
index 0000000..08e393f
--- /dev/null
+++ b/tools/perf/utils.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2023 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.
+
+import os
+import pathlib
+
+DEFAULT_REPORT_DIR = "benchmarks"
+
+def get_root():
+    top_dir = os.environ.get("ANDROID_BUILD_TOP")
+    if top_dir:
+        return pathlib.Path(top_dir).resolve()
+    d = pathlib.Path.cwd()
+    while True:
+        if d.joinpath("build", "soong", "soong_ui.bash").exists():
+            return d.resolve().absolute()
+        d = d.parent
+        if d == pathlib.Path("/"):
+            return None
diff --git a/tools/rbcrun/go.mod b/tools/rbcrun/go.mod
index 5ae2972..6e99ce9 100644
--- a/tools/rbcrun/go.mod
+++ b/tools/rbcrun/go.mod
@@ -4,4 +4,4 @@
 
 replace go.starlark.net => ../../../../external/starlark-go
 
-go 1.15
+go 1.21
diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py
index 4a5facd..29042a5 100755
--- a/tools/releasetools/ota_from_target_files.py
+++ b/tools/releasetools/ota_from_target_files.py
@@ -259,6 +259,9 @@
 
   --vabc_cow_version
       Specify the VABC cow version to be used
+
+  --compression_factor
+      Specify the maximum block size to be compressed at once during OTA. supported options: 4k, 8k, 16k, 32k, 64k, 128k
 """
 
 from __future__ import print_function
@@ -331,6 +334,7 @@
 OPTIONS.security_patch_level = None
 OPTIONS.max_threads = None
 OPTIONS.vabc_cow_version = None
+OPTIONS.compression_factor = None
 
 
 POSTINSTALL_CONFIG = 'META/postinstall_config.txt'
@@ -393,17 +397,6 @@
   """
   return ModifyKeyvalueList(content, "virtual_ab_compression_method", algo)
 
-def SetVABCCowVersion(content, cow_version):
-  """ Update virtual_ab_cow_version in dynamic_partitions_info.txt
-  Args:
-    content: The string content of dynamic_partitions_info.txt
-    algo: The cow version be used for VABC. See
-          https://cs.android.com/android/platform/superproject/main/+/main:system/core/fs_mgr/libsnapshot/include/libsnapshot/cow_format.h;l=36
-  Returns:
-    Updated content of dynamic_partitions_info.txt , updated cow version
-  """
-  return ModifyKeyvalueList(content, "virtual_ab_cow_version", cow_version)
-
 
 def UpdatesInfoForSpecialUpdates(content, partitions_filter,
                                  delete_keys=None):
@@ -1020,6 +1013,8 @@
         target_file, vabc_compression_param)
   if OPTIONS.vabc_cow_version:
     target_file = ModifyTargetFilesDynamicPartitionInfo(target_file, "virtual_ab_cow_version", OPTIONS.vabc_cow_version)
+  if OPTIONS.compression_factor:
+    target_file = ModifyTargetFilesDynamicPartitionInfo(target_file, "virtual_ab_compression_factor", OPTIONS.compression_factor)
   if OPTIONS.skip_postinstall:
     target_file = GetTargetFilesZipWithoutPostinstallConfig(target_file)
   # Target_file may have been modified, reparse ab_partitions
@@ -1038,7 +1033,11 @@
   partition_timestamps_flags = []
   # Enforce a max timestamp this payload can be applied on top of.
   if OPTIONS.downgrade:
-    max_timestamp = source_info.GetBuildProp("ro.build.date.utc")
+    # When generating ota between merged target-files, partition build date can
+    # decrease in target, at the same time as ro.build.date.utc increases,
+    # so always pick largest value.
+    max_timestamp = max(source_info.GetBuildProp("ro.build.date.utc"),
+        str(metadata.postcondition.timestamp))
     partition_timestamps_flags = GeneratePartitionTimestampFlagsDowngrade(
         metadata.precondition.partition_state,
         metadata.postcondition.partition_state
@@ -1276,6 +1275,13 @@
       else:
         raise ValueError("Cannot parse value %r for option %r - only "
                          "integers are allowed." % (a, o))
+    elif o in ("--compression_factor"):
+        values = ["4k", "8k", "16k", "32k", "64k", "128k"]
+        if a[:-1].isdigit() and a in values and a.endswith("k"):
+            OPTIONS.compression_factor = str(int(a[:-1]) * 1024)
+        else:
+            raise ValueError("Please specify value from following options: 4k, 8k, 16k, 32k, 64k, 128k")
+
     elif o == "--vabc_cow_version":
       if a.isdigit():
         OPTIONS.vabc_cow_version = a
@@ -1331,6 +1337,7 @@
                                  "security_patch_level=",
                                  "max_threads=",
                                  "vabc_cow_version=",
+                                 "compression_factor=",
                              ], extra_option_handler=[option_handler, payload_signer.signer_options])
   common.InitLogging()
 
diff --git a/tools/releasetools/ota_utils.py b/tools/releasetools/ota_utils.py
index ddd2d36..048a497 100644
--- a/tools/releasetools/ota_utils.py
+++ b/tools/releasetools/ota_utils.py
@@ -364,26 +364,66 @@
   # Only incremental OTAs are allowed to reach here.
   assert OPTIONS.incremental_source is not None
 
+  # used for logging upon errors
+  log_downgrades = []
+  log_upgrades = []
+
   post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
   pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
-  is_downgrade = int(post_timestamp) < int(pre_timestamp)
+  if int(post_timestamp) < int(pre_timestamp):
+    logger.info(f"ro.build.date.utc pre timestamp: {pre_timestamp}, "
+                f"post timestamp: {post_timestamp}. Downgrade detected.")
+    log_downgrades.append(f"ro.build.date.utc pre: {pre_timestamp} post: {post_timestamp}")
+  else:
+    logger.info(f"ro.build.date.utc pre timestamp: {pre_timestamp}, "
+                f"post timestamp: {post_timestamp}.")
+    log_upgrades.append(f"ro.build.date.utc pre: {pre_timestamp} post: {post_timestamp}")
+
+  # When merging system and vendor target files, it is not enough
+  # to check ro.build.date.utc, the timestamp for each partition must
+  # be checked.
+  if source_info.is_ab:
+    ab_partitions = set(source_info.get("ab_partitions"))
+    for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions):
+
+      partition_prop = source_info.get('{}.build.prop'.format(partition))
+      # Skip if the partition is missing, or it doesn't have a build.prop
+      if not partition_prop or not partition_prop.build_props:
+        continue
+      partition_prop = target_info.get('{}.build.prop'.format(partition))
+      # Skip if the partition is missing, or it doesn't have a build.prop
+      if not partition_prop or not partition_prop.build_props:
+        continue
+
+      post_timestamp = target_info.GetPartitionBuildProp(
+        'ro.build.date.utc', partition)
+      pre_timestamp = source_info.GetPartitionBuildProp(
+        'ro.build.date.utc', partition)
+      if int(post_timestamp) < int(pre_timestamp):
+        logger.info(f"Partition {partition} pre timestamp: {pre_timestamp}, "
+                    f"post time: {post_timestamp}. Downgrade detected.")
+        log_downgrades.append(f"{partition} pre: {pre_timestamp} post: {post_timestamp}")
+      else:
+        logger.info(f"Partition {partition} pre timestamp: {pre_timestamp}, "
+                    f"post timestamp: {post_timestamp}.")
+        log_upgrades.append(f"{partition} pre: {pre_timestamp} post: {post_timestamp}")
 
   if OPTIONS.spl_downgrade:
     metadata_proto.spl_downgrade = True
 
   if OPTIONS.downgrade:
-    if not is_downgrade:
+    if len(log_downgrades) == 0:
       raise RuntimeError(
           "--downgrade or --override_timestamp specified but no downgrade "
-          "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
+          "detected. Current values for ro.build.date.utc: " + ', '.join(log_upgrades))
     metadata_proto.downgrade = True
   else:
-    if is_downgrade:
+    if len(log_downgrades) != 0:
       raise RuntimeError(
-          "Downgrade detected based on timestamp check: pre: %s, post: %s. "
+          "Downgrade detected based on timestamp check in ro.build.date.utc. "
           "Need to specify --override_timestamp OR --downgrade to allow "
-          "building the incremental." % (pre_timestamp, post_timestamp))
-
+          "building the incremental. Downgrades detected for: "
+          + ', '.join(log_downgrades))
 
 def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
   """Returns a set of build info objects that may exist during runtime."""
diff --git a/tools/releasetools/test_ota_from_target_files.py b/tools/releasetools/test_ota_from_target_files.py
index ad0f7a8..d1e76b9 100644
--- a/tools/releasetools/test_ota_from_target_files.py
+++ b/tools/releasetools/test_ota_from_target_files.py
@@ -163,6 +163,20 @@
       'oem_fingerprint_properties': 'ro.product.device ro.product.brand',
   }
 
+  TEST_TARGET_VENDOR_INFO_DICT = common.PartitionBuildProps.FromDictionary(
+    'vendor', {
+      'ro.vendor.build.date.utc' : '87654321',
+      'ro.product.vendor.device':'vendor-device',
+      'ro.vendor.build.fingerprint': 'build-fingerprint-vendor'}
+  )
+
+  TEST_SOURCE_VENDOR_INFO_DICT = common.PartitionBuildProps.FromDictionary(
+    'vendor', {
+      'ro.vendor.build.date.utc' : '12345678',
+      'ro.product.vendor.device':'vendor-device',
+      'ro.vendor.build.fingerprint': 'build-fingerprint-vendor'}
+  )
+
   def setUp(self):
     self.testdata_dir = test_utils.get_testdata_dir()
     self.assertTrue(os.path.exists(self.testdata_dir))
@@ -351,6 +365,13 @@
          source_info['build.prop'].build_props['ro.build.date.utc'],
          target_info['build.prop'].build_props['ro.build.date.utc'])
 
+  @staticmethod
+  def _test_GetPackageMetadata_swapVendorBuildTimestamps(target_info, source_info):
+    (target_info['vendor.build.prop'].build_props['ro.vendor.build.date.utc'],
+     source_info['vendor.build.prop'].build_props['ro.vendor.build.date.utc']) = (
+         source_info['vendor.build.prop'].build_props['ro.vendor.build.date.utc'],
+         target_info['vendor.build.prop'].build_props['ro.vendor.build.date.utc'])
+
   def test_GetPackageMetadata_unintentionalDowngradeDetected(self):
     target_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
     source_info_dict = copy.deepcopy(self.TEST_SOURCE_INFO_DICT)
@@ -363,6 +384,24 @@
     self.assertRaises(RuntimeError, self.GetLegacyOtaMetadata, target_info,
                       source_info)
 
+  def test_GetPackageMetadata_unintentionalVendorDowngradeDetected(self):
+    target_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
+    target_info_dict['ab_update'] = 'true'
+    target_info_dict['ab_partitions'] = ['vendor']
+    target_info_dict["vendor.build.prop"] = copy.deepcopy(self.TEST_TARGET_VENDOR_INFO_DICT)
+    source_info_dict = copy.deepcopy(self.TEST_SOURCE_INFO_DICT)
+    source_info_dict['ab_update'] = 'true'
+    source_info_dict['ab_partitions'] = ['vendor']
+    source_info_dict["vendor.build.prop"] = copy.deepcopy(self.TEST_SOURCE_VENDOR_INFO_DICT)
+    self._test_GetPackageMetadata_swapVendorBuildTimestamps(
+        target_info_dict, source_info_dict)
+
+    target_info = common.BuildInfo(target_info_dict, None)
+    source_info = common.BuildInfo(source_info_dict, None)
+    common.OPTIONS.incremental_source = ''
+    self.assertRaises(RuntimeError, self.GetLegacyOtaMetadata, target_info,
+                      source_info)
+
   def test_GetPackageMetadata_downgrade(self):
     target_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
     source_info_dict = copy.deepcopy(self.TEST_SOURCE_INFO_DICT)
@@ -397,6 +436,55 @@
         },
         metadata)
 
+  def test_GetPackageMetadata_vendorDowngrade(self):
+    target_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
+    target_info_dict['ab_update'] = 'true'
+    target_info_dict['ab_partitions'] = ['vendor']
+    target_info_dict["vendor.build.prop"] = copy.deepcopy(self.TEST_TARGET_VENDOR_INFO_DICT)
+    source_info_dict = copy.deepcopy(self.TEST_SOURCE_INFO_DICT)
+    source_info_dict['ab_update'] = 'true'
+    source_info_dict['ab_partitions'] = ['vendor']
+    source_info_dict["vendor.build.prop"] = copy.deepcopy(self.TEST_SOURCE_VENDOR_INFO_DICT)
+    self._test_GetPackageMetadata_swapVendorBuildTimestamps(
+        target_info_dict, source_info_dict)
+
+    target_info = common.BuildInfo(target_info_dict, None)
+    source_info = common.BuildInfo(source_info_dict, None)
+    common.OPTIONS.incremental_source = ''
+    common.OPTIONS.downgrade = True
+    common.OPTIONS.wipe_user_data = True
+    common.OPTIONS.spl_downgrade = True
+    metadata = self.GetLegacyOtaMetadata(target_info, source_info)
+    # Reset spl_downgrade so other tests are unaffected
+    common.OPTIONS.spl_downgrade = False
+
+    self.assertDictEqual(
+        {
+            'ota-downgrade': 'yes',
+            'ota-type': 'AB',
+            'ota-required-cache': '0',
+            'ota-wipe': 'yes',
+            'post-build': 'build-fingerprint-target',
+            'post-build-incremental': 'build-version-incremental-target',
+            'post-sdk-level': '27',
+            'post-security-patch-level': '2017-12-01',
+            'post-timestamp': '1500000000',
+            'pre-device': 'product-device',
+            'pre-build': 'build-fingerprint-source',
+            'pre-build-incremental': 'build-version-incremental-source',
+            'spl-downgrade': 'yes',
+        },
+        metadata)
+
+    post_build = GetPackageMetadata(target_info, source_info).postcondition
+    self.assertEqual('vendor', post_build.partition_state[0].partition_name)
+    self.assertEqual('12345678', post_build.partition_state[0].version)
+
+    pre_build = GetPackageMetadata(target_info, source_info).precondition
+    self.assertEqual('vendor', pre_build.partition_state[0].partition_name)
+    self.assertEqual('87654321', pre_build.partition_state[0].version)
+
+
   @test_utils.SkipIfExternalToolsUnavailable()
   def test_GetTargetFilesZipForSecondaryImages(self):
     input_file = construct_target_files(secondary=True)
diff --git a/tools/sbom/generate-sbom-framework_res.py b/tools/sbom/generate-sbom-framework_res.py
index e637d53..d0d232d 100644
--- a/tools/sbom/generate-sbom-framework_res.py
+++ b/tools/sbom/generate-sbom-framework_res.py
@@ -52,8 +52,19 @@
   filename = 'data/framework_res.jar'
   file_id = f'SPDXRef-{sbom_data.encode_for_spdxid(filename)}'
   file = sbom_data.File(id=file_id, name=filename, checksum='SHA1: <checksum>')
+
+  package_name = 'framework_res'
+  package_id = f'SPDXRef-PREBUILT-{sbom_data.encode_for_spdxid(package_name)}'
+  package = sbom_data.Package(id=package_id, name=package_name, version='<package_version>',
+                    download_location=sbom_data.VALUE_NONE,
+                    supplier='Organization: <organization>',
+                    files_analyzed=True,
+                    verification_code='<package_verification_code>')
+  package.file_ids.append(file_id)
+
+  doc.packages.append(package)
   doc.files.append(file)
-  doc.describes = file_id
+  doc.describes = package_id
 
   with open(args.layoutlib_sbom, 'r', encoding='utf-8') as f:
     layoutlib_sbom = json.load(f)
@@ -72,7 +83,9 @@
     if file[sbom_writers.PropNames.FILE_NAME].startswith('data/res/'):
       resource_file_spdxids.append(file[sbom_writers.PropNames.SPDXID])
 
-  doc.relationships = []
+  doc.relationships = [
+    sbom_data.Relationship(package_id, sbom_data.RelationshipType.CONTAINS, file_id)
+  ]
   for spdxid in resource_file_spdxids:
     doc.relationships.append(
       sbom_data.Relationship(file_id, sbom_data.RelationshipType.GENERATED_FROM,