Merge "Emit apex_info to target_files META/apex_info.pb"
diff --git a/core/Makefile b/core/Makefile
index 7504687..79255ca 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -2328,12 +2328,12 @@
 # -----------------------------------------------------------------
 # vendor debug ramdisk
 # Combines vendor ramdisk files and debug ramdisk files to build the vendor debug ramdisk.
-INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET := $(PRODUCT_OUT)/vendor-ramdisk-debug.cpio$(RAMDISK_EXT)
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): DEBUG_RAMDISK_FILES := $(INTERNAL_DEBUG_RAMDISK_FILES)
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): VENDOR_RAMDISK_DIR := $(TARGET_VENDOR_RAMDISK_OUT)
+INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET := $(call intermediates-dir-for,PACKAGING,vendor_boot-debug)/vendor-ramdisk-debug.cpio$(RAMDISK_EXT)
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): DEBUG_RAMDISK_FILES := $(INTERNAL_DEBUG_RAMDISK_FILES)
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): VENDOR_RAMDISK_DIR := $(TARGET_VENDOR_RAMDISK_OUT)
 
 ifeq (true,$(BOARD_MOVE_RECOVERY_RESOURCES_TO_VENDOR_BOOT))
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): PRIVATE_ADDITIONAL_DIR := $(TARGET_RECOVERY_ROOT_OUT)
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): PRIVATE_ADDITIONAL_DIR := $(TARGET_RECOVERY_ROOT_OUT)
 endif
 
 INTERNAL_VENDOR_DEBUG_RAMDISK_FILES := $(filter $(TARGET_VENDOR_DEBUG_RAMDISK_OUT)/%, \
@@ -2344,14 +2344,15 @@
 # if BOARD_USES_RECOVERY_AS_BOOT is true. Otherwise, it will be $(PRODUCT_OUT)/vendor_debug_ramdisk.
 # But the path of $(VENDOR_DEBUG_RAMDISK_DIR) to build the vendor debug ramdisk, is always
 # $(PRODUCT_OUT)/vendor_debug_ramdisk.
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): VENDOR_DEBUG_RAMDISK_DIR := $(PRODUCT_OUT)/vendor_debug_ramdisk
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): $(INTERNAL_VENDOR_RAMDISK_TARGET) $(INSTALLED_DEBUG_RAMDISK_TARGET)
-$(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET): $(MKBOOTFS) $(INTERNAL_VENDOR_DEBUG_RAMDISK_FILES) | $(COMPRESSION_COMMAND_DEPS)
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): DEBUG_RAMDISK_DIR := $(PRODUCT_OUT)/debug_ramdisk
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): VENDOR_DEBUG_RAMDISK_DIR := $(PRODUCT_OUT)/vendor_debug_ramdisk
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): $(INTERNAL_VENDOR_RAMDISK_TARGET) $(INSTALLED_DEBUG_RAMDISK_TARGET)
+$(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET): $(MKBOOTFS) $(INTERNAL_VENDOR_DEBUG_RAMDISK_FILES) | $(COMPRESSION_COMMAND_DEPS)
 	$(call pretty,"Target vendor debug ram disk: $@")
 	mkdir -p $(TARGET_VENDOR_DEBUG_RAMDISK_OUT)
 	touch $(TARGET_VENDOR_DEBUG_RAMDISK_OUT)/force_debuggable
 	$(foreach debug_file,$(DEBUG_RAMDISK_FILES), \
-	  cp -f $(debug_file) $(subst $(PRODUCT_OUT)/debug_ramdisk,$(PRODUCT_OUT)/vendor_debug_ramdisk,$(debug_file)) &&) true
+	  cp -f $(debug_file) $(patsubst $(DEBUG_RAMDISK_DIR)/%,$(VENDOR_DEBUG_RAMDISK_DIR)/%,$(debug_file)) &&) true
 	$(MKBOOTFS) -d $(TARGET_OUT) $(VENDOR_RAMDISK_DIR) $(VENDOR_DEBUG_RAMDISK_DIR) $(PRIVATE_ADDITIONAL_DIR) | $(COMPRESSION_COMMAND) > $@
 
 INSTALLED_FILES_FILE_VENDOR_DEBUG_RAMDISK := $(PRODUCT_OUT)/installed-files-vendor-ramdisk-debug.txt
@@ -2361,7 +2362,7 @@
 
 # The vendor debug ramdisk will rsync from $(TARGET_VENDOR_RAMDISK_OUT) and $(INTERNAL_DEBUG_RAMDISK_FILES),
 # so we have to wait for the vendor debug ramdisk to be built before generating the installed file list.
-$(INSTALLED_FILES_FILE_VENDOR_DEBUG_RAMDISK): $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET)
+$(INSTALLED_FILES_FILE_VENDOR_DEBUG_RAMDISK): $(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET)
 $(INSTALLED_FILES_FILE_VENDOR_DEBUG_RAMDISK): $(INTERNAL_VENDOR_DEBUG_RAMDISK_FILES) $(FILESLIST) $(FILESLIST_UTIL)
 	echo Installed file list: $@
 	mkdir -p $(dir $@)
@@ -2392,10 +2393,10 @@
 endif
 
 # Depends on vendor_boot.img and vendor-ramdisk-debug.cpio.gz to build the new vendor_boot-debug.img
-$(INSTALLED_VENDOR_DEBUG_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(INSTALLED_VENDOR_BOOTIMAGE_TARGET) $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET)
+$(INSTALLED_VENDOR_DEBUG_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(INSTALLED_VENDOR_BOOTIMAGE_TARGET) $(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET)
 $(INSTALLED_VENDOR_DEBUG_BOOTIMAGE_TARGET): $(INTERNAL_VENDOR_RAMDISK_FRAGMENT_TARGETS)
 	$(call pretty,"Target vendor_boot debug image: $@")
-	$(MKBOOTIMG) $(INTERNAL_VENDOR_BOOTIMAGE_ARGS) $(BOARD_MKBOOTIMG_ARGS) --vendor_ramdisk $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET) $(INTERNAL_VENDOR_RAMDISK_FRAGMENT_ARGS) --vendor_boot $@
+	$(MKBOOTIMG) $(INTERNAL_VENDOR_BOOTIMAGE_ARGS) $(BOARD_MKBOOTIMG_ARGS) --vendor_ramdisk $(INTERNAL_VENDOR_DEBUG_RAMDISK_TARGET) $(INTERNAL_VENDOR_RAMDISK_FRAGMENT_ARGS) --vendor_boot $@
 	$(call assert-max-image-size,$@,$(BOARD_VENDOR_BOOTIMAGE_PARTITION_SIZE))
 	$(if $(BOARD_AVB_VENDOR_BOOT_KEY_PATH),$(call test-key-sign-vendor-bootimage,$@))
 
diff --git a/core/android_soong_config_vars.mk b/core/android_soong_config_vars.mk
index 883f92d..3a0c0f1 100644
--- a/core/android_soong_config_vars.mk
+++ b/core/android_soong_config_vars.mk
@@ -34,3 +34,8 @@
   SOONG_CONFIG_art_module += source_build
 endif
 SOONG_CONFIG_art_module_source_build ?= true
+
+# Apex build mode variables
+ifdef APEX_BUILD_FOR_PRE_S_DEVICES
+$(call add_soong_config_var_value,ANDROID,library_linking_strategy,prefer_static)
+endif
diff --git a/core/clear_vars.mk b/core/clear_vars.mk
index 5f16363..5effac7 100644
--- a/core/clear_vars.mk
+++ b/core/clear_vars.mk
@@ -243,6 +243,7 @@
 # lite(default),micro,nano,stream,full,nanopb-c,nanopb-c-enable_malloc,nanopb-c-16bit,nanopb-c-enable_malloc-16bit,nanopb-c-32bit,nanopb-c-enable_malloc-32bit
 LOCAL_PROTOC_OPTIMIZE_TYPE:=
 LOCAL_PROTO_JAVA_OUTPUT_PARAMS:=
+LOCAL_PROVIDES_USES_LIBRARY:=
 LOCAL_R8_FLAG_FILES:=
 LOCAL_RECORDED_MODULE_TYPE:=
 LOCAL_RENDERSCRIPT_CC:=
diff --git a/core/config_sanitizers.mk b/core/config_sanitizers.mk
index c92cea2..f39b84a 100644
--- a/core/config_sanitizers.mk
+++ b/core/config_sanitizers.mk
@@ -53,6 +53,18 @@
   endif
 endif
 
+# Disable global memtag_heap in excluded paths
+ifneq ($(filter memtag_heap, $(my_global_sanitize)),)
+  combined_exclude_paths := $(MEMTAG_HEAP_EXCLUDE_PATHS) \
+                            $(PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS)
+
+  ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_exclude_paths)),\
+         $(filter $(dir)%,$(LOCAL_PATH)))),)
+    my_global_sanitize := $(filter-out memtag_heap,$(my_global_sanitize))
+    my_global_sanitize_diag := $(filter-out memtag_heap,$(my_global_sanitize_diag))
+  endif
+endif
+
 ifneq ($(my_global_sanitize),)
   my_sanitize := $(my_global_sanitize) $(my_sanitize)
 endif
@@ -116,6 +128,25 @@
   endif
 endif
 
+# Enable memtag_heap in included paths (for Arm64 only).
+ifeq ($(filter memtag_heap, $(my_sanitize)),)
+  ifneq ($(filter arm64,$(TARGET_$(LOCAL_2ND_ARCH_VAR_PREFIX)ARCH)),)
+    combined_sync_include_paths := $(MEMTAG_HEAP_SYNC_INCLUDE_PATHS) \
+                                   $(PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS)
+    combined_async_include_paths := $(MEMTAG_HEAP_ASYNC_INCLUDE_PATHS) \
+                                    $(PRODUCT_MEMTAG_HEAP_ASYNC_INCLUDE_PATHS)
+
+    ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_sync_include_paths)),\
+           $(filter $(dir)%,$(LOCAL_PATH)))),)
+      my_sanitize := memtag_heap $(my_sanitize)
+      my_sanitize_diag := memtag_heap $(my_sanitize)
+    else ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_async_include_paths)),\
+           $(filter $(dir)%,$(LOCAL_PATH)))),)
+      my_sanitize := memtag_heap $(my_sanitize)
+    endif
+  endif
+endif
+
 # If CFI is disabled globally, remove it from my_sanitize.
 ifeq ($(strip $(ENABLE_CFI)),false)
   my_sanitize := $(filter-out cfi,$(my_sanitize))
@@ -164,6 +195,7 @@
 
 ifneq ($(filter arm x86 x86_64,$(TARGET_$(LOCAL_2ND_ARCH_VAR_PREFIX)ARCH)),)
   my_sanitize := $(filter-out hwaddress,$(my_sanitize))
+  my_sanitize := $(filter-out memtag_heap,$(my_sanitize))
 endif
 
 ifneq ($(filter hwaddress,$(my_sanitize)),)
@@ -183,6 +215,20 @@
   endif
 endif
 
+ifneq ($(filter memtag_heap,$(my_sanitize)),)
+  # Add memtag ELF note.
+  ifneq ($(filter memtag_heap,$(my_sanitize_diag)),)
+    my_whole_static_libraries += note_memtag_heap_sync
+  else
+    my_whole_static_libraries += note_memtag_heap_async
+  endif
+  # This is all that memtag_heap does - it is not an actual -fsanitize argument.
+  # Remove it from the list.
+  my_sanitize := $(filter-out memtag_heap,$(my_sanitize))
+endif
+
+my_sanitize_diag := $(filter-out memtag_heap,$(my_sanitize_diag))
+
 # TSAN is not supported on 32-bit architectures. For non-multilib cases, make
 # its use an error. For multilib cases, don't use it for the 32-bit case.
 ifneq ($(filter thread,$(my_sanitize)),)
diff --git a/core/dex_preopt_odex_install.mk b/core/dex_preopt_odex_install.mk
index c31d4e8..b74e047 100644
--- a/core/dex_preopt_odex_install.mk
+++ b/core/dex_preopt_odex_install.mk
@@ -220,8 +220,9 @@
     $(foreach lib, $(2),\
       $(call add_json_map, $(lib)) \
       $(eval file := $(filter %/$(lib).jar, $(call module-installed-files,$(lib)))) \
-      $(call add_json_str, Host,       $(call intermediates-dir-for,JAVA_LIBRARIES,$(lib),,COMMON)/javalib.jar) \
-      $(call add_json_str, Device,     $(call install-path-to-on-device-path,$(file))) \
+      $(call add_json_str, Host,        $(call intermediates-dir-for,JAVA_LIBRARIES,$(lib),,COMMON)/javalib.jar) \
+      $(call add_json_str, Device,      $(call install-path-to-on-device-path,$(file))) \
+      $(call add_json_map, Subcontexts, ${$}) $(call end_json_map) \
       $(call end_json_map)) \
     $(call end_json_map)
 
@@ -252,6 +253,7 @@
   $(call add_json_str,  ProfileClassListing,            $(if $(my_process_profile),$(LOCAL_DEX_PREOPT_PROFILE)))
   $(call add_json_bool, ProfileIsTextListing,           $(my_profile_is_text_listing))
   $(call add_json_bool, EnforceUsesLibraries,           $(LOCAL_ENFORCE_USES_LIBRARIES))
+  $(call add_json_str,  ProvidesUsesLibrary,            $(firstword $(LOCAL_PROVIDES_USES_LIBRARY) $(LOCAL_MODULE)))
   $(call add_json_map,  ClassLoaderContexts)
   $(call add_json_class_loader_context, any, $(my_dexpreopt_libs))
   $(call add_json_class_loader_context,  28, $(my_dexpreopt_libs_compat_28))
diff --git a/core/main.mk b/core/main.mk
index 508ae19..5ea95c8 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -1463,9 +1463,6 @@
 .PHONY: ramdisk_test_harness
 ramdisk_test_harness: $(INSTALLED_TEST_HARNESS_RAMDISK_TARGET)
 
-.PHONY: vendor_ramdisk_debug
-vendor_ramdisk_debug: $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET)
-
 .PHONY: userdataimage
 userdataimage: $(INSTALLED_USERDATAIMAGE_TARGET)
 
@@ -1545,7 +1542,6 @@
     $(INSTALLED_BPTIMAGE_TARGET) \
     $(INSTALLED_VENDORIMAGE_TARGET) \
     $(INSTALLED_VENDOR_BOOTIMAGE_TARGET) \
-    $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET) \
     $(INSTALLED_VENDOR_DEBUG_BOOTIMAGE_TARGET) \
     $(INSTALLED_ODMIMAGE_TARGET) \
     $(INSTALLED_VENDOR_DLKMIMAGE_TARGET) \
@@ -1730,7 +1726,6 @@
       $(INSTALLED_FILES_JSON_VENDOR_DEBUG_RAMDISK) \
       $(INSTALLED_DEBUG_RAMDISK_TARGET) \
       $(INSTALLED_DEBUG_BOOTIMAGE_TARGET) \
-      $(INSTALLED_VENDOR_DEBUG_RAMDISK_TARGET) \
       $(INSTALLED_VENDOR_DEBUG_BOOTIMAGE_TARGET) \
     )
     $(call dist-for-goals, bootimage_test_harness, \
diff --git a/target/board/BoardConfigGsiCommon.mk b/target/board/BoardConfigGsiCommon.mk
index e34dc23..a2150ad 100644
--- a/target/board/BoardConfigGsiCommon.mk
+++ b/target/board/BoardConfigGsiCommon.mk
@@ -49,6 +49,10 @@
 BOARD_GSI_DYNAMIC_PARTITIONS_SIZE := 3221225472
 endif
 
+# TODO(b/123695868, b/146149698):
+#     This flag is set by mainline but isn't desired for GSI
+BOARD_BLUETOOTH_BDROID_BUILDCFG_INCLUDE_DIR :=
+
 # Enable chain partition for boot, mainly for GKI images.
 BOARD_AVB_BOOT_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
 BOARD_AVB_BOOT_ALGORITHM := SHA256_RSA2048
diff --git a/target/product/gsi/current.txt b/target/product/gsi/current.txt
index 9f2b940..2ca6687 100644
--- a/target/product/gsi/current.txt
+++ b/target/product/gsi/current.txt
@@ -19,8 +19,12 @@
 LLNDK: libvndksupport.so
 LLNDK: libvulkan.so
 VNDK-SP: android.hardware.common-V2-ndk_platform.so
+VNDK-SP: android.hardware.common-unstable-ndk_platform.so
 VNDK-SP: android.hardware.common.fmq-V1-ndk_platform.so
+VNDK-SP: android.hardware.common.fmq-ndk_platform.so
+VNDK-SP: android.hardware.common.fmq-unstable-ndk_platform.so
 VNDK-SP: android.hardware.graphics.common-V2-ndk_platform.so
+VNDK-SP: android.hardware.graphics.common-unstable-ndk_platform.so
 VNDK-SP: android.hardware.graphics.common@1.0.so
 VNDK-SP: android.hardware.graphics.common@1.1.so
 VNDK-SP: android.hardware.graphics.common@1.2.so
@@ -58,7 +62,10 @@
 VNDK-SP: libz.so
 VNDK-core: android.hardware.audio.common@2.0.so
 VNDK-core: android.hardware.authsecret-V1-ndk_platform.so
+VNDK-core: android.hardware.authsecret-ndk_platform.so
+VNDK-core: android.hardware.authsecret-unstable-ndk_platform.so
 VNDK-core: android.hardware.automotive.occupant_awareness-V1-ndk_platform.so
+VNDK-core: android.hardware.automotive.occupant_awareness-ndk_platform.so
 VNDK-core: android.hardware.configstore-utils.so
 VNDK-core: android.hardware.configstore@1.0.so
 VNDK-core: android.hardware.configstore@1.1.so
@@ -69,28 +76,50 @@
 VNDK-core: android.hardware.graphics.bufferqueue@1.0.so
 VNDK-core: android.hardware.graphics.bufferqueue@2.0.so
 VNDK-core: android.hardware.health.storage-V1-ndk_platform.so
+VNDK-core: android.hardware.health.storage-ndk_platform.so
+VNDK-core: android.hardware.health.storage-unstable-ndk_platform.so
 VNDK-core: android.hardware.identity-V2-ndk_platform.so
+VNDK-core: android.hardware.identity-ndk_platform.so
 VNDK-core: android.hardware.keymaster-V2-ndk_platform.so
+VNDK-core: android.hardware.keymaster-ndk_platform.so
 VNDK-core: android.hardware.light-V1-ndk_platform.so
+VNDK-core: android.hardware.light-ndk_platform.so
 VNDK-core: android.hardware.media.bufferpool@2.0.so
 VNDK-core: android.hardware.media.omx@1.0.so
 VNDK-core: android.hardware.media@1.0.so
 VNDK-core: android.hardware.memtrack-V1-ndk_platform.so
+VNDK-core: android.hardware.memtrack-ndk_platform.so
+VNDK-core: android.hardware.memtrack-unstable-ndk_platform.so
 VNDK-core: android.hardware.memtrack@1.0.so
 VNDK-core: android.hardware.oemlock-V1-ndk_platform.so
+VNDK-core: android.hardware.oemlock-ndk_platform.so
+VNDK-core: android.hardware.oemlock-unstable-ndk_platform.so
 VNDK-core: android.hardware.power-V1-ndk_platform.so
+VNDK-core: android.hardware.power-ndk_platform.so
 VNDK-core: android.hardware.rebootescrow-V1-ndk_platform.so
+VNDK-core: android.hardware.rebootescrow-ndk_platform.so
 VNDK-core: android.hardware.security.keymint-V1-ndk_platform.so
+VNDK-core: android.hardware.security.keymint-ndk_platform.so
+VNDK-core: android.hardware.security.keymint-unstable-ndk_platform.so
 VNDK-core: android.hardware.security.secureclock-V1-ndk_platform.so
+VNDK-core: android.hardware.security.secureclock-ndk_platform.so
+VNDK-core: android.hardware.security.secureclock-unstable-ndk_platform.so
 VNDK-core: android.hardware.security.sharedsecret-V1-ndk_platform.so
+VNDK-core: android.hardware.security.sharedsecret-ndk_platform.so
+VNDK-core: android.hardware.security.sharedsecret-unstable-ndk_platform.so
 VNDK-core: android.hardware.soundtrigger@2.0-core.so
 VNDK-core: android.hardware.soundtrigger@2.0.so
 VNDK-core: android.hardware.vibrator-V1-ndk_platform.so
+VNDK-core: android.hardware.vibrator-ndk_platform.so
+VNDK-core: android.hardware.weaver-V1-ndk_platform.so
+VNDK-core: android.hardware.weaver-ndk_platform.so
+VNDK-core: android.hardware.weaver-unstable-ndk_platform.so
 VNDK-core: android.hidl.token@1.0-utils.so
 VNDK-core: android.hidl.token@1.0.so
 VNDK-core: android.system.keystore2-V1-ndk_platform.so
+VNDK-core: android.system.keystore2-ndk_platform.so
+VNDK-core: android.system.keystore2-unstable-ndk_platform.so
 VNDK-core: android.system.suspend@1.0.so
-VNDK-core: libadf.so
 VNDK-core: libaudioroute.so
 VNDK-core: libaudioutils.so
 VNDK-core: libbinder.so
diff --git a/target/product/runtime_libart.mk b/target/product/runtime_libart.mk
index 4da8794..e655d51 100644
--- a/target/product/runtime_libart.mk
+++ b/target/product/runtime_libart.mk
@@ -94,3 +94,15 @@
 PRODUCT_SYSTEM_PROPERTIES += \
     dalvik.vm.minidebuginfo=true \
     dalvik.vm.dex2oat-minidebuginfo=true
+
+# Two other device configs are added to IORap besides "ro.iorapd.enable".
+# IORap by default is off and starts when
+# (https://source.corp.google.com/android/system/iorap/iorapd.rc?q=iorapd.rc)
+#
+# * "ro.iorapd.enable" is true excluding unset
+# * One of the device configs is true.
+#
+# "ro.iorapd.enable" has to be set to true, so that iorap can be started.
+PRODUCT_SYSTEM_PROPERTIES += \
+    ro.iorapd.enable?=true
+
diff --git a/tools/product_config/Android.bp b/tools/product_config/Android.bp
new file mode 100644
index 0000000..287ed5a
--- /dev/null
+++ b/tools/product_config/Android.bp
@@ -0,0 +1,23 @@
+java_defaults {
+    name: "product-config-defaults",
+    srcs: ["src/**/*.java"],
+}
+
+java_binary_host {
+    name: "product-config",
+    defaults: ["product-config-defaults"],
+    manifest: "MANIFEST.MF"
+}
+
+java_test_host {
+    name: "product-config-test",
+    defaults: ["product-config-defaults"],
+    srcs: [
+        "test/**/*.java",
+    ],
+    static_libs: [
+        "junit"
+    ],
+    test_suites: ["general-tests"]
+}
+
diff --git a/tools/product_config/MANIFEST.MF b/tools/product_config/MANIFEST.MF
new file mode 100644
index 0000000..db88df3
--- /dev/null
+++ b/tools/product_config/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.build.config.Main
diff --git a/tools/product_config/TEST_MAPPING b/tools/product_config/TEST_MAPPING
new file mode 100644
index 0000000..d3568f1
--- /dev/null
+++ b/tools/product_config/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "product_config_test"
+    }
+  ]
+}
diff --git a/tools/product_config/src/com/android/build/config/ErrorReporter.java b/tools/product_config/src/com/android/build/config/ErrorReporter.java
new file mode 100644
index 0000000..f382b4e
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/ErrorReporter.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.lang.reflect.Field;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for reporting errors.
+ */
+public class ErrorReporter {
+    /**
+     * List of Entries that have occurred.
+     */
+    // Also used as the lock for this object.
+    private final ArrayList<Entry> mEntries = new ArrayList();
+
+    /**
+     * The categories that are for this Errors object.
+     */
+    private Map<Integer, Category> mCategories;
+
+    /**
+     * Whether there has been a warning or an error yet.
+     */
+    private boolean mHadWarningOrError;
+
+    /**
+     * Whether there has been an error yet.
+     */
+    private boolean mHadError;
+
+    /**
+     * Whether errors are errors, warnings or hidden.
+     */
+    public static enum Level {
+        HIDDEN("hidden"),
+        WARNING("warning"),
+        ERROR("error");
+
+        private final String mLabel;
+
+        Level(String label) {
+            mLabel = label;
+        }
+
+        String getLabel() {
+            return mLabel;
+        }
+    }
+
+    /**
+     * The available error codes.
+     */
+    public class Category {
+        private final int mCode;
+        private boolean mIsLevelSettable;
+        private Level mLevel;
+        private String mHelp;
+
+        /**
+         * Construct a Category object.
+         */
+        public Category(int code, boolean isLevelSettable, Level level, String help) {
+            if (!isLevelSettable && level != Level.ERROR) {
+                throw new RuntimeException("Don't have WARNING or HIDDEN without isLevelSettable");
+            }
+            mCode = code;
+            mIsLevelSettable = isLevelSettable;
+            mLevel = level;
+            mHelp = help;
+        }
+
+        /**
+         * Get the numeric code for the Category, which can be used to set the level.
+         */
+        public int getCode() {
+            return mCode;
+        }
+
+        /**
+         * Get whether the level of this Category can be changed.
+         */
+        public boolean isLevelSettable() {
+            return mIsLevelSettable;
+        }
+
+        /**
+         * Set the level of this category.
+         */
+        public void setLevel(Level level) {
+            if (!mIsLevelSettable) {
+                throw new RuntimeException("Can't set level for error " + mCode);
+            }
+            mLevel = level;
+        }
+
+        /**
+         * Return the level, including any overrides.
+         */
+        public Level getLevel() {
+            return mLevel;
+        }
+
+        /**
+         * Return the category's help text.
+         */
+        public String getHelp() {
+            return mHelp;
+        }
+    }
+
+    /**
+     * An instance of an error happening.
+     */
+    public class Entry {
+        private final Category mCategory;
+        private final Position mPosition;
+        private final String mMessage;
+
+        Entry(Category category, Position position, String message) {
+            mCategory = category;
+            mPosition = position;
+            mMessage = message;
+        }
+
+        public Category getCategory() {
+            return mCategory;
+        }
+
+        public Position getPosition() {
+            return mPosition;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+    }
+
+    private void initLocked() {
+        if (mCategories == null) {
+            HashMap<Integer, Category> categories = new HashMap();
+            for (Field field: getClass().getFields()) {
+                if (Category.class.isAssignableFrom(field.getType())) {
+                    Category category = null;
+                    try {
+                        category = (Category)field.get(this);
+                    } catch (IllegalAccessException ex) {
+                        // Wrap and rethrow, this is always on this class, so it's
+                        // our programming error if this happens.
+                        throw new RuntimeException("Categories on Errors should be public.", ex);
+                    }
+                    Category prev = categories.put(category.getCode(), category);
+                    if (prev != null) {
+                        throw new RuntimeException("Duplicate categories with code "
+                                + category.getCode());
+                    }
+                }
+            }
+            mCategories = Collections.unmodifiableMap(categories);
+        }
+    }
+
+    /**
+     * Returns a map of the category codes to the categories.
+     */
+    public Map<Integer, Category> getCategories() {
+        synchronized (mEntries) {
+            initLocked();
+            return mCategories;
+        }
+    }
+
+    /**
+     * Add an error with no source position.
+     */
+    public void add(Category category, String message) {
+        add(category, new Position(), message);
+    }
+
+    /**
+     * Add an error.
+     */
+    public void add(Category category, Position pos, String message) {
+        synchronized (mEntries) {
+            initLocked();
+            if (mCategories.get(category.getCode()) != category) {
+                throw new RuntimeException("Errors.Category used from the wrong Errors object.");
+            }
+            mEntries.add(new Entry(category, pos, message));
+            final Level level = category.getLevel();
+            if (level == Level.WARNING || level == Level.ERROR) {
+                mHadWarningOrError = true;
+            }
+            if (level == Level.ERROR) {
+                mHadError = true;
+            }
+        }
+    }
+
+    /**
+     * Returns whether there has been a warning or an error yet.
+     */
+    public boolean hadWarningOrError() {
+        synchronized (mEntries) {
+            return mHadWarningOrError;
+        }
+    }
+
+    /**
+     * Returns whether there has been an error yet.
+     */
+    public boolean hadError() {
+        synchronized (mEntries) {
+            return mHadError;
+        }
+    }
+
+    /**
+     * Returns a list of all entries that were added.
+     */
+    public List<Entry> getEntries() {
+        synchronized (mEntries) {
+            return new ArrayList<Entry>(mEntries);
+        }
+    }
+
+    /**
+     * Prints the errors.
+     */
+    public void printErrors(PrintStream out) {
+        synchronized (mEntries) {
+            for (Entry entry: mEntries) {
+                final Category category = entry.getCategory();
+                final Level level = category.getLevel();
+                if (level == Level.HIDDEN) {
+                    continue;
+                }
+                out.println(entry.getPosition() + "[" + level.getLabel() + " "
+                        + category.getCode() + "] " + entry.getMessage());
+            }
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Errors.java b/tools/product_config/src/com/android/build/config/Errors.java
new file mode 100644
index 0000000..63792c8
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Errors.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.lang.reflect.Field;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Error constants and error reporting.
+ * <p>
+ * <b>Naming Convention:</b>
+ * <ul>
+ *  <li>ERROR_ for Categories with isLevelSettable false and Level.ERROR
+ *  <li>WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN
+ *  <li>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
+ * </ul>
+ */
+public class Errors extends ErrorReporter {
+
+    public final Category ERROR_COMMAND_LINE = new Category(1, false, Level.ERROR,
+            "Error on the command line.");
+
+    public final Category WARNING_UNKNOWN_COMMAND_LINE_ERROR = new Category(2, true, Level.HIDDEN,
+            "Passing unknown errors on the command line.  Hidden by default for\n"
+            + "forward compatibility.");
+}
diff --git a/tools/product_config/src/com/android/build/config/Main.java b/tools/product_config/src/com/android/build/config/Main.java
new file mode 100644
index 0000000..7669742
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Main.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+public class Main {
+    private final Errors mErrors;
+    private final Options mOptions;
+
+    public Main(Errors errors, Options options) {
+        mErrors = errors;
+        mOptions = options;
+    }
+
+    void run() {
+        System.out.println("Hello World");
+
+        // TODO: Check the build environment to make sure we're running in a real
+        // build environment, e.g. actually inside a source tree, with TARGET_PRODUCT
+        // and TARGET_BUILD_VARIANT defined, etc.
+
+        // TODO: Run kati and extract the variables and convert all that into starlark files.
+
+        // TODO: Run starlark with all the generated ones and the hand written ones.
+
+        // TODO: Get the variables that were defined in starlark and use that to write
+        // out the make, soong and bazel input files.
+    }
+
+    public static void main(String[] args) {
+        Errors errors = new Errors();
+
+        Options options = Options.parse(errors, args);
+        if (errors.hadError()) {
+            Options.printHelp(System.err);
+            System.err.println();
+            errors.printErrors(System.err);
+            System.exit(1);
+        }
+
+        switch (options.getAction()) {
+            case DEFAULT:
+                (new Main(errors, options)).run();
+                errors.printErrors(System.err);
+                return;
+            case HELP:
+                Options.printHelp(System.out);
+                return;
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Options.java b/tools/product_config/src/com/android/build/config/Options.java
new file mode 100644
index 0000000..494b947
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Options.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.TreeMap;
+
+public class Options {
+    public enum Action {
+        DEFAULT,
+        HELP
+    }
+
+    private Action mAction = Action.DEFAULT;
+
+    public Action getAction() {
+        return mAction;
+    }
+
+    public static void printHelp(PrintStream out) {
+        out.println("usage: product_config");
+        out.println();
+        out.println("OPTIONS");
+        out.println("  --hide ERROR_ID          Suppress this error.");
+        out.println("  --error ERROR_ID         Make this ERROR_ID a fatal error.");
+        out.println("  --help -h                This message.");
+        out.println("  --warning ERROR_ID       Make this ERROR_ID a warning.");
+        out.println();
+        out.println("ERRORS");
+        out.println("  The following are the errors that can be controlled on the");
+        out.println("  commandline with the --hide --warning --error flags.");
+
+        TreeMap<Integer,Errors.Category> sorted = new TreeMap((new Errors()).getCategories());
+
+        for (final Errors.Category category: sorted.values()) {
+            if (category.isLevelSettable()) {
+                out.println(String.format("    %-3d      %s", category.getCode(),
+                category.getHelp().replace("\n", "\n             ")));
+            }
+        }
+    }
+
+    static class Parser {
+        private class ParseException extends Exception {
+            public ParseException(String message) {
+                super(message);
+            }
+        }
+
+        private Errors mErrors;
+        private String[] mArgs;
+        private Options mResult = new Options();
+        private int mIndex;
+
+        public Parser(Errors errors, String[] args) {
+            mErrors = errors;
+            mArgs = args;
+        }
+
+        public Options parse() {
+            try {
+                while (mIndex < mArgs.length) {
+                    final String arg = mArgs[mIndex];
+
+                    if ("--hide".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.HIDDEN);
+                    } else if ("--error".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.ERROR);
+                    } else if ("--help".equals(arg) || "-h".equals(arg)) {
+                        // Help overrides all other commands if there isn't an error, but
+                        // we will stop here.
+                        if (!mErrors.hadError()) {
+                            mResult.mAction = Action.HELP;
+                        }
+                        return mResult;
+                    } else if ("--warning".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.WARNING);
+                    } else {
+                        throw new ParseException("Unknown command line argument: " + arg);
+                    }
+
+                    mIndex++;
+                }
+            } catch (ParseException ex) {
+                mErrors.add(mErrors.ERROR_COMMAND_LINE, ex.getMessage());
+            }
+
+            return mResult;
+        }
+
+        private void addWarning(Errors.Category category, String message) {
+            mErrors.add(category, message);
+        }
+
+        private String getNextNonFlagArg() {
+            if (mIndex == mArgs.length - 1) {
+                return null;
+            }
+            if (mArgs[mIndex + 1].startsWith("-")) {
+                return null;
+            }
+            mIndex++;
+            return mArgs[mIndex];
+        }
+
+        private int requireNextNumberArg(String arg) throws ParseException {
+            final String val = getNextNonFlagArg();
+            if (val == null) {
+                throw new ParseException(arg + " requires a numeric argument.");
+            }
+            try {
+                return Integer.parseInt(val);
+            } catch (NumberFormatException ex) {
+                throw new ParseException(arg + " requires a numeric argument. found: " + val);
+            }
+        }
+
+        private void handleErrorCode(String arg, Errors.Level level) throws ParseException {
+            final int code = requireNextNumberArg(arg);
+            final Errors.Category category = mErrors.getCategories().get(code);
+            if (category == null) {
+                mErrors.add(mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR,
+                        "Unknown error code: " + code);
+                return;
+            }
+            if (!category.isLevelSettable()) {
+                mErrors.add(mErrors.ERROR_COMMAND_LINE, "Can't set level for error " + code);
+                return;
+            }
+            category.setLevel(level);
+        }
+    }
+
+    /**
+     * Parse the arguments and return an options object.
+     * <p>
+     * Updates errors with the hidden / warning / error levels.
+     * <p>
+     * Adds errors encountered to Errors object.
+     */
+    public static Options parse(Errors errors, String[] args) {
+        return (new Parser(errors, args)).parse();
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Position.java b/tools/product_config/src/com/android/build/config/Position.java
new file mode 100644
index 0000000..7953942
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Position.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+/**
+ * Position in a source file.
+ */
+public class Position implements Comparable<Position> {
+    /**
+     * Sentinel line number for when there is no known line number.
+     */
+    public static final int NO_LINE = -1;
+
+    private final String mFile;
+    private final int mLine;
+
+    public Position() {
+        mFile = null;
+        mLine = NO_LINE;
+    }
+
+    public Position(String file) {
+        mFile = file;
+        mLine = NO_LINE;
+    }
+
+    public Position(String file, int line) {
+        if (line < NO_LINE) {
+            throw new IllegalArgumentException("Negative line number. file=" + file
+                    + " line=" + line);
+        }
+        mFile = file;
+        mLine = line;
+    }
+
+    public int compareTo(Position that) {
+        int result = mFile.compareTo(that.mFile);
+        if (result != 0) {
+            return result;
+        }
+        return mLine - that.mLine;
+    }
+
+    public String getFile() {
+        return mFile;
+    }
+
+    public int getLine() {
+        return mLine;
+    }
+
+    @Override
+    public String toString() {
+      if (mFile == null && mLine == NO_LINE) {
+        return "";
+      } else if (mFile == null && mLine != NO_LINE) {
+        return "<unknown>:" + mLine + ": ";
+      } else if (mFile != null && mLine == NO_LINE) {
+        return mFile + ": ";
+      } else { // if (mFile != null && mLine != NO_LINE)
+        return mFile + ':' + mLine + ": ";
+      }
+    }
+}
diff --git a/tools/product_config/test/com/android/build/config/ErrorReporterTest.java b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
new file mode 100644
index 0000000..2cde476
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class ErrorReporterTest {
+    /**
+     * Test that errors can be recorded and retrieved.
+     */
+    @Test
+    public void testAdding() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.ERROR, new Position("a", 12), "Errrororrrr");
+
+        Assert.assertTrue(errors.hadWarningOrError());
+        Assert.assertTrue(errors.hadError());
+
+        List<TestErrors.Entry> entries = errors.getEntries();
+        Assert.assertEquals(1, entries.size());
+
+        TestErrors.Entry entry = entries.get(0);
+        Assert.assertEquals(errors.ERROR, entry.getCategory());
+        Assert.assertEquals("a", entry.getPosition().getFile());
+        Assert.assertEquals(12, entry.getPosition().getLine());
+        Assert.assertEquals("Errrororrrr", entry.getMessage());
+
+        Assert.assertNotEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that not adding an error doesn't record errors.
+     */
+    @Test
+    public void testNoError() {
+        TestErrors errors = new TestErrors();
+
+        Assert.assertFalse(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that not adding a warning doesn't record errors.
+     */
+    @Test
+    public void testWarning() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.WARNING, "Waaaaarninggggg");
+
+        Assert.assertTrue(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertNotEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that hidden warnings don't report.
+     */
+    @Test
+    public void testHidden() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.HIDDEN, "Hidddeennn");
+
+        Assert.assertFalse(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test changing an error level.
+     */
+    @Test
+    public void testSetLevel() {
+        TestErrors errors = new TestErrors();
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR.getLevel());
+
+        errors.ERROR.setLevel(TestErrors.Level.WARNING);
+
+        Assert.assertEquals(TestErrors.Level.WARNING, errors.ERROR.getLevel());
+    }
+
+    /**
+     * Test that changing a fixed error fails.
+     */
+    @Test
+    public void testSetLevelFails() {
+        TestErrors errors = new TestErrors();
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
+
+        boolean exceptionThrown = false;
+        try {
+            errors.ERROR_FIXED.setLevel(TestErrors.Level.WARNING);
+        } catch (RuntimeException ex) {
+            exceptionThrown = true;
+        }
+
+        Assert.assertTrue(exceptionThrown);
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
+    }
+}
diff --git a/tools/product_config/test/com/android/build/config/OptionsTest.java b/tools/product_config/test/com/android/build/config/OptionsTest.java
new file mode 100644
index 0000000..2c36322
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/OptionsTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OptionsTest {
+    @Test
+    public void testErrorMissingLast() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorMissingNotLast() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "--warning", "2"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorNotNumeric() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "notgood"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorInvalidError() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "50000"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.WARNING_UNKNOWN_COMMAND_LINE_ERROR, errors);
+    }
+
+    @Test
+    public void testErrorOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+
+    @Test
+    public void testWarningOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--warning", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+
+    @Test
+    public void testHideOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--hide", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+}
+
diff --git a/tools/product_config/test/com/android/build/config/TestErrors.java b/tools/product_config/test/com/android/build/config/TestErrors.java
new file mode 100644
index 0000000..dde88b0
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/TestErrors.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Errors for testing.
+ */
+public class TestErrors extends ErrorReporter {
+
+    public static final int ERROR_CODE = 1;
+
+    public final Category ERROR = new Category(ERROR_CODE, true, Level.ERROR,
+            "An error.");
+
+    public static final int WARNING_CODE = 2;
+
+    public final Category WARNING = new Category(WARNING_CODE, true, Level.WARNING,
+            "A warning.");
+
+    public static final int HIDDEN_CODE = 3;
+
+    public final Category HIDDEN = new Category(HIDDEN_CODE, true, Level.HIDDEN,
+            "A hidden warning.");
+
+    public static final int ERROR_FIXED_CODE = 4;
+
+    public final Category ERROR_FIXED = new Category(ERROR_FIXED_CODE, false, Level.ERROR,
+            "An error that can't have its level changed.");
+
+    public void assertHasEntry(Errors.Category category) {
+        assertHasEntry(category, this);
+    }
+
+    public String getErrorMessages() {
+        return getErrorMessages(this);
+    }
+
+    public static void assertHasEntry(Errors.Category category, ErrorReporter errors) {
+        StringBuilder found = new StringBuilder();
+        for (Errors.Entry entry: errors.getEntries()) {
+            if (entry.getCategory() == category) {
+                return;
+            }
+            found.append(' ');
+            found.append(entry.getCategory().getCode());
+        }
+        throw new AssertionError("No error category " + category.getCode() + " found."
+                + " Found category codes were:" + found);
+    }
+
+    public static String getErrorMessages(ErrorReporter errors) {
+        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try {
+            errors.printErrors(new PrintStream(stream, true, StandardCharsets.UTF_8.name()));
+        } catch (UnsupportedEncodingException ex) {
+            // utf-8 is always supported
+        }
+        return new String(stream.toByteArray(), StandardCharsets.UTF_8);
+    }
+}
+