Merge "Don't allow using framework and SDK at the same time."
diff --git a/Android.bp b/Android.bp
index 380a388..42a8e5c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -61,6 +61,9 @@
         linux_bionic: {
             enabled: true,
         },
+        linux_musl: {
+            enabled: false,
+        },
         linux_glibc: {
             enabled: false,
         },
@@ -82,6 +85,9 @@
         linux_bionic: {
             enabled: true,
         },
+        linux_musl: {
+            enabled: false,
+        },
         linux_glibc: {
             enabled: false,
         },
diff --git a/android/bazel.go b/android/bazel.go
index e3fb0a6..af5de12 100644
--- a/android/bazel.go
+++ b/android/bazel.go
@@ -573,6 +573,8 @@
 		"prebuilt_car-ui-androidx-core-common",         // b/224773339, genrule dependency creates an .aar, not a .jar
 		"prebuilt_platform-robolectric-4.4-prebuilt",   // aosp/1999250, needs .aar support in Jars
 		"prebuilt_platform-robolectric-4.5.1-prebuilt", // aosp/1999250, needs .aar support in Jars
+
+		"libtombstoned_client_rust_bridge_code", "libtombstoned_client_wrapper", // rust conversions are not supported
 	}
 
 	// Per-module denylist of cc_library modules to only generate the static
diff --git a/android/rule_builder.go b/android/rule_builder.go
index 098c1fc..11da36c 100644
--- a/android/rule_builder.go
+++ b/android/rule_builder.go
@@ -101,12 +101,7 @@
 }
 
 // Restat marks the rule as a restat rule, which will be passed to ModuleContext.Rule in BuildParams.Restat.
-//
-// Restat is not compatible with Sbox()
 func (r *RuleBuilder) Restat() *RuleBuilder {
-	if r.sbox {
-		panic("Restat() is not compatible with Sbox()")
-	}
 	r.restat = true
 	return r
 }
@@ -141,8 +136,6 @@
 // point to a location where sbox's manifest will be written and must be outside outputDir. sbox
 // will ensure that all outputs have been written, and will discard any output files that were not
 // specified.
-//
-// Sbox is not compatible with Restat()
 func (r *RuleBuilder) Sbox(outputDir WritablePath, manifestPath WritablePath) *RuleBuilder {
 	if r.sbox {
 		panic("Sbox() may not be called more than once")
@@ -150,9 +143,6 @@
 	if len(r.commands) > 0 {
 		panic("Sbox() may not be called after Command()")
 	}
-	if r.restat {
-		panic("Sbox() is not compatible with Restat()")
-	}
 	r.sbox = true
 	r.outDir = outputDir
 	r.sboxManifestPath = manifestPath
@@ -636,11 +626,14 @@
 				ctx: r.ctx,
 			},
 		}
-		sboxCmd.Text("rm -rf").Output(r.outDir)
-		sboxCmd.Text("&&")
 		sboxCmd.builtToolWithoutDeps("sbox").
-			Flag("--sandbox-path").Text(shared.TempDirForOutDir(PathForOutput(r.ctx).String())).
-			Flag("--manifest").Input(r.sboxManifestPath)
+			FlagWithArg("--sandbox-path ", shared.TempDirForOutDir(PathForOutput(r.ctx).String())).
+			FlagWithArg("--output-dir ", r.outDir.String()).
+			FlagWithInput("--manifest ", r.sboxManifestPath)
+
+		if r.restat {
+			sboxCmd.Flag("--write-if-changed")
+		}
 
 		// Replace the command string, and add the sbox tool and manifest textproto to the
 		// dependencies of the final sbox rule.
diff --git a/android/rule_builder_test.go b/android/rule_builder_test.go
index 3766bb0..86647eb 100644
--- a/android/rule_builder_test.go
+++ b/android/rule_builder_test.go
@@ -678,32 +678,32 @@
 	})
 	t.Run("sbox", func(t *testing.T) {
 		outDir := "out/soong/.intermediates/foo_sbox"
-		outFile := filepath.Join(outDir, "gen/foo_sbox")
-		depFile := filepath.Join(outDir, "gen/foo_sbox.d")
+		sboxOutDir := filepath.Join(outDir, "gen")
+		outFile := filepath.Join(sboxOutDir, "foo_sbox")
+		depFile := filepath.Join(sboxOutDir, "foo_sbox.d")
 		rspFile := filepath.Join(outDir, "rsp")
 		rspFile2 := filepath.Join(outDir, "rsp2")
 		manifest := filepath.Join(outDir, "sbox.textproto")
 		sbox := filepath.Join("out", "soong", "host", result.Config.PrebuiltOS(), "bin/sbox")
 		sandboxPath := shared.TempDirForOutDir("out/soong")
 
-		cmd := `rm -rf ` + outDir + `/gen && ` +
-			sbox + ` --sandbox-path ` + sandboxPath + ` --manifest ` + manifest
+		cmd := sbox + ` --sandbox-path ` + sandboxPath + ` --output-dir ` + sboxOutDir + ` --manifest ` + manifest
 		module := result.ModuleForTests("foo_sbox", "")
 		check(t, module.Output("gen/foo_sbox"), module.Output(rspFile2),
 			cmd, outFile, depFile, rspFile, rspFile2, false, []string{manifest}, []string{sbox})
 	})
 	t.Run("sbox_inputs", func(t *testing.T) {
 		outDir := "out/soong/.intermediates/foo_sbox_inputs"
-		outFile := filepath.Join(outDir, "gen/foo_sbox_inputs")
-		depFile := filepath.Join(outDir, "gen/foo_sbox_inputs.d")
+		sboxOutDir := filepath.Join(outDir, "gen")
+		outFile := filepath.Join(sboxOutDir, "foo_sbox_inputs")
+		depFile := filepath.Join(sboxOutDir, "foo_sbox_inputs.d")
 		rspFile := filepath.Join(outDir, "rsp")
 		rspFile2 := filepath.Join(outDir, "rsp2")
 		manifest := filepath.Join(outDir, "sbox.textproto")
 		sbox := filepath.Join("out", "soong", "host", result.Config.PrebuiltOS(), "bin/sbox")
 		sandboxPath := shared.TempDirForOutDir("out/soong")
 
-		cmd := `rm -rf ` + outDir + `/gen && ` +
-			sbox + ` --sandbox-path ` + sandboxPath + ` --manifest ` + manifest
+		cmd := sbox + ` --sandbox-path ` + sandboxPath + ` --output-dir ` + sboxOutDir + ` --manifest ` + manifest
 
 		module := result.ModuleForTests("foo_sbox_inputs", "")
 		check(t, module.Output("gen/foo_sbox_inputs"), module.Output(rspFile2),
diff --git a/apex/builder.go b/apex/builder.go
index 50c8dd1..ea61e1a 100644
--- a/apex/builder.go
+++ b/apex/builder.go
@@ -617,10 +617,16 @@
 		}
 
 		// Create a NOTICE file, and embed it as an asset file in the APEX.
-		a.htmlGzNotice = android.PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
+		a.htmlGzNotice = android.PathForModuleOut(ctx, "NOTICE.html.gz")
 		android.BuildNoticeHtmlOutputFromLicenseMetadata(ctx, a.htmlGzNotice)
-		implicitInputs = append(implicitInputs, a.htmlGzNotice)
-		optFlags = append(optFlags, "--assets_dir "+filepath.Dir(a.htmlGzNotice.String()))
+		noticeAssetPath := android.PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
+		builder := android.NewRuleBuilder(pctx, ctx)
+		builder.Command().Text("cp").
+			Input(a.htmlGzNotice).
+			Output(noticeAssetPath)
+		builder.Build("notice_dir", "Building notice dir")
+		implicitInputs = append(implicitInputs, noticeAssetPath)
+		optFlags = append(optFlags, "--assets_dir "+filepath.Dir(noticeAssetPath.String()))
 
 		if (moduleMinSdkVersion.GreaterThan(android.SdkVersion_Android10) && !a.shouldGenerateHashtree()) && !compressionEnabled {
 			// Apexes which are supposed to be installed in builtin dirs(/system, etc)
diff --git a/bp2build/java_library_conversion_test.go b/bp2build/java_library_conversion_test.go
index 2f6bce2..4b75e3b 100644
--- a/bp2build/java_library_conversion_test.go
+++ b/bp2build/java_library_conversion_test.go
@@ -30,6 +30,7 @@
 }
 
 func runJavaLibraryTestCase(t *testing.T, tc bp2buildTestCase) {
+	t.Helper()
 	runJavaLibraryTestCaseWithRegistrationCtxFunc(t, tc, func(ctx android.RegistrationContext) {})
 }
 
@@ -156,3 +157,65 @@
 		ctx.RegisterModuleType("java_plugin", java.PluginFactory)
 	})
 }
+
+func TestJavaLibraryErrorproneJavacflagsEnabledManually(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    srcs: ["a.java"],
+    javacflags: ["-Xsuper-fast"],
+    errorprone: {
+        enabled: true,
+        javacflags: ["-Xep:SpeedLimit:OFF"],
+    },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"javacopts": `[
+        "-Xsuper-fast",
+        "-Xep:SpeedLimit:OFF",
+    ]`,
+				"srcs": `["a.java"]`,
+			}),
+		},
+	})
+}
+
+func TestJavaLibraryErrorproneJavacflagsErrorproneDisabledByDefault(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    srcs: ["a.java"],
+    javacflags: ["-Xsuper-fast"],
+    errorprone: {
+        javacflags: ["-Xep:SpeedLimit:OFF"],
+    },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"javacopts": `["-Xsuper-fast"]`,
+				"srcs":      `["a.java"]`,
+			}),
+		},
+	})
+}
+
+func TestJavaLibraryErrorproneJavacflagsErrorproneDisabledManually(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    srcs: ["a.java"],
+    javacflags: ["-Xsuper-fast"],
+    errorprone: {
+		enabled: false,
+        javacflags: ["-Xep:SpeedLimit:OFF"],
+    },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"javacopts": `["-Xsuper-fast"]`,
+				"srcs":      `["a.java"]`,
+			}),
+		},
+	})
+}
diff --git a/cc/androidmk.go b/cc/androidmk.go
index 318cd7c..ff5ba45 100644
--- a/cc/androidmk.go
+++ b/cc/androidmk.go
@@ -331,6 +331,14 @@
 		})
 }
 
+func (test *testDecorator) AndroidMkEntries(ctx AndroidMkContext, entries *android.AndroidMkEntries) {
+	entries.ExtraEntries = append(entries.ExtraEntries, func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
+		if len(test.InstallerProperties.Test_suites) > 0 {
+			entries.AddCompatibilityTestSuites(test.InstallerProperties.Test_suites...)
+		}
+	})
+}
+
 func (binary *binaryDecorator) AndroidMkEntries(ctx AndroidMkContext, entries *android.AndroidMkEntries) {
 	ctx.subAndroidMk(entries, binary.baseInstaller)
 
@@ -379,14 +387,13 @@
 
 func (test *testBinary) AndroidMkEntries(ctx AndroidMkContext, entries *android.AndroidMkEntries) {
 	ctx.subAndroidMk(entries, test.binaryDecorator)
+	ctx.subAndroidMk(entries, test.testDecorator)
+
 	entries.Class = "NATIVE_TESTS"
 	if Bool(test.Properties.Test_per_src) {
 		entries.SubName = "_" + String(test.binaryDecorator.Properties.Stem)
 	}
 	entries.ExtraEntries = append(entries.ExtraEntries, func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
-		if len(test.Properties.Test_suites) > 0 {
-			entries.AddCompatibilityTestSuites(test.Properties.Test_suites...)
-		}
 		if test.testConfig != nil {
 			entries.SetString("LOCAL_FULL_TEST_CONFIG", test.testConfig.String())
 		}
@@ -445,6 +452,7 @@
 
 func (test *testLibrary) AndroidMkEntries(ctx AndroidMkContext, entries *android.AndroidMkEntries) {
 	ctx.subAndroidMk(entries, test.libraryDecorator)
+	ctx.subAndroidMk(entries, test.testDecorator)
 }
 
 func (installer *baseInstaller) AndroidMkEntries(ctx AndroidMkContext, entries *android.AndroidMkEntries) {
diff --git a/cc/cc.go b/cc/cc.go
index 58ab28c..ac6da05 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -3595,7 +3595,8 @@
 		&SharedProperties{},
 		&FlagExporterProperties{},
 		&BinaryLinkerProperties{},
-		&TestProperties{},
+		&TestLinkerProperties{},
+		&TestInstallerProperties{},
 		&TestBinaryProperties{},
 		&BenchmarkProperties{},
 		&fuzz.FuzzProperties{},
diff --git a/cc/cc_test.go b/cc/cc_test.go
index 278efa1..09cc352 100644
--- a/cc/cc_test.go
+++ b/cc/cc_test.go
@@ -779,6 +779,68 @@
 	}
 }
 
+func TestTestBinaryTestSuites(t *testing.T) {
+	bp := `
+		cc_test {
+			name: "main_test",
+			srcs: ["main_test.cpp"],
+			test_suites: [
+				"suite_1",
+				"suite_2",
+			],
+			gtest: false,
+		}
+	`
+
+	ctx := prepareForCcTest.RunTestWithBp(t, bp).TestContext
+	module := ctx.ModuleForTests("main_test", "android_arm_armv7-a-neon").Module()
+
+	entries := android.AndroidMkEntriesForTest(t, ctx, module)[0]
+	compatEntries := entries.EntryMap["LOCAL_COMPATIBILITY_SUITE"]
+	if len(compatEntries) != 2 {
+		t.Errorf("expected two elements in LOCAL_COMPATIBILITY_SUITE. got %d", len(compatEntries))
+	}
+	if compatEntries[0] != "suite_1" {
+		t.Errorf("expected LOCAL_COMPATIBILITY_SUITE to be`suite_1`,"+
+			" but was '%s'", compatEntries[0])
+	}
+	if compatEntries[1] != "suite_2" {
+		t.Errorf("expected LOCAL_COMPATIBILITY_SUITE to be`suite_2`,"+
+			" but was '%s'", compatEntries[1])
+	}
+}
+
+func TestTestLibraryTestSuites(t *testing.T) {
+	bp := `
+		cc_test_library {
+			name: "main_test_lib",
+			srcs: ["main_test_lib.cpp"],
+			test_suites: [
+				"suite_1",
+				"suite_2",
+			],
+			gtest: false,
+		}
+	`
+
+	ctx := prepareForCcTest.RunTestWithBp(t, bp).TestContext
+	module := ctx.ModuleForTests("main_test_lib", "android_arm_armv7-a-neon_shared").Module()
+
+	entries := android.AndroidMkEntriesForTest(t, ctx, module)[0]
+	compatEntries := entries.EntryMap["LOCAL_COMPATIBILITY_SUITE"]
+	if len(compatEntries) != 2 {
+		t.Errorf("expected two elements in LOCAL_COMPATIBILITY_SUITE. got %d", len(compatEntries))
+	}
+	if compatEntries[0] != "suite_1" {
+		t.Errorf("expected LOCAL_COMPATIBILITY_SUITE to be`suite_1`,"+
+			" but was '%s'", compatEntries[0])
+	}
+	if compatEntries[1] != "suite_2" {
+		t.Errorf("expected LOCAL_COMPATIBILITY_SUITE to be`suite_2`,"+
+			" but was '%s'", compatEntries[1])
+	}
+}
+
 func TestVndkWhenVndkVersionIsNotSet(t *testing.T) {
 	ctx := testCcNoVndk(t, `
 		cc_library {
diff --git a/cc/config/global.go b/cc/config/global.go
index fad675a..0f31931 100644
--- a/cc/config/global.go
+++ b/cc/config/global.go
@@ -225,7 +225,6 @@
 		"-Wno-deprecated-enum-enum-conversion",      // http://b/153746563
 		"-Wno-string-compare",                       // http://b/153764102
 		"-Wno-enum-enum-conversion",                 // http://b/154138986
-		"-Wno-enum-float-conversion",                // http://b/154255917
 		"-Wno-pessimizing-move",                     // http://b/154270751
 		// New warnings to be fixed after clang-r399163
 		"-Wno-non-c-typedef-for-linkage", // http://b/161304145
diff --git a/cc/test.go b/cc/test.go
index d8b7833..ead7877 100644
--- a/cc/test.go
+++ b/cc/test.go
@@ -25,7 +25,8 @@
 	"android/soong/tradefed"
 )
 
-type TestProperties struct {
+// TestLinkerProperties properties to be registered via the linker
+type TestLinkerProperties struct {
 	// if set, build against the gtest library. Defaults to true.
 	Gtest *bool
 
@@ -33,6 +34,12 @@
 	Isolated *bool
 }
 
+// TestInstallerProperties properties to be registered via the installer
+type TestInstallerProperties struct {
+	// list of compatibility suites (for example "cts", "vts") that the module should be installed into.
+	Test_suites []string `android:"arch_variant"`
+}
+
 // Test option struct.
 type TestOptions struct {
 	// The UID that you want to run the test as on a device.
@@ -83,10 +90,6 @@
 	// list of binary modules that should be installed alongside the test
 	Data_bins []string `android:"arch_variant"`
 
-	// list of compatibility suites (for example "cts", "vts") that the module should be
-	// installed into.
-	Test_suites []string `android:"arch_variant"`
-
 	// the name of the test configuration (for example "AndroidTest.xml") that should be
 	// installed with the module.
 	Test_config *string `android:"path,arch_variant"`
@@ -243,12 +246,14 @@
 }
 
 type testDecorator struct {
-	Properties TestProperties
-	linker     *baseLinker
+	LinkerProperties    TestLinkerProperties
+	InstallerProperties TestInstallerProperties
+	installer           *baseInstaller
+	linker              *baseLinker
 }
 
 func (test *testDecorator) gtest() bool {
-	return BoolDefault(test.Properties.Gtest, true)
+	return BoolDefault(test.LinkerProperties.Gtest, true)
 }
 
 func (test *testDecorator) testBinary() bool {
@@ -283,7 +288,7 @@
 	if test.gtest() {
 		if ctx.useSdk() && ctx.Device() {
 			deps.StaticLibs = append(deps.StaticLibs, "libgtest_main_ndk_c++", "libgtest_ndk_c++")
-		} else if BoolDefault(test.Properties.Isolated, false) {
+		} else if BoolDefault(test.LinkerProperties.Isolated, false) {
 			deps.StaticLibs = append(deps.StaticLibs, "libgtest_isolated_main")
 			// The isolated library requires liblog, but adding it
 			// as a static library means unit tests cannot override
@@ -316,7 +321,11 @@
 }
 
 func (test *testDecorator) linkerProps() []interface{} {
-	return []interface{}{&test.Properties}
+	return []interface{}{&test.LinkerProperties}
+}
+
+func (test *testDecorator) installerProps() []interface{} {
+	return []interface{}{&test.InstallerProperties}
 }
 
 func NewTestInstaller() *baseInstaller {
@@ -324,7 +333,7 @@
 }
 
 type testBinary struct {
-	testDecorator
+	*testDecorator
 	*binaryDecorator
 	*baseCompiler
 	Properties       TestBinaryProperties
@@ -358,6 +367,10 @@
 	return flags
 }
 
+func (test *testBinary) installerProps() []interface{} {
+	return append(test.baseInstaller.installerProps(), test.testDecorator.installerProps()...)
+}
+
 func (test *testBinary) install(ctx ModuleContext, file android.Path) {
 	// TODO: (b/167308193) Switch to /data/local/tests/unrestricted as the default install base.
 	testInstallBase := "/data/local/tmp"
@@ -411,7 +424,7 @@
 		var options []tradefed.Option
 		configs = append(configs, tradefed.Object{"target_preparer", "com.android.tradefed.targetprep.StopServicesSetup", options})
 	}
-	if Bool(test.testDecorator.Properties.Isolated) {
+	if Bool(test.testDecorator.LinkerProperties.Isolated) {
 		configs = append(configs, tradefed.Option{Name: "not-shardable", Value: "true"})
 	}
 	if test.Properties.Test_options.Run_test_as != nil {
@@ -441,7 +454,7 @@
 	}
 
 	test.testConfig = tradefed.AutoGenNativeTestConfig(ctx, test.Properties.Test_config,
-		test.Properties.Test_config_template, test.Properties.Test_suites, configs, test.Properties.Auto_gen_config, testInstallBase)
+		test.Properties.Test_config_template, test.testDecorator.InstallerProperties.Test_suites, configs, test.Properties.Auto_gen_config, testInstallBase)
 
 	test.extraTestConfigs = android.PathsForModuleSrc(ctx, test.Properties.Test_options.Extra_test_configs)
 
@@ -466,8 +479,9 @@
 	binary.baseInstaller = NewTestInstaller()
 
 	test := &testBinary{
-		testDecorator: testDecorator{
-			linker: binary.baseLinker,
+		testDecorator: &testDecorator{
+			linker:    binary.baseLinker,
+			installer: binary.baseInstaller,
 		},
 		binaryDecorator: binary,
 		baseCompiler:    NewBaseCompiler(),
@@ -479,12 +493,14 @@
 }
 
 type testLibrary struct {
-	testDecorator
+	*testDecorator
 	*libraryDecorator
 }
 
 func (test *testLibrary) linkerProps() []interface{} {
-	return append(test.testDecorator.linkerProps(), test.libraryDecorator.linkerProps()...)
+	var props []interface{}
+	props = append(props, test.testDecorator.linkerProps()...)
+	return append(props, test.libraryDecorator.linkerProps()...)
 }
 
 func (test *testLibrary) linkerInit(ctx BaseModuleContext) {
@@ -504,16 +520,22 @@
 	return flags
 }
 
+func (test *testLibrary) installerProps() []interface{} {
+	return append(test.baseInstaller.installerProps(), test.testDecorator.installerProps()...)
+}
+
 func NewTestLibrary(hod android.HostOrDeviceSupported) *Module {
 	module, library := NewLibrary(android.HostAndDeviceSupported)
 	library.baseInstaller = NewTestInstaller()
 	test := &testLibrary{
-		testDecorator: testDecorator{
-			linker: library.baseLinker,
+		testDecorator: &testDecorator{
+			linker:    library.baseLinker,
+			installer: library.baseInstaller,
 		},
 		libraryDecorator: library,
 	}
 	module.linker = test
+	module.installer = test
 	return module
 }
 
diff --git a/cmd/sbox/sbox.go b/cmd/sbox/sbox.go
index 4fa7486..418826c 100644
--- a/cmd/sbox/sbox.go
+++ b/cmd/sbox/sbox.go
@@ -38,9 +38,11 @@
 )
 
 var (
-	sandboxesRoot string
-	manifestFile  string
-	keepOutDir    bool
+	sandboxesRoot  string
+	outputDir      string
+	manifestFile   string
+	keepOutDir     bool
+	writeIfChanged bool
 )
 
 const (
@@ -51,10 +53,14 @@
 func init() {
 	flag.StringVar(&sandboxesRoot, "sandbox-path", "",
 		"root of temp directory to put the sandbox into")
+	flag.StringVar(&outputDir, "output-dir", "",
+		"directory which will contain all output files and only output files")
 	flag.StringVar(&manifestFile, "manifest", "",
 		"textproto manifest describing the sandboxed command(s)")
 	flag.BoolVar(&keepOutDir, "keep-out-dir", false,
 		"whether to keep the sandbox directory when done")
+	flag.BoolVar(&writeIfChanged, "write-if-changed", false,
+		"only write the output files if they have changed")
 }
 
 func usageViolation(violation string) {
@@ -241,6 +247,12 @@
 		return "", fmt.Errorf("command is required")
 	}
 
+	// Remove files from the output directory
+	err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
+	if err != nil {
+		return "", err
+	}
+
 	pathToTempDirInSbox := tempDir
 	if command.GetChdir() {
 		pathToTempDirInSbox = "."
@@ -252,7 +264,7 @@
 	}
 
 	// Copy in any files specified by the manifest.
-	err = copyFiles(command.CopyBefore, "", tempDir, false)
+	err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
 	if err != nil {
 		return "", err
 	}
@@ -306,7 +318,7 @@
 		// especially useful for linters with baselines that print an error message on failure
 		// with a command to copy the output lint errors to the new baseline.  Use a copy instead of
 		// a move to leave the sandbox intact for manual inspection
-		copyFiles(command.CopyAfter, tempDir, "", true)
+		copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
 	}
 
 	// If the command  was executed but failed with an error, print a debugging message before
@@ -327,39 +339,16 @@
 		return "", err
 	}
 
-	missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir)
-
-	if len(missingOutputErrors) > 0 {
-		// find all created files for making a more informative error message
-		createdFiles := findAllFilesUnder(tempDir)
-
-		// build error message
-		errorMessage := "mismatch between declared and actual outputs\n"
-		errorMessage += "in sbox command(" + rawCommand + ")\n\n"
-		errorMessage += "in sandbox " + tempDir + ",\n"
-		errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
-		for _, missingOutputError := range missingOutputErrors {
-			errorMessage += "  " + missingOutputError.Error() + "\n"
-		}
-		if len(createdFiles) < 1 {
-			errorMessage += "created 0 files."
-		} else {
-			errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
-			creationMessages := createdFiles
-			maxNumCreationLines := 10
-			if len(creationMessages) > maxNumCreationLines {
-				creationMessages = creationMessages[:maxNumCreationLines]
-				creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
-			}
-			for _, creationMessage := range creationMessages {
-				errorMessage += "  " + creationMessage + "\n"
-			}
-		}
-
-		return "", errors.New(errorMessage)
+	err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
+	if err != nil {
+		return "", err
 	}
+
 	// the created files match the declared files; now move them
-	err = moveFiles(command.CopyAfter, tempDir, "")
+	err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
+	if err != nil {
+		return "", err
+	}
 
 	return depFile, nil
 }
@@ -380,8 +369,9 @@
 
 // validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
 // were created by the command.
-func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error {
+func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
 	var missingOutputErrors []error
+	var incorrectOutputDirectoryErrors []error
 	for _, copyPair := range copies {
 		fromPath := joinPath(sandboxDir, copyPair.GetFrom())
 		fileInfo, err := os.Stat(fromPath)
@@ -392,17 +382,91 @@
 		if fileInfo.IsDir() {
 			missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
 		}
+
+		toPath := copyPair.GetTo()
+		if rel, err := filepath.Rel(outputDir, toPath); err != nil {
+			return err
+		} else if strings.HasPrefix(rel, "../") {
+			incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
+				fmt.Errorf("%s is not under %s", toPath, outputDir))
+		}
 	}
-	return missingOutputErrors
+
+	const maxErrors = 10
+
+	if len(incorrectOutputDirectoryErrors) > 0 {
+		errorMessage := ""
+		more := 0
+		if len(incorrectOutputDirectoryErrors) > maxErrors {
+			more = len(incorrectOutputDirectoryErrors) - maxErrors
+			incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
+		}
+
+		for _, err := range incorrectOutputDirectoryErrors {
+			errorMessage += err.Error() + "\n"
+		}
+		if more > 0 {
+			errorMessage += fmt.Sprintf("...%v more", more)
+		}
+
+		return errors.New(errorMessage)
+	}
+
+	if len(missingOutputErrors) > 0 {
+		// find all created files for making a more informative error message
+		createdFiles := findAllFilesUnder(sandboxDir)
+
+		// build error message
+		errorMessage := "mismatch between declared and actual outputs\n"
+		errorMessage += "in sbox command(" + rawCommand + ")\n\n"
+		errorMessage += "in sandbox " + sandboxDir + ",\n"
+		errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
+		for _, missingOutputError := range missingOutputErrors {
+			errorMessage += "  " + missingOutputError.Error() + "\n"
+		}
+		if len(createdFiles) < 1 {
+			errorMessage += "created 0 files."
+		} else {
+			errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
+			creationMessages := createdFiles
+			if len(creationMessages) > maxErrors {
+				creationMessages = creationMessages[:maxErrors]
+				creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
+			}
+			for _, creationMessage := range creationMessages {
+				errorMessage += "  " + creationMessage + "\n"
+			}
+		}
+
+		return errors.New(errorMessage)
+	}
+
+	return nil
 }
 
-// copyFiles copies files in or out of the sandbox.  If allowFromNotExists is true then errors
-// caused by a from path not existing are ignored.
-func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, allowFromNotExists bool) error {
+type existsType bool
+
+const (
+	requireFromExists  existsType = false
+	allowFromNotExists            = true
+)
+
+type writeType bool
+
+const (
+	alwaysWrite        writeType = false
+	onlyWriteIfChanged           = true
+)
+
+// copyFiles copies files in or out of the sandbox.  If exists is allowFromNotExists then errors
+// caused by a from path not existing are ignored.  If write is onlyWriteIfChanged then the output
+// file is compared to the input file and not written to if it is the same, avoiding updating
+// the timestamp.
+func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
 	for _, copyPair := range copies {
 		fromPath := joinPath(fromDir, copyPair.GetFrom())
 		toPath := joinPath(toDir, copyPair.GetTo())
-		err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), allowFromNotExists)
+		err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
 		if err != nil {
 			return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
 		}
@@ -411,8 +475,11 @@
 }
 
 // copyOneFile copies a file and its permissions.  If forceExecutable is true it adds u+x to the
-// permissions.  If allowFromNotExists is true it returns nil if the from path doesn't exist.
-func copyOneFile(from string, to string, forceExecutable, allowFromNotExists bool) error {
+// permissions.  If exists is allowFromNotExists it returns nil if the from path doesn't exist.
+// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to
+// if it is the same, avoiding updating the timestamp.
+func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
+	write writeType) error {
 	err := os.MkdirAll(filepath.Dir(to), 0777)
 	if err != nil {
 		return err
@@ -420,7 +487,7 @@
 
 	stat, err := os.Stat(from)
 	if err != nil {
-		if os.IsNotExist(err) && allowFromNotExists {
+		if os.IsNotExist(err) && exists == allowFromNotExists {
 			return nil
 		}
 		return err
@@ -431,6 +498,10 @@
 		perm = perm | 0100 // u+x
 	}
 
+	if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
+		return nil
+	}
+
 	in, err := os.Open(from)
 	if err != nil {
 		return err
@@ -504,7 +575,7 @@
 		to := applyPathMappings(rspFile.PathMappings, from)
 
 		// Copy the file into the sandbox.
-		err := copyOneFile(from, joinPath(toDir, to), false, false)
+		err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
 		if err != nil {
 			return err
 		}
@@ -551,9 +622,10 @@
 
 // moveFiles moves files specified by a set of copy rules.  It uses os.Rename, so it is restricted
 // to moving files where the source and destination are in the same filesystem.  This is OK for
-// sbox because the temporary directory is inside the out directory.  It updates the timestamp
-// of the new file.
-func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
+// sbox because the temporary directory is inside the out directory.  If write is onlyWriteIfChanged
+// then the output file is compared to the input file and not written to if it is the same, avoiding
+// updating the timestamp.  Otherwise it always updates the timestamp of the new file.
+func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
 	for _, copyPair := range copies {
 		fromPath := joinPath(fromDir, copyPair.GetFrom())
 		toPath := joinPath(toDir, copyPair.GetTo())
@@ -562,6 +634,10 @@
 			return err
 		}
 
+		if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
+			continue
+		}
+
 		err = os.Rename(fromPath, toPath)
 		if err != nil {
 			return err
@@ -578,6 +654,37 @@
 	return nil
 }
 
+// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
+// any files not listed in copies if write is onlyWriteIfChanged
+func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
+	if outputDir == "" {
+		return fmt.Errorf("output directory must be set")
+	}
+
+	if write == alwaysWrite {
+		// When writing all the output files remove the whole output directory
+		return os.RemoveAll(outputDir)
+	}
+
+	outputFiles := make(map[string]bool, len(copies))
+	for _, copyPair := range copies {
+		outputFiles[copyPair.GetTo()] = true
+	}
+
+	existingFiles := findAllFilesUnder(outputDir)
+	for _, existingFile := range existingFiles {
+		fullExistingFile := filepath.Join(outputDir, existingFile)
+		if !outputFiles[fullExistingFile] {
+			err := os.Remove(fullExistingFile)
+			if err != nil {
+				return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
+			}
+		}
+	}
+
+	return nil
+}
+
 // Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
 // to an output file.
 func rewriteDepFiles(ins []string, out string) error {
@@ -621,6 +728,66 @@
 	return filepath.Join(dir, file)
 }
 
+// filesHaveSameContents compares the contents if two files, returning true if they are the same
+// and returning false if they are different or any errors occur.
+func filesHaveSameContents(a, b string) bool {
+	// Compare the sizes of the two files
+	statA, err := os.Stat(a)
+	if err != nil {
+		return false
+	}
+	statB, err := os.Stat(b)
+	if err != nil {
+		return false
+	}
+
+	if statA.Size() != statB.Size() {
+		return false
+	}
+
+	// Open the two files
+	fileA, err := os.Open(a)
+	if err != nil {
+		return false
+	}
+	defer fileA.Close()
+	fileB, err := os.Open(a)
+	if err != nil {
+		return false
+	}
+	defer fileB.Close()
+
+	// Compare the files 1MB at a time
+	const bufSize = 1 * 1024 * 1024
+	bufA := make([]byte, bufSize)
+	bufB := make([]byte, bufSize)
+
+	remain := statA.Size()
+	for remain > 0 {
+		toRead := int64(bufSize)
+		if toRead > remain {
+			toRead = remain
+		}
+
+		_, err = io.ReadFull(fileA, bufA[:toRead])
+		if err != nil {
+			return false
+		}
+		_, err = io.ReadFull(fileB, bufB[:toRead])
+		if err != nil {
+			return false
+		}
+
+		if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
+			return false
+		}
+
+		remain -= toRead
+	}
+
+	return true
+}
+
 func makeAbsPathEnv(pathEnv string) (string, error) {
 	pathEnvElements := filepath.SplitList(pathEnv)
 	for i, p := range pathEnvElements {
diff --git a/cmd/symbols_map/Android.bp b/cmd/symbols_map/Android.bp
new file mode 100644
index 0000000..0ba3b07
--- /dev/null
+++ b/cmd/symbols_map/Android.bp
@@ -0,0 +1,34 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+blueprint_go_binary {
+    name: "symbols_map",
+    srcs: [
+        "elf.go",
+        "r8.go",
+        "symbols_map.go",
+    ],
+    testSrcs: [
+        "elf_test.go",
+        "r8_test.go",
+    ],
+    deps: [
+        "blueprint-pathtools",
+        "golang-protobuf-encoding-prototext",
+        "soong-response",
+        "symbols_map_proto",
+    ],
+}
+
+bootstrap_go_package {
+    name: "symbols_map_proto",
+    pkgPath: "android/soong/cmd/symbols_map/symbols_map_proto",
+    deps: [
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+    ],
+    srcs: [
+        "symbols_map_proto/symbols_map.pb.go",
+    ],
+}
diff --git a/cmd/symbols_map/elf.go b/cmd/symbols_map/elf.go
new file mode 100644
index 0000000..b38896a
--- /dev/null
+++ b/cmd/symbols_map/elf.go
@@ -0,0 +1,95 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"debug/elf"
+	"encoding/binary"
+	"encoding/hex"
+	"fmt"
+	"io"
+)
+
+const gnuBuildID = "GNU\x00"
+
+// elfIdentifier extracts the elf build ID from an elf file.  If allowMissing is true it returns
+// an empty identifier if the file exists but the build ID note does not.
+func elfIdentifier(filename string, allowMissing bool) (string, error) {
+	f, err := elf.Open(filename)
+	if err != nil {
+		return "", fmt.Errorf("failed to open %s: %w", filename, err)
+	}
+	defer f.Close()
+
+	buildIDNote := f.Section(".note.gnu.build-id")
+	if buildIDNote == nil {
+		if allowMissing {
+			return "", nil
+		}
+		return "", fmt.Errorf("failed to find .note.gnu.build-id in  %s", filename)
+	}
+
+	buildIDs, err := readNote(buildIDNote.Open(), f.ByteOrder)
+	if err != nil {
+		return "", fmt.Errorf("failed to read .note.gnu.build-id: %w", err)
+	}
+
+	for name, desc := range buildIDs {
+		if name == gnuBuildID {
+			return hex.EncodeToString(desc), nil
+		}
+	}
+
+	return "", nil
+}
+
+// readNote reads the contents of a note section, returning it as a map from name to descriptor.
+func readNote(note io.Reader, byteOrder binary.ByteOrder) (map[string][]byte, error) {
+	var noteHeader struct {
+		Namesz uint32
+		Descsz uint32
+		Type   uint32
+	}
+
+	notes := make(map[string][]byte)
+	for {
+		err := binary.Read(note, byteOrder, &noteHeader)
+		if err != nil {
+			if err == io.EOF {
+				return notes, nil
+			}
+			return nil, fmt.Errorf("failed to read note header: %w", err)
+		}
+
+		nameBuf := make([]byte, align4(noteHeader.Namesz))
+		err = binary.Read(note, byteOrder, &nameBuf)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read note name: %w", err)
+		}
+		name := string(nameBuf[:noteHeader.Namesz])
+
+		descBuf := make([]byte, align4(noteHeader.Descsz))
+		err = binary.Read(note, byteOrder, &descBuf)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read note desc: %w", err)
+		}
+		notes[name] = descBuf[:noteHeader.Descsz]
+	}
+}
+
+// align4 rounds the input up to the next multiple of 4.
+func align4(i uint32) uint32 {
+	return (i + 3) &^ 3
+}
diff --git a/cmd/symbols_map/elf_test.go b/cmd/symbols_map/elf_test.go
new file mode 100644
index 0000000..e616228
--- /dev/null
+++ b/cmd/symbols_map/elf_test.go
@@ -0,0 +1,45 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"bytes"
+	"encoding/binary"
+	"reflect"
+	"testing"
+)
+
+func Test_readNote(t *testing.T) {
+	note := []byte{
+		0x04, 0x00, 0x00, 0x00,
+		0x10, 0x00, 0x00, 0x00,
+		0x03, 0x00, 0x00, 0x00,
+		0x47, 0x4e, 0x55, 0x00,
+		0xca, 0xaf, 0x44, 0xd2, 0x82, 0x78, 0x68, 0xfe, 0xc0, 0x90, 0xa3, 0x43, 0x85, 0x36, 0x6c, 0xc7,
+	}
+
+	descs, err := readNote(bytes.NewBuffer(note), binary.LittleEndian)
+	if err != nil {
+		t.Fatalf("unexpected error in readNote: %s", err)
+	}
+
+	expectedDescs := map[string][]byte{
+		"GNU\x00": []byte{0xca, 0xaf, 0x44, 0xd2, 0x82, 0x78, 0x68, 0xfe, 0xc0, 0x90, 0xa3, 0x43, 0x85, 0x36, 0x6c, 0xc7},
+	}
+
+	if !reflect.DeepEqual(descs, expectedDescs) {
+		t.Errorf("incorrect return, want %#v got %#v", expectedDescs, descs)
+	}
+}
diff --git a/cmd/symbols_map/r8.go b/cmd/symbols_map/r8.go
new file mode 100644
index 0000000..6f73e09
--- /dev/null
+++ b/cmd/symbols_map/r8.go
@@ -0,0 +1,56 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+const hashPrefix = "# pg_map_hash: "
+const hashTypePrefix = "SHA-256 "
+const commentPrefix = "#"
+
+// r8Identifier extracts the hash from the comments of a dictionary produced by R8. It returns
+// an empty identifier if no matching comment was found before the first non-comment line.
+func r8Identifier(filename string) (string, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return "", fmt.Errorf("failed to open %s: %w", filename, err)
+	}
+	defer f.Close()
+
+	return extractR8CompilerHash(f)
+}
+
+func extractR8CompilerHash(r io.Reader) (string, error) {
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		line := s.Text()
+		if strings.HasPrefix(line, hashPrefix) {
+			hash := strings.TrimPrefix(line, hashPrefix)
+			if !strings.HasPrefix(hash, hashTypePrefix) {
+				return "", fmt.Errorf("invalid hash type found in %q", line)
+			}
+			return strings.TrimPrefix(hash, hashTypePrefix), nil
+		} else if !strings.HasPrefix(line, commentPrefix) {
+			break
+		}
+	}
+	return "", nil
+}
diff --git a/cmd/symbols_map/r8_test.go b/cmd/symbols_map/r8_test.go
new file mode 100644
index 0000000..5712da9
--- /dev/null
+++ b/cmd/symbols_map/r8_test.go
@@ -0,0 +1,91 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+)
+
+func Test_extractR8CompilerHash(t *testing.T) {
+	testCases := []struct {
+		name string
+		data string
+
+		hash string
+		err  string
+	}{
+		{
+			name: "simple",
+			data: `# compiler: R8
+# compiler_version: 3.3.18-dev
+# min_api: 10000
+# compiler_hash: bab44c1a04a2201b55fe10394f477994205c34e0
+# common_typos_disable
+# {"id":"com.android.tools.r8.mapping","version":"2.0"}
+# pg_map_id: 7fe8b95
+# pg_map_hash: SHA-256 7fe8b95ae71f179f63d2a585356fb9cf2c8fb94df9c9dd50621ffa6d9e9e88da
+android.car.userlib.UserHelper -> android.car.userlib.UserHelper:
+`,
+			hash: "7fe8b95ae71f179f63d2a585356fb9cf2c8fb94df9c9dd50621ffa6d9e9e88da",
+		},
+		{
+			name: "empty",
+			data: ``,
+			hash: "",
+		},
+		{
+			name: "non comment line",
+			data: `# compiler: R8
+# compiler_version: 3.3.18-dev
+# min_api: 10000
+# compiler_hash: bab44c1a04a2201b55fe10394f477994205c34e0
+# common_typos_disable
+# {"id":"com.android.tools.r8.mapping","version":"2.0"}
+# pg_map_id: 7fe8b95
+android.car.userlib.UserHelper -> android.car.userlib.UserHelper:
+# pg_map_hash: SHA-256 7fe8b95ae71f179f63d2a585356fb9cf2c8fb94df9c9dd50621ffa6d9e9e88da
+`,
+			hash: "",
+		},
+		{
+			name: "invalid hash",
+			data: `# pg_map_hash: foobar 7fe8b95ae71f179f63d2a585356fb9cf2c8fb94df9c9dd50621ffa6d9e9e88da`,
+			err:  "invalid hash type",
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			hash, err := extractR8CompilerHash(bytes.NewBufferString(tt.data))
+			if err != nil {
+				if tt.err != "" {
+					if !strings.Contains(err.Error(), tt.err) {
+						t.Fatalf("incorrect error in extractR8CompilerHash, want %s got %s", tt.err, err)
+					}
+				} else {
+					t.Fatalf("unexpected error in extractR8CompilerHash: %s", err)
+				}
+			} else if tt.err != "" {
+				t.Fatalf("missing error in extractR8CompilerHash, want %s", tt.err)
+			}
+
+			if g, w := hash, tt.hash; g != w {
+				t.Errorf("incorrect hash, want %q got %q", w, g)
+			}
+		})
+	}
+}
diff --git a/cmd/symbols_map/symbols_map.go b/cmd/symbols_map/symbols_map.go
new file mode 100644
index 0000000..938446d
--- /dev/null
+++ b/cmd/symbols_map/symbols_map.go
@@ -0,0 +1,202 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"android/soong/cmd/symbols_map/symbols_map_proto"
+	"android/soong/response"
+
+	"github.com/google/blueprint/pathtools"
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
+)
+
+// This tool is used to extract a hash from an elf file or an r8 dictionary and store it as a
+// textproto, or to merge multiple textprotos together.
+
+func main() {
+	var expandedArgs []string
+	for _, arg := range os.Args[1:] {
+		if strings.HasPrefix(arg, "@") {
+			f, err := os.Open(strings.TrimPrefix(arg, "@"))
+			if err != nil {
+				fmt.Fprintln(os.Stderr, err.Error())
+				os.Exit(1)
+			}
+
+			respArgs, err := response.ReadRspFile(f)
+			f.Close()
+			if err != nil {
+				fmt.Fprintln(os.Stderr, err.Error())
+				os.Exit(1)
+			}
+			expandedArgs = append(expandedArgs, respArgs...)
+		} else {
+			expandedArgs = append(expandedArgs, arg)
+		}
+	}
+
+	flags := flag.NewFlagSet("flags", flag.ExitOnError)
+
+	// Hide the flag package to prevent accidental references to flag instead of flags.
+	flag := struct{}{}
+	_ = flag
+
+	flags.Usage = func() {
+		fmt.Fprintf(flags.Output(), "Usage of %s:\n", os.Args[0])
+		fmt.Fprintf(flags.Output(), "  %s -elf|-r8 <input file> [-write_if_changed] <output file>\n", os.Args[0])
+		fmt.Fprintf(flags.Output(), "  %s -merge <output file> [-write_if_changed] [-ignore_missing_files] [-strip_prefix <prefix>] [<input file>...]\n", os.Args[0])
+		fmt.Fprintln(flags.Output())
+
+		flags.PrintDefaults()
+	}
+
+	elfFile := flags.String("elf", "", "extract identifier from an elf file")
+	r8File := flags.String("r8", "", "extract identifier from an r8 dictionary")
+	merge := flags.String("merge", "", "merge multiple identifier protos")
+
+	writeIfChanged := flags.Bool("write_if_changed", false, "only write output file if it is modified")
+	ignoreMissingFiles := flags.Bool("ignore_missing_files", false, "ignore missing input files in merge mode")
+	stripPrefix := flags.String("strip_prefix", "", "prefix to strip off of the location field in merge mode")
+
+	flags.Parse(expandedArgs)
+
+	if *merge != "" {
+		// If merge mode was requested perform the merge and exit early.
+		err := mergeProtos(*merge, flags.Args(), *stripPrefix, *writeIfChanged, *ignoreMissingFiles)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to merge protos: %s", err)
+			os.Exit(1)
+		}
+		os.Exit(0)
+	}
+
+	if *elfFile == "" && *r8File == "" {
+		fmt.Fprintf(os.Stderr, "-elf or -r8 argument is required\n")
+		flags.Usage()
+		os.Exit(1)
+	}
+
+	if *elfFile != "" && *r8File != "" {
+		fmt.Fprintf(os.Stderr, "only one of -elf or -r8 argument is allowed\n")
+		flags.Usage()
+		os.Exit(1)
+	}
+
+	if flags.NArg() != 1 {
+		flags.Usage()
+		os.Exit(1)
+	}
+
+	output := flags.Arg(0)
+
+	var identifier string
+	var location string
+	var typ symbols_map_proto.Mapping_Type
+	var err error
+
+	if *elfFile != "" {
+		typ = symbols_map_proto.Mapping_ELF
+		location = *elfFile
+		identifier, err = elfIdentifier(*elfFile, true)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "error reading elf identifier: %s\n", err)
+			os.Exit(1)
+		}
+	} else if *r8File != "" {
+		typ = symbols_map_proto.Mapping_R8
+		identifier, err = r8Identifier(*r8File)
+		location = *r8File
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "error reading r8 identifier: %s\n", err)
+			os.Exit(1)
+		}
+	} else {
+		panic("shouldn't get here")
+	}
+
+	mapping := symbols_map_proto.Mapping{
+		Identifier: proto.String(identifier),
+		Location:   proto.String(location),
+		Type:       typ.Enum(),
+	}
+
+	err = writeTextProto(output, &mapping, *writeIfChanged)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error writing output: %s\n", err)
+		os.Exit(1)
+	}
+}
+
+// writeTextProto writes a proto to an output file as a textproto, optionally leaving the file
+// unmodified if it was already up to date.
+func writeTextProto(output string, message proto.Message, writeIfChanged bool) error {
+	marshaller := prototext.MarshalOptions{Multiline: true}
+	data, err := marshaller.Marshal(message)
+	if err != nil {
+		return fmt.Errorf("error marshalling textproto: %w", err)
+	}
+
+	if writeIfChanged {
+		err = pathtools.WriteFileIfChanged(output, data, 0666)
+	} else {
+		err = ioutil.WriteFile(output, data, 0666)
+	}
+
+	if err != nil {
+		return fmt.Errorf("error writing to %s: %w\n", output, err)
+	}
+
+	return nil
+}
+
+// mergeProtos merges a list of textproto files containing Mapping messages into a single textproto
+// containing a Mappings message.
+func mergeProtos(output string, inputs []string, stripPrefix string, writeIfChanged bool, ignoreMissingFiles bool) error {
+	mappings := symbols_map_proto.Mappings{}
+	for _, input := range inputs {
+		mapping := symbols_map_proto.Mapping{}
+		data, err := ioutil.ReadFile(input)
+		if err != nil {
+			if ignoreMissingFiles && os.IsNotExist(err) {
+				// Merge mode is used on a list of files in the packaging directory.  If multiple
+				// goals are included on the build command line, for example `dist` and `tests`,
+				// then the symbols packaging rule for `dist` can run while a dependency of `tests`
+				// is modifying the symbols packaging directory.  That can result in a file that
+				// existed when the file list was generated being deleted as part of updating it,
+				// resulting in sporadic ENOENT errors.  Ignore them if -ignore_missing_files
+				// was passed on the command line.
+				continue
+			}
+			return fmt.Errorf("failed to read %s: %w", input, err)
+		}
+		err = prototext.Unmarshal(data, &mapping)
+		if err != nil {
+			return fmt.Errorf("failed to parse textproto %s: %w", input, err)
+		}
+		if stripPrefix != "" && mapping.Location != nil {
+			mapping.Location = proto.String(strings.TrimPrefix(*mapping.Location, stripPrefix))
+		}
+		mappings.Mappings = append(mappings.Mappings, &mapping)
+	}
+
+	return writeTextProto(output, &mappings, writeIfChanged)
+}
diff --git a/cmd/symbols_map/symbols_map_proto/symbols_map.pb.go b/cmd/symbols_map/symbols_map_proto/symbols_map.pb.go
new file mode 100644
index 0000000..f9c0ce5
--- /dev/null
+++ b/cmd/symbols_map/symbols_map_proto/symbols_map.pb.go
@@ -0,0 +1,315 @@
+// Copyright 2022 Google Inc. All Rights Reserved.
+//
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.27.1
+// 	protoc        v3.9.1
+// source: symbols_map.proto
+
+package symbols_map_proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Type is the valid types of a mapping.
+type Mapping_Type int32
+
+const (
+	// ELF denotes a mapping from an elf build ID to an unstripped elf file.
+	Mapping_ELF Mapping_Type = 0
+	// R8 denotes a mapping from an R8 dictionary hash to an R8 dictionary.
+	Mapping_R8 Mapping_Type = 1
+)
+
+// Enum value maps for Mapping_Type.
+var (
+	Mapping_Type_name = map[int32]string{
+		0: "ELF",
+		1: "R8",
+	}
+	Mapping_Type_value = map[string]int32{
+		"ELF": 0,
+		"R8":  1,
+	}
+)
+
+func (x Mapping_Type) Enum() *Mapping_Type {
+	p := new(Mapping_Type)
+	*p = x
+	return p
+}
+
+func (x Mapping_Type) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Mapping_Type) Descriptor() protoreflect.EnumDescriptor {
+	return file_symbols_map_proto_enumTypes[0].Descriptor()
+}
+
+func (Mapping_Type) Type() protoreflect.EnumType {
+	return &file_symbols_map_proto_enumTypes[0]
+}
+
+func (x Mapping_Type) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Do not use.
+func (x *Mapping_Type) UnmarshalJSON(b []byte) error {
+	num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
+	if err != nil {
+		return err
+	}
+	*x = Mapping_Type(num)
+	return nil
+}
+
+// Deprecated: Use Mapping_Type.Descriptor instead.
+func (Mapping_Type) EnumDescriptor() ([]byte, []int) {
+	return file_symbols_map_proto_rawDescGZIP(), []int{0, 0}
+}
+
+type Mapping struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// identifier is a unique identifier of a location, generally the hash of the file.  For an
+	// elf file it is the elf build ID, for an R8 dictionary it is the hash from the comments in the
+	// top of the file.  It may be empty if no hash could be extracted from the file.
+	Identifier *string `protobuf:"bytes,1,opt,name=identifier" json:"identifier,omitempty"`
+	// location is the path to the file with the given identifier.  The location should be valid
+	// both on the local disk and in the distributed symbols.zip or proguard_dict.zip files.
+	Location *string `protobuf:"bytes,2,opt,name=location" json:"location,omitempty"`
+	// type is the type of the mapping, either ELF or R8.
+	Type *Mapping_Type `protobuf:"varint,3,opt,name=type,enum=symbols_map.Mapping_Type" json:"type,omitempty"`
+}
+
+func (x *Mapping) Reset() {
+	*x = Mapping{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_symbols_map_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Mapping) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Mapping) ProtoMessage() {}
+
+func (x *Mapping) ProtoReflect() protoreflect.Message {
+	mi := &file_symbols_map_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Mapping.ProtoReflect.Descriptor instead.
+func (*Mapping) Descriptor() ([]byte, []int) {
+	return file_symbols_map_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Mapping) GetIdentifier() string {
+	if x != nil && x.Identifier != nil {
+		return *x.Identifier
+	}
+	return ""
+}
+
+func (x *Mapping) GetLocation() string {
+	if x != nil && x.Location != nil {
+		return *x.Location
+	}
+	return ""
+}
+
+func (x *Mapping) GetType() Mapping_Type {
+	if x != nil && x.Type != nil {
+		return *x.Type
+	}
+	return Mapping_ELF
+}
+
+type Mappings struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Mappings []*Mapping `protobuf:"bytes,4,rep,name=mappings" json:"mappings,omitempty"`
+}
+
+func (x *Mappings) Reset() {
+	*x = Mappings{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_symbols_map_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Mappings) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Mappings) ProtoMessage() {}
+
+func (x *Mappings) ProtoReflect() protoreflect.Message {
+	mi := &file_symbols_map_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Mappings.ProtoReflect.Descriptor instead.
+func (*Mappings) Descriptor() ([]byte, []int) {
+	return file_symbols_map_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *Mappings) GetMappings() []*Mapping {
+	if x != nil {
+		return x.Mappings
+	}
+	return nil
+}
+
+var File_symbols_map_proto protoreflect.FileDescriptor
+
+var file_symbols_map_proto_rawDesc = []byte{
+	0x0a, 0x11, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x5f, 0x6d, 0x61, 0x70,
+	0x22, 0x8d, 0x01, 0x0a, 0x07, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1e, 0x0a, 0x0a,
+	0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08,
+	0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+	0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73,
+	0x5f, 0x6d, 0x61, 0x70, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x54, 0x79, 0x70,
+	0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x17, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
+	0x07, 0x0a, 0x03, 0x45, 0x4c, 0x46, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x52, 0x38, 0x10, 0x01,
+	0x22, 0x3c, 0x0a, 0x08, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x30, 0x0a, 0x08,
+	0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14,
+	0x2e, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x2e, 0x4d, 0x61, 0x70,
+	0x70, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x31,
+	0x5a, 0x2f, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f,
+	0x63, 0x6d, 0x64, 0x2f, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x2f,
+	0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x5f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f,
+}
+
+var (
+	file_symbols_map_proto_rawDescOnce sync.Once
+	file_symbols_map_proto_rawDescData = file_symbols_map_proto_rawDesc
+)
+
+func file_symbols_map_proto_rawDescGZIP() []byte {
+	file_symbols_map_proto_rawDescOnce.Do(func() {
+		file_symbols_map_proto_rawDescData = protoimpl.X.CompressGZIP(file_symbols_map_proto_rawDescData)
+	})
+	return file_symbols_map_proto_rawDescData
+}
+
+var file_symbols_map_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_symbols_map_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_symbols_map_proto_goTypes = []interface{}{
+	(Mapping_Type)(0), // 0: symbols_map.Mapping.Type
+	(*Mapping)(nil),   // 1: symbols_map.Mapping
+	(*Mappings)(nil),  // 2: symbols_map.Mappings
+}
+var file_symbols_map_proto_depIdxs = []int32{
+	0, // 0: symbols_map.Mapping.type:type_name -> symbols_map.Mapping.Type
+	1, // 1: symbols_map.Mappings.mappings:type_name -> symbols_map.Mapping
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_symbols_map_proto_init() }
+func file_symbols_map_proto_init() {
+	if File_symbols_map_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_symbols_map_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Mapping); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_symbols_map_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Mappings); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_symbols_map_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_symbols_map_proto_goTypes,
+		DependencyIndexes: file_symbols_map_proto_depIdxs,
+		EnumInfos:         file_symbols_map_proto_enumTypes,
+		MessageInfos:      file_symbols_map_proto_msgTypes,
+	}.Build()
+	File_symbols_map_proto = out.File
+	file_symbols_map_proto_rawDesc = nil
+	file_symbols_map_proto_goTypes = nil
+	file_symbols_map_proto_depIdxs = nil
+}
diff --git a/cmd/symbols_map/symbols_map_proto/symbols_map.proto b/cmd/symbols_map/symbols_map_proto/symbols_map.proto
new file mode 100644
index 0000000..693fe3e
--- /dev/null
+++ b/cmd/symbols_map/symbols_map_proto/symbols_map.proto
@@ -0,0 +1,44 @@
+// Copyright 2022 Google Inc. All Rights Reserved.
+//
+// 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.
+
+syntax = "proto2";
+
+package symbols_map;
+option go_package = "android/soong/cmd/symbols_map/symbols_map_proto";
+
+message Mapping {
+  // identifier is a unique identifier of a location, generally the hash of the file.  For an
+  // elf file it is the elf build ID, for an R8 dictionary it is the hash from the comments in the
+  // top of the file.  It may be empty if no hash could be extracted from the file.
+  optional string identifier = 1;
+
+  // location is the path to the file with the given identifier.  The location should be valid
+  // both on the local disk and in the distributed symbols.zip or proguard_dict.zip files.
+  optional string location = 2;
+
+  // Type is the valid types of a mapping.
+  enum Type {
+    // ELF denotes a mapping from an elf build ID to an unstripped elf file.
+    ELF = 0;
+    // R8 denotes a mapping from an R8 dictionary hash to an R8 dictionary.
+    R8 = 1;
+  }
+
+  // type is the type of the mapping, either ELF or R8.
+  optional Type type = 3;
+}
+
+message Mappings {
+  repeated Mapping mappings = 4;
+}
\ No newline at end of file
diff --git a/cmd/symbols_map/symbols_map_test.go b/cmd/symbols_map/symbols_map_test.go
new file mode 100644
index 0000000..754b7ef
--- /dev/null
+++ b/cmd/symbols_map/symbols_map_test.go
@@ -0,0 +1,217 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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 main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"android/soong/cmd/symbols_map/symbols_map_proto"
+
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
+)
+
+func Test_mergeProtos(t *testing.T) {
+	type testFile struct {
+		filename string
+		contents *symbols_map_proto.Mapping
+		missing  bool
+	}
+
+	tests := []struct {
+		name               string
+		inputs             []testFile
+		stripPrefix        string
+		writeIfChanged     bool
+		ignoreMissingFiles bool
+
+		error  string
+		output *symbols_map_proto.Mappings
+	}{
+		{
+			name:   "empty",
+			output: &symbols_map_proto.Mappings{},
+		},
+		{
+			name: "merge",
+			inputs: []testFile{
+				{
+					filename: "foo",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+				},
+				{
+					filename: "bar",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("bar"),
+						Location:   proto.String("symbols/bar"),
+						Type:       symbols_map_proto.Mapping_R8.Enum(),
+					},
+				},
+			},
+			output: &symbols_map_proto.Mappings{
+				Mappings: []*symbols_map_proto.Mapping{
+					{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+					{
+						Identifier: proto.String("bar"),
+						Location:   proto.String("symbols/bar"),
+						Type:       symbols_map_proto.Mapping_R8.Enum(),
+					},
+				},
+			},
+		},
+		{
+			name: "strip prefix",
+			inputs: []testFile{
+				{
+					filename: "foo",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+				},
+				{
+					filename: "bar",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("bar"),
+						Location:   proto.String("symbols/bar"),
+						Type:       symbols_map_proto.Mapping_R8.Enum(),
+					},
+				},
+			},
+			stripPrefix: "symbols/",
+			output: &symbols_map_proto.Mappings{
+				Mappings: []*symbols_map_proto.Mapping{
+					{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+					{
+						Identifier: proto.String("bar"),
+						Location:   proto.String("bar"),
+						Type:       symbols_map_proto.Mapping_R8.Enum(),
+					},
+				},
+			},
+		},
+		{
+			name: "missing",
+			inputs: []testFile{
+				{
+					filename: "foo",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+				},
+				{
+					filename: "bar",
+					missing:  true,
+				},
+			},
+			error: "no such file or directory",
+		},
+		{
+			name: "ignore missing",
+			inputs: []testFile{
+				{
+					filename: "foo",
+					contents: &symbols_map_proto.Mapping{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+				},
+				{
+					filename: "bar",
+					missing:  true,
+				},
+			},
+			ignoreMissingFiles: true,
+			output: &symbols_map_proto.Mappings{
+				Mappings: []*symbols_map_proto.Mapping{
+					{
+						Identifier: proto.String("foo"),
+						Location:   proto.String("symbols/foo"),
+						Type:       symbols_map_proto.Mapping_ELF.Enum(),
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			dir, err := os.MkdirTemp("", "test_mergeProtos")
+			if err != nil {
+				t.Fatalf("failed to create temporary directory: %s", err)
+			}
+			defer os.RemoveAll(dir)
+
+			var inputs []string
+			for _, in := range tt.inputs {
+				path := filepath.Join(dir, in.filename)
+				inputs = append(inputs, path)
+				if !in.missing {
+					err := writeTextProto(path, in.contents, false)
+					if err != nil {
+						t.Fatalf("failed to create input file %s: %s", path, err)
+					}
+				}
+			}
+			output := filepath.Join(dir, "out")
+
+			err = mergeProtos(output, inputs, tt.stripPrefix, tt.writeIfChanged, tt.ignoreMissingFiles)
+			if err != nil {
+				if tt.error != "" {
+					if !strings.Contains(err.Error(), tt.error) {
+						t.Fatalf("expected error %q, got %s", tt.error, err.Error())
+					}
+				} else {
+					t.Fatalf("unexpected error %q", err)
+				}
+			} else if tt.error != "" {
+				t.Fatalf("missing error %q", tt.error)
+			} else {
+				data, err := ioutil.ReadFile(output)
+				if err != nil {
+					t.Fatalf("failed to read output file %s: %s", output, err)
+				}
+				var got symbols_map_proto.Mappings
+				err = prototext.Unmarshal(data, &got)
+				if err != nil {
+					t.Fatalf("failed to unmarshal textproto %s: %s", output, err)
+				}
+
+				if !proto.Equal(tt.output, &got) {
+					t.Fatalf("expected output %q, got %q", tt.output.String(), got.String())
+				}
+			}
+		})
+	}
+}
diff --git a/java/app.go b/java/app.go
index 5b1daa4..21ee34e 100755
--- a/java/app.go
+++ b/java/app.go
@@ -586,10 +586,16 @@
 	}
 	a.onDeviceDir = android.InstallPathToOnDevicePath(ctx, a.installDir)
 
-	noticeFile := android.PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
-	android.BuildNoticeHtmlOutputFromLicenseMetadata(ctx, noticeFile)
 	if Bool(a.appProperties.Embed_notices) || ctx.Config().IsEnvTrue("ALWAYS_EMBED_NOTICES") {
-		a.aapt.noticeFile = android.OptionalPathForPath(noticeFile)
+		noticeFile := android.PathForModuleOut(ctx, "NOTICE.html.gz")
+		android.BuildNoticeHtmlOutputFromLicenseMetadata(ctx, noticeFile)
+		noticeAssetPath := android.PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
+		builder := android.NewRuleBuilder(pctx, ctx)
+		builder.Command().Text("cp").
+			Input(noticeFile).
+			Output(noticeAssetPath)
+		builder.Build("notice_dir", "Building notice dir")
+		a.aapt.noticeFile = android.OptionalPathForPath(noticeAssetPath)
 	}
 
 	a.classLoaderContexts = a.usesLibrary.classLoaderContextForUsesLibDeps(ctx)
diff --git a/java/base.go b/java/base.go
index 2f425cd..8b258d6 100644
--- a/java/base.go
+++ b/java/base.go
@@ -1052,6 +1052,7 @@
 	j.expandIDEInfoCompiledSrcs = append(j.expandIDEInfoCompiledSrcs, uniqueSrcFiles.Strings()...)
 
 	var kotlinJars android.Paths
+	var kotlinHeaderJars android.Paths
 
 	if srcFiles.HasExt(".kt") {
 		// user defined kotlin flags.
@@ -1109,18 +1110,22 @@
 		}
 
 		kotlinJar := android.PathForModuleOut(ctx, "kotlin", jarName)
-		kotlinCompile(ctx, kotlinJar, kotlinSrcFiles, kotlinCommonSrcFiles, srcJars, flags)
+		kotlinHeaderJar := android.PathForModuleOut(ctx, "kotlin_headers", jarName)
+		kotlinCompile(ctx, kotlinJar, kotlinHeaderJar, kotlinSrcFiles, kotlinCommonSrcFiles, srcJars, flags)
 		if ctx.Failed() {
 			return
 		}
 
 		// Make javac rule depend on the kotlinc rule
-		flags.classpath = append(flags.classpath, kotlinJar)
+		flags.classpath = append(classpath{kotlinHeaderJar}, flags.classpath...)
 
 		kotlinJars = append(kotlinJars, kotlinJar)
+		kotlinHeaderJars = append(kotlinHeaderJars, kotlinHeaderJar)
+
 		// Jar kotlin classes into the final jar after javac
 		if BoolDefault(j.properties.Static_kotlin_stdlib, true) {
 			kotlinJars = append(kotlinJars, deps.kotlinStdlib...)
+			kotlinHeaderJars = append(kotlinHeaderJars, deps.kotlinStdlib...)
 		} else {
 			flags.dexClasspath = append(flags.dexClasspath, deps.kotlinStdlib...)
 		}
@@ -1144,7 +1149,7 @@
 			// with sharding enabled. See: b/77284273.
 		}
 		headerJarFileWithoutDepsOrJarjar, j.headerJarFile =
-			j.compileJavaHeader(ctx, uniqueSrcFiles, srcJars, deps, flags, jarName, kotlinJars)
+			j.compileJavaHeader(ctx, uniqueSrcFiles, srcJars, deps, flags, jarName, kotlinHeaderJars)
 		if ctx.Failed() {
 			return
 		}
diff --git a/java/config/config.go b/java/config/config.go
index 05dfde6..262c531 100644
--- a/java/config/config.go
+++ b/java/config/config.go
@@ -50,11 +50,6 @@
 		"core-icu4j",
 		"core-oj",
 		"core-libart",
-		// TODO: Could this be all updatable bootclasspath jars?
-		"updatable-media",
-		"framework-mediaprovider",
-		"framework-sdkextensions",
-		"android.net.ipsec.ike",
 	}
 )
 
diff --git a/java/config/kotlin.go b/java/config/kotlin.go
index a83f87f..fc63f4d 100644
--- a/java/config/kotlin.go
+++ b/java/config/kotlin.go
@@ -34,6 +34,7 @@
 	pctx.SourcePathVariable("KotlinKaptJar", "external/kotlinc/lib/kotlin-annotation-processing.jar")
 	pctx.SourcePathVariable("KotlinAnnotationJar", "external/kotlinc/lib/annotations-13.0.jar")
 	pctx.SourcePathVariable("KotlinStdlibJar", KotlinStdlibJar)
+	pctx.SourcePathVariable("KotlinAbiGenPluginJar", "external/kotlinc/lib/jvm-abi-gen.jar")
 
 	// These flags silence "Illegal reflective access" warnings when running kapt in OpenJDK9+
 	pctx.StaticVariable("KaptSuppressJDK9Warnings", strings.Join([]string{
diff --git a/java/droidstubs.go b/java/droidstubs.go
index e7aeeb8..3b1f7c0 100644
--- a/java/droidstubs.go
+++ b/java/droidstubs.go
@@ -433,6 +433,10 @@
 	}
 }
 
+func metalavaUseRbe(ctx android.ModuleContext) bool {
+	return ctx.Config().UseRBE() && ctx.Config().IsEnvTrue("RBE_METALAVA")
+}
+
 func metalavaCmd(ctx android.ModuleContext, rule *android.RuleBuilder, javaVersion javaVersion, srcs android.Paths,
 	srcJarList android.Path, bootclasspath, classpath classpath, homeDir android.WritablePath) *android.RuleBuilderCommand {
 	rule.Command().Text("rm -rf").Flag(homeDir.String())
@@ -441,7 +445,7 @@
 	cmd := rule.Command()
 	cmd.FlagWithArg("ANDROID_PREFS_ROOT=", homeDir.String())
 
-	if ctx.Config().UseRBE() && ctx.Config().IsEnvTrue("RBE_METALAVA") {
+	if metalavaUseRbe(ctx) {
 		rule.Remoteable(android.RemoteRuleSupports{RBE: true})
 		execStrategy := ctx.Config().GetenvWithDefault("RBE_METALAVA_EXEC_STRATEGY", remoteexec.LocalExecStrategy)
 		labels := map[string]string{"type": "tool", "name": "metalava"}
@@ -665,7 +669,9 @@
 	}
 
 	// TODO(b/183630617): rewrapper doesn't support restat rules
-	// rule.Restat()
+	if !metalavaUseRbe(ctx) {
+		rule.Restat()
+	}
 
 	zipSyncCleanupCmd(rule, srcJarDir)
 
diff --git a/java/java.go b/java/java.go
index ecbbc32..713fe94 100644
--- a/java/java.go
+++ b/java/java.go
@@ -2060,15 +2060,22 @@
 		protoSrcPartition: android.ProtoSrcLabelPartition,
 	})
 
+	var javacopts []string
+	if m.properties.Javacflags != nil {
+		javacopts = append(javacopts, m.properties.Javacflags...)
+	}
+	epEnabled := m.properties.Errorprone.Enabled
+	//TODO(b/227504307) add configuration that depends on RUN_ERROR_PRONE environment variable
+	if Bool(epEnabled) {
+		javacopts = append(javacopts, m.properties.Errorprone.Javacflags...)
+	}
+
 	commonAttrs := &javaCommonAttributes{
 		Srcs: srcPartitions[javaSrcPartition],
 		Plugins: bazel.MakeLabelListAttribute(
 			android.BazelLabelForModuleDeps(ctx, m.properties.Plugins),
 		),
-	}
-
-	if m.properties.Javacflags != nil {
-		commonAttrs.Javacopts = bazel.MakeStringListAttribute(m.properties.Javacflags)
+		Javacopts: bazel.MakeStringListAttribute(javacopts),
 	}
 
 	depLabels := &javaDependencyLabels{}
diff --git a/java/kotlin.go b/java/kotlin.go
index ce79bae..eff5bb5 100644
--- a/java/kotlin.go
+++ b/java/kotlin.go
@@ -28,17 +28,20 @@
 
 var kotlinc = pctx.AndroidRemoteStaticRule("kotlinc", android.RemoteRuleSupports{Goma: true},
 	blueprint.RuleParams{
-		Command: `rm -rf "$classesDir" "$srcJarDir" "$kotlinBuildFile" "$emptyDir" && ` +
-			`mkdir -p "$classesDir" "$srcJarDir" "$emptyDir" && ` +
+		Command: `rm -rf "$classesDir" "$headerClassesDir" "$srcJarDir" "$kotlinBuildFile" "$emptyDir" && ` +
+			`mkdir -p "$classesDir" "$headerClassesDir" "$srcJarDir" "$emptyDir" && ` +
 			`${config.ZipSyncCmd} -d $srcJarDir -l $srcJarDir/list -f "*.java" $srcJars && ` +
 			`${config.GenKotlinBuildFileCmd} --classpath "$classpath" --name "$name"` +
 			` --out_dir "$classesDir" --srcs "$out.rsp" --srcs "$srcJarDir/list"` +
 			` $commonSrcFilesArg --out "$kotlinBuildFile" && ` +
 			`${config.KotlincCmd} ${config.KotlincGlobalFlags} ` +
-			`${config.KotlincSuppressJDK9Warnings} ${config.JavacHeapFlags} ` +
-			`$kotlincFlags -jvm-target $kotlinJvmTarget -Xbuild-file=$kotlinBuildFile ` +
-			`-kotlin-home $emptyDir && ` +
-			`${config.SoongZipCmd} -jar -o $out -C $classesDir -D $classesDir && ` +
+			` ${config.KotlincSuppressJDK9Warnings} ${config.JavacHeapFlags} ` +
+			` $kotlincFlags -jvm-target $kotlinJvmTarget -Xbuild-file=$kotlinBuildFile ` +
+			` -kotlin-home $emptyDir ` +
+			` -Xplugin=${config.KotlinAbiGenPluginJar} ` +
+			` -P plugin:org.jetbrains.kotlin.jvm.abi:outputDir=$headerClassesDir && ` +
+			`${config.SoongZipCmd} -jar -o $out -C $classesDir -D $classesDir -write_if_changed && ` +
+			`${config.SoongZipCmd} -jar -o $headerJar -C $headerClassesDir -D $headerClassesDir -write_if_changed && ` +
 			`rm -rf "$srcJarDir"`,
 		CommandDeps: []string{
 			"${config.KotlincCmd}",
@@ -49,15 +52,17 @@
 			"${config.KotlinStdlibJar}",
 			"${config.KotlinTrove4jJar}",
 			"${config.KotlinAnnotationJar}",
+			"${config.KotlinAbiGenPluginJar}",
 			"${config.GenKotlinBuildFileCmd}",
 			"${config.SoongZipCmd}",
 			"${config.ZipSyncCmd}",
 		},
 		Rspfile:        "$out.rsp",
 		RspfileContent: `$in`,
+		Restat:         true,
 	},
 	"kotlincFlags", "classpath", "srcJars", "commonSrcFilesArg", "srcJarDir", "classesDir",
-	"kotlinJvmTarget", "kotlinBuildFile", "emptyDir", "name")
+	"headerClassesDir", "headerJar", "kotlinJvmTarget", "kotlinBuildFile", "emptyDir", "name")
 
 func kotlinCommonSrcsList(ctx android.ModuleContext, commonSrcFiles android.Paths) android.OptionalPath {
 	if len(commonSrcFiles) > 0 {
@@ -76,7 +81,7 @@
 }
 
 // kotlinCompile takes .java and .kt sources and srcJars, and compiles the .kt sources into a classes jar in outputFile.
-func kotlinCompile(ctx android.ModuleContext, outputFile android.WritablePath,
+func kotlinCompile(ctx android.ModuleContext, outputFile, headerOutputFile android.WritablePath,
 	srcFiles, commonSrcFiles, srcJars android.Paths,
 	flags javaBuilderFlags) {
 
@@ -97,17 +102,20 @@
 	}
 
 	ctx.Build(pctx, android.BuildParams{
-		Rule:        kotlinc,
-		Description: "kotlinc",
-		Output:      outputFile,
-		Inputs:      srcFiles,
-		Implicits:   deps,
+		Rule:           kotlinc,
+		Description:    "kotlinc",
+		Output:         outputFile,
+		ImplicitOutput: headerOutputFile,
+		Inputs:         srcFiles,
+		Implicits:      deps,
 		Args: map[string]string{
 			"classpath":         flags.kotlincClasspath.FormJavaClassPath(""),
 			"kotlincFlags":      flags.kotlincFlags,
 			"commonSrcFilesArg": commonSrcFilesArg,
 			"srcJars":           strings.Join(srcJars.Strings(), " "),
 			"classesDir":        android.PathForModuleOut(ctx, "kotlinc", "classes").String(),
+			"headerClassesDir":  android.PathForModuleOut(ctx, "kotlinc", "header_classes").String(),
+			"headerJar":         headerOutputFile.String(),
 			"srcJarDir":         android.PathForModuleOut(ctx, "kotlinc", "srcJars").String(),
 			"kotlinBuildFile":   android.PathForModuleOut(ctx, "kotlinc-build.xml").String(),
 			"emptyDir":          android.PathForModuleOut(ctx, "kotlinc", "empty").String(),
diff --git a/java/kotlin_test.go b/java/kotlin_test.go
index d51bc04..f9ff982 100644
--- a/java/kotlin_test.go
+++ b/java/kotlin_test.go
@@ -45,6 +45,10 @@
 	fooKotlinc := ctx.ModuleForTests("foo", "android_common").Rule("kotlinc")
 	fooJavac := ctx.ModuleForTests("foo", "android_common").Rule("javac")
 	fooJar := ctx.ModuleForTests("foo", "android_common").Output("combined/foo.jar")
+	fooHeaderJar := ctx.ModuleForTests("foo", "android_common").Output("turbine-combined/foo.jar")
+
+	fooKotlincClasses := fooKotlinc.Output
+	fooKotlincHeaderClasses := fooKotlinc.ImplicitOutput
 
 	if len(fooKotlinc.Inputs) != 2 || fooKotlinc.Inputs[0].String() != "a.java" ||
 		fooKotlinc.Inputs[1].String() != "b.kt" {
@@ -55,17 +59,21 @@
 		t.Errorf(`foo inputs %v != ["a.java"]`, fooJavac.Inputs)
 	}
 
-	if !strings.Contains(fooJavac.Args["classpath"], fooKotlinc.Output.String()) {
+	if !strings.Contains(fooJavac.Args["classpath"], fooKotlincHeaderClasses.String()) {
 		t.Errorf("foo classpath %v does not contain %q",
-			fooJavac.Args["classpath"], fooKotlinc.Output.String())
+			fooJavac.Args["classpath"], fooKotlincHeaderClasses.String())
 	}
 
-	if !inList(fooKotlinc.Output.String(), fooJar.Inputs.Strings()) {
+	if !inList(fooKotlincClasses.String(), fooJar.Inputs.Strings()) {
 		t.Errorf("foo jar inputs %v does not contain %q",
-			fooJar.Inputs.Strings(), fooKotlinc.Output.String())
+			fooJar.Inputs.Strings(), fooKotlincClasses.String())
 	}
 
-	fooHeaderJar := ctx.ModuleForTests("foo", "android_common").Output("turbine-combined/foo.jar")
+	if !inList(fooKotlincHeaderClasses.String(), fooHeaderJar.Inputs.Strings()) {
+		t.Errorf("foo header jar inputs %v does not contain %q",
+			fooHeaderJar.Inputs.Strings(), fooKotlincHeaderClasses.String())
+	}
+
 	bazHeaderJar := ctx.ModuleForTests("baz", "android_common").Output("turbine-combined/baz.jar")
 	barKotlinc := ctx.ModuleForTests("bar", "android_common").Rule("kotlinc")
 
diff --git a/mk2rbc/expr.go b/mk2rbc/expr.go
index dc16d1d..54bb6d1 100644
--- a/mk2rbc/expr.go
+++ b/mk2rbc/expr.go
@@ -221,11 +221,9 @@
 }
 
 func (xi *interpolateExpr) transform(transformer func(expr starlarkExpr) starlarkExpr) starlarkExpr {
-	argsCopy := make([]starlarkExpr, len(xi.args))
-	for i, arg := range xi.args {
-		argsCopy[i] = arg.transform(transformer)
+	for i := range xi.args {
+		xi.args[i] = xi.args[i].transform(transformer)
 	}
-	xi.args = argsCopy
 	if replacement := transformer(xi); replacement != nil {
 		return replacement
 	} else {
@@ -591,11 +589,9 @@
 	if cx.object != nil {
 		cx.object = cx.object.transform(transformer)
 	}
-	argsCopy := make([]starlarkExpr, len(cx.args))
-	for i, arg := range cx.args {
-		argsCopy[i] = arg.transform(transformer)
+	for i := range cx.args {
+		cx.args[i] = cx.args[i].transform(transformer)
 	}
-	cx.args = argsCopy
 	if replacement := transformer(cx); replacement != nil {
 		return replacement
 	} else {
@@ -769,3 +765,35 @@
 	x, ok := expr.(*stringLiteralExpr)
 	return ok && x.literal == ""
 }
+
+func negateExpr(expr starlarkExpr) starlarkExpr {
+	switch typedExpr := expr.(type) {
+	case *notExpr:
+		return typedExpr.expr
+	case *inExpr:
+		typedExpr.isNot = !typedExpr.isNot
+		return typedExpr
+	case *eqExpr:
+		typedExpr.isEq = !typedExpr.isEq
+		return typedExpr
+	case *binaryOpExpr:
+		switch typedExpr.op {
+		case ">":
+			typedExpr.op = "<="
+			return typedExpr
+		case "<":
+			typedExpr.op = ">="
+			return typedExpr
+		case ">=":
+			typedExpr.op = "<"
+			return typedExpr
+		case "<=":
+			typedExpr.op = ">"
+			return typedExpr
+		default:
+			return &notExpr{expr: expr}
+		}
+	default:
+		return &notExpr{expr: expr}
+	}
+}
diff --git a/mk2rbc/mk2rbc.go b/mk2rbc/mk2rbc.go
index 950a1e5..8807437 100644
--- a/mk2rbc/mk2rbc.go
+++ b/mk2rbc/mk2rbc.go
@@ -86,7 +86,7 @@
 	"filter":                               &simpleCallParser{name: baseName + ".filter", returnType: starlarkTypeList},
 	"filter-out":                           &simpleCallParser{name: baseName + ".filter_out", returnType: starlarkTypeList},
 	"firstword":                            &firstOrLastwordCallParser{isLastWord: false},
-	"foreach":                              &foreachCallPaser{},
+	"foreach":                              &foreachCallParser{},
 	"if":                                   &ifCallParser{},
 	"info":                                 &makeControlFuncParser{name: baseName + ".mkinfo"},
 	"is-board-platform":                    &simpleCallParser{name: baseName + ".board_platform_is", returnType: starlarkTypeBool, addGlobals: true},
@@ -117,6 +117,17 @@
 	"wildcard": &simpleCallParser{name: baseName + ".expand_wildcard", returnType: starlarkTypeList},
 }
 
+// The same as knownFunctions, but returns a []starlarkNode instead of a starlarkExpr
+var knownNodeFunctions = map[string]interface {
+	parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode
+}{
+	"eval":                      &evalNodeParser{},
+	"if":                        &ifCallNodeParser{},
+	"inherit-product":           &inheritProductCallParser{loadAlways: true},
+	"inherit-product-if-exists": &inheritProductCallParser{loadAlways: false},
+	"foreach":                   &foreachCallNodeParser{},
+}
+
 // These are functions that we don't implement conversions for, but
 // we allow seeing their definitions in the product config files.
 var ignoredDefines = map[string]bool{
@@ -846,15 +857,19 @@
 	return res
 }
 
-func (ctx *parseContext) handleInheritModule(v mkparser.Node, args *mkparser.MakeString, loadAlways bool) []starlarkNode {
+type inheritProductCallParser struct {
+	loadAlways bool
+}
+
+func (p *inheritProductCallParser) parse(ctx *parseContext, v mkparser.Node, args *mkparser.MakeString) []starlarkNode {
 	args.TrimLeftSpaces()
 	args.TrimRightSpaces()
 	pathExpr := ctx.parseMakeString(v, args)
 	if _, ok := pathExpr.(*badExpr); ok {
 		return []starlarkNode{ctx.newBadNode(v, "Unable to parse argument to inherit")}
 	}
-	return ctx.handleSubConfig(v, pathExpr, loadAlways, func(im inheritedModule) starlarkNode {
-		return &inheritNode{im, loadAlways}
+	return ctx.handleSubConfig(v, pathExpr, p.loadAlways, func(im inheritedModule) starlarkNode {
+		return &inheritNode{im, p.loadAlways}
 	})
 }
 
@@ -873,19 +888,12 @@
 	//   $(error xxx)
 	//   $(call other-custom-functions,...)
 
-	// inherit-product(-if-exists) gets converted to a series of statements,
-	// not just a single expression like parseReference returns. So handle it
-	// separately at the beginning here.
-	if strings.HasPrefix(v.Name.Dump(), "call inherit-product,") {
-		args := v.Name.Clone()
-		args.ReplaceLiteral("call inherit-product,", "")
-		return ctx.handleInheritModule(v, args, true)
+	if name, args, ok := ctx.maybeParseFunctionCall(v, v.Name); ok {
+		if kf, ok := knownNodeFunctions[name]; ok {
+			return kf.parse(ctx, v, args)
+		}
 	}
-	if strings.HasPrefix(v.Name.Dump(), "call inherit-product-if-exists,") {
-		args := v.Name.Clone()
-		args.ReplaceLiteral("call inherit-product-if-exists,", "")
-		return ctx.handleInheritModule(v, args, false)
-	}
+
 	return []starlarkNode{&exprNode{expr: ctx.parseReference(v, v.Name)}}
 }
 
@@ -1030,49 +1038,19 @@
 		otherOperand = xLeft
 	}
 
-	not := func(expr starlarkExpr) starlarkExpr {
-		switch typedExpr := expr.(type) {
-		case *inExpr:
-			typedExpr.isNot = !typedExpr.isNot
-			return typedExpr
-		case *eqExpr:
-			typedExpr.isEq = !typedExpr.isEq
-			return typedExpr
-		case *binaryOpExpr:
-			switch typedExpr.op {
-			case ">":
-				typedExpr.op = "<="
-				return typedExpr
-			case "<":
-				typedExpr.op = ">="
-				return typedExpr
-			case ">=":
-				typedExpr.op = "<"
-				return typedExpr
-			case "<=":
-				typedExpr.op = ">"
-				return typedExpr
-			default:
-				return &notExpr{expr: expr}
-			}
-		default:
-			return &notExpr{expr: expr}
-		}
-	}
-
 	// If we've identified one of the operands as being a string literal, check
 	// for some special cases we can do to simplify the resulting expression.
 	if otherOperand != nil {
 		if stringOperand == "" {
 			if isEq {
-				return not(otherOperand)
+				return negateExpr(otherOperand)
 			} else {
 				return otherOperand
 			}
 		}
 		if stringOperand == "true" && otherOperand.typ() == starlarkTypeBool {
 			if !isEq {
-				return not(otherOperand)
+				return negateExpr(otherOperand)
 			} else {
 				return otherOperand
 			}
@@ -1228,6 +1206,37 @@
 		right: xValue, isEq: !negate}
 }
 
+func (ctx *parseContext) maybeParseFunctionCall(node mkparser.Node, ref *mkparser.MakeString) (name string, args *mkparser.MakeString, ok bool) {
+	ref.TrimLeftSpaces()
+	ref.TrimRightSpaces()
+
+	words := ref.SplitN(" ", 2)
+	if !words[0].Const() {
+		return "", nil, false
+	}
+
+	name = words[0].Dump()
+	args = mkparser.SimpleMakeString("", words[0].Pos())
+	if len(words) >= 2 {
+		args = words[1]
+	}
+	args.TrimLeftSpaces()
+	if name == "call" {
+		words = args.SplitN(",", 2)
+		if words[0].Empty() || !words[0].Const() {
+			return "", nil, false
+		}
+		name = words[0].Dump()
+		if len(words) < 2 {
+			args = &mkparser.MakeString{}
+		} else {
+			args = words[1]
+		}
+	}
+	ok = true
+	return
+}
+
 // parses $(...), returning an expression
 func (ctx *parseContext) parseReference(node mkparser.Node, ref *mkparser.MakeString) starlarkExpr {
 	ref.TrimLeftSpaces()
@@ -1242,7 +1251,7 @@
 
 	// If it is a single word, it can be a simple variable
 	// reference or a function call
-	if len(words) == 1 && !isMakeControlFunc(refDump) && refDump != "shell" {
+	if len(words) == 1 && !isMakeControlFunc(refDump) && refDump != "shell" && refDump != "eval" {
 		if strings.HasPrefix(refDump, soongNsPrefix) {
 			// TODO (asmundak): if we find many, maybe handle them.
 			return ctx.newBadExpr(node, "SOONG_CONFIG_ variables cannot be referenced, use soong_config_get instead: %s", refDump)
@@ -1281,28 +1290,14 @@
 		return ctx.newBadExpr(node, "unknown variable %s", refDump)
 	}
 
-	expr := &callExpr{name: words[0].Dump(), returnType: starlarkTypeUnknown}
-	args := mkparser.SimpleMakeString("", words[0].Pos())
-	if len(words) >= 2 {
-		args = words[1]
-	}
-	args.TrimLeftSpaces()
-	if expr.name == "call" {
-		words = args.SplitN(",", 2)
-		if words[0].Empty() || !words[0].Const() {
-			return ctx.newBadExpr(node, "cannot handle %s", refDump)
-		}
-		expr.name = words[0].Dump()
-		if len(words) < 2 {
-			args = &mkparser.MakeString{}
+	if name, args, ok := ctx.maybeParseFunctionCall(node, ref); ok {
+		if kf, found := knownFunctions[name]; found {
+			return kf.parse(ctx, node, args)
 		} else {
-			args = words[1]
+			return ctx.newBadExpr(node, "cannot handle invoking %s", name)
 		}
-	}
-	if kf, found := knownFunctions[expr.name]; found {
-		return kf.parse(ctx, node, args)
 	} else {
-		return ctx.newBadExpr(node, "cannot handle invoking %s", expr.name)
+		return ctx.newBadExpr(node, "cannot handle %s", refDump)
 	}
 }
 
@@ -1486,9 +1481,46 @@
 	}
 }
 
-type foreachCallPaser struct{}
+type ifCallNodeParser struct{}
 
-func (p *foreachCallPaser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr {
+func (p *ifCallNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
+	words := args.Split(",")
+	if len(words) != 2 && len(words) != 3 {
+		return []starlarkNode{ctx.newBadNode(node, "if function should have 2 or 3 arguments, found "+strconv.Itoa(len(words)))}
+	}
+
+	ifn := &ifNode{expr: ctx.parseMakeString(node, words[0])}
+	cases := []*switchCase{
+		{
+			gate:  ifn,
+			nodes: ctx.parseNodeMakeString(node, words[1]),
+		},
+	}
+	if len(words) == 3 {
+		cases = append(cases, &switchCase{
+			gate:  &elseNode{},
+			nodes: ctx.parseNodeMakeString(node, words[2]),
+		})
+	}
+	if len(cases) == 2 {
+		if len(cases[1].nodes) == 0 {
+			// Remove else branch if it has no contents
+			cases = cases[:1]
+		} else if len(cases[0].nodes) == 0 {
+			// If the if branch has no contents but the else does,
+			// move them to the if and negate its condition
+			ifn.expr = negateExpr(ifn.expr)
+			cases[0].nodes = cases[1].nodes
+			cases = cases[:1]
+		}
+	}
+
+	return []starlarkNode{&switchNode{ssCases: cases}}
+}
+
+type foreachCallParser struct{}
+
+func (p *foreachCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr {
 	words := args.Split(",")
 	if len(words) != 3 {
 		return ctx.newBadExpr(node, "foreach function should have 3 arguments, found "+strconv.Itoa(len(words)))
@@ -1520,6 +1552,71 @@
 	}
 }
 
+func transformNode(node starlarkNode, transformer func(expr starlarkExpr) starlarkExpr) {
+	switch a := node.(type) {
+	case *ifNode:
+		a.expr = a.expr.transform(transformer)
+	case *switchCase:
+		transformNode(a.gate, transformer)
+		for _, n := range a.nodes {
+			transformNode(n, transformer)
+		}
+	case *switchNode:
+		for _, n := range a.ssCases {
+			transformNode(n, transformer)
+		}
+	case *exprNode:
+		a.expr = a.expr.transform(transformer)
+	case *assignmentNode:
+		a.value = a.value.transform(transformer)
+	case *foreachNode:
+		a.list = a.list.transform(transformer)
+		for _, n := range a.actions {
+			transformNode(n, transformer)
+		}
+	}
+}
+
+type foreachCallNodeParser struct{}
+
+func (p *foreachCallNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
+	words := args.Split(",")
+	if len(words) != 3 {
+		return []starlarkNode{ctx.newBadNode(node, "foreach function should have 3 arguments, found "+strconv.Itoa(len(words)))}
+	}
+	if !words[0].Const() || words[0].Empty() || !identifierFullMatchRegex.MatchString(words[0].Strings[0]) {
+		return []starlarkNode{ctx.newBadNode(node, "first argument to foreach function must be a simple string identifier")}
+	}
+
+	loopVarName := words[0].Strings[0]
+
+	list := ctx.parseMakeString(node, words[1])
+	if list.typ() != starlarkTypeList {
+		list = &callExpr{
+			name:       baseName + ".words",
+			returnType: starlarkTypeList,
+			args:       []starlarkExpr{list},
+		}
+	}
+
+	actions := ctx.parseNodeMakeString(node, words[2])
+	// TODO(colefaust): Replace transforming code with something more elegant
+	for _, action := range actions {
+		transformNode(action, func(expr starlarkExpr) starlarkExpr {
+			if varRefExpr, ok := expr.(*variableRefExpr); ok && varRefExpr.ref.name() == loopVarName {
+				return &identifierExpr{loopVarName}
+			}
+			return nil
+		})
+	}
+
+	return []starlarkNode{&foreachNode{
+		varName: loopVarName,
+		list:    list,
+		actions: actions,
+	}}
+}
+
 type wordCallParser struct{}
 
 func (p *wordCallParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) starlarkExpr {
@@ -1630,6 +1727,31 @@
 	}
 }
 
+type evalNodeParser struct{}
+
+func (p *evalNodeParser) parse(ctx *parseContext, node mkparser.Node, args *mkparser.MakeString) []starlarkNode {
+	parser := mkparser.NewParser("Eval expression", strings.NewReader(args.Dump()))
+	nodes, errs := parser.Parse()
+	if errs != nil {
+		return []starlarkNode{ctx.newBadNode(node, "Unable to parse eval statement")}
+	}
+
+	if len(nodes) == 0 {
+		return []starlarkNode{}
+	} else if len(nodes) == 1 {
+		switch n := nodes[0].(type) {
+		case *mkparser.Assignment:
+			if n.Name.Const() {
+				return ctx.handleAssignment(n)
+			}
+		case *mkparser.Comment:
+			return []starlarkNode{&commentNode{strings.TrimSpace("#" + n.Comment)}}
+		}
+	}
+
+	return []starlarkNode{ctx.newBadNode(node, "Eval expression too complex; only assignments and comments are supported")}
+}
+
 func (ctx *parseContext) parseMakeString(node mkparser.Node, mk *mkparser.MakeString) starlarkExpr {
 	if mk.Const() {
 		return &stringLiteralExpr{mk.Dump()}
@@ -1654,6 +1776,16 @@
 	return NewInterpolateExpr(parts)
 }
 
+func (ctx *parseContext) parseNodeMakeString(node mkparser.Node, mk *mkparser.MakeString) []starlarkNode {
+	// Discard any constant values in the make string, as they would be top level
+	// string literals and do nothing.
+	result := make([]starlarkNode, 0, len(mk.Variables))
+	for i := range mk.Variables {
+		result = append(result, ctx.handleVariable(&mk.Variables[i])...)
+	}
+	return result
+}
+
 // Handles the statements whose treatment is the same in all contexts: comment,
 // assignment, variable (which is a macro call in reality) and all constructs that
 // do not handle in any context ('define directive and any unrecognized stuff).
@@ -1698,6 +1830,7 @@
 	if result == nil {
 		result = []starlarkNode{}
 	}
+
 	return result
 }
 
diff --git a/mk2rbc/mk2rbc_test.go b/mk2rbc/mk2rbc_test.go
index 2b447e3..3698813 100644
--- a/mk2rbc/mk2rbc_test.go
+++ b/mk2rbc/mk2rbc_test.go
@@ -1313,6 +1313,11 @@
 FOREACH_WITH_IF := $(foreach module,\
   $(BOOT_KERNEL_MODULES_LIST),\
   $(if $(filter $(module),foo.ko),,$(error module "$(module)" has an error!)))
+
+# Same as above, but not assigning it to a variable allows it to be converted to statements
+$(foreach module,\
+  $(BOOT_KERNEL_MODULES_LIST),\
+  $(if $(filter $(module),foo.ko),,$(error module "$(module)" has an error!)))
 `,
 		expected: `load("//build/make/core:product_config.rbc", "rblf")
 
@@ -1324,6 +1329,10 @@
   g["BOOT_KERNEL_MODULES_LIST"] += ["bar.ko"]
   g["BOOT_KERNEL_MODULES_FILTER_2"] = ["%%/%s" % m for m in g["BOOT_KERNEL_MODULES_LIST"]]
   g["FOREACH_WITH_IF"] = [("" if rblf.filter(module, "foo.ko") else rblf.mkerror("product.mk", "module \"%s\" has an error!" % module)) for module in g["BOOT_KERNEL_MODULES_LIST"]]
+  # Same as above, but not assigning it to a variable allows it to be converted to statements
+  for module in g["BOOT_KERNEL_MODULES_LIST"]:
+    if not rblf.filter(module, "foo.ko"):
+      rblf.mkerror("product.mk", "module \"%s\" has an error!" % module)
 `,
 	},
 	{
@@ -1474,6 +1483,34 @@
   
 `,
 	},
+	{
+		desc:   "Evals",
+		mkname: "product.mk",
+		in: `
+$(eval)
+$(eval MY_VAR := foo)
+$(eval # This is a test of eval functions)
+$(eval $(TOO_COMPLICATED) := bar)
+$(foreach x,$(MY_LIST_VAR), \
+  $(eval PRODUCT_COPY_FILES += foo/bar/$(x):$(TARGET_COPY_OUT_VENDOR)/etc/$(x)) \
+  $(if $(MY_OTHER_VAR),$(eval PRODUCT_COPY_FILES += $(MY_OTHER_VAR):foo/bar/$(x))) \
+)
+
+`,
+		expected: `load("//build/make/core:product_config.rbc", "rblf")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+  g["MY_VAR"] = "foo"
+  # This is a test of eval functions
+  rblf.mk2rbc_error("product.mk:5", "Eval expression too complex; only assignments and comments are supported")
+  for x in rblf.words(g.get("MY_LIST_VAR", "")):
+    rblf.setdefault(handle, "PRODUCT_COPY_FILES")
+    cfg["PRODUCT_COPY_FILES"] += ("foo/bar/%s:%s/etc/%s" % (x, g.get("TARGET_COPY_OUT_VENDOR", ""), x)).split()
+    if g.get("MY_OTHER_VAR", ""):
+      cfg["PRODUCT_COPY_FILES"] += ("%s:foo/bar/%s" % (g.get("MY_OTHER_VAR", ""), x)).split()
+`,
+	},
 }
 
 var known_variables = []struct {
diff --git a/mk2rbc/node.go b/mk2rbc/node.go
index 9d5af91..c0c4c98 100644
--- a/mk2rbc/node.go
+++ b/mk2rbc/node.go
@@ -294,3 +294,28 @@
 		ssCase.emit(gctx)
 	}
 }
+
+type foreachNode struct {
+	varName string
+	list    starlarkExpr
+	actions []starlarkNode
+}
+
+func (f *foreachNode) emit(gctx *generationContext) {
+	gctx.newLine()
+	gctx.writef("for %s in ", f.varName)
+	f.list.emit(gctx)
+	gctx.write(":")
+	gctx.indentLevel++
+	hasStatements := false
+	for _, a := range f.actions {
+		if _, ok := a.(*commentNode); !ok {
+			hasStatements = true
+		}
+		a.emit(gctx)
+	}
+	if !hasStatements {
+		gctx.emitPass()
+	}
+	gctx.indentLevel--
+}
diff --git a/rust/config/allowed_list.go b/rust/config/allowed_list.go
index 30700dd..802e1da 100644
--- a/rust/config/allowed_list.go
+++ b/rust/config/allowed_list.go
@@ -28,6 +28,7 @@
 		"prebuilts/rust",
 		"system/core/debuggerd/rust",
 		"system/core/libstats/pull_rust",
+		"system/core/trusty/libtrusty-rs",
 		"system/extras/profcollectd",
 		"system/extras/simpleperf",
 		"system/hardware/interfaces/keystore2",
diff --git a/scripts/hiddenapi/analyze_bcpf.py b/scripts/hiddenapi/analyze_bcpf.py
index 1ad8d07..595343b 100644
--- a/scripts/hiddenapi/analyze_bcpf.py
+++ b/scripts/hiddenapi/analyze_bcpf.py
@@ -319,12 +319,48 @@
 
 
 @dataclasses.dataclass()
+class PackagePropertyReason:
+    """Provides the reasons why a package was added to a specific property.
+
+    A split package is one that contains classes from the bootclasspath_fragment
+    and other bootclasspath modules. So, for a split package this contains the
+    corresponding lists of classes.
+
+    A single package is one that contains classes sub-packages from the
+    For a split package this contains a list of classes in that package that are
+    provided by the bootclasspath_fragment and a list of classes
+    """
+
+    # The list of classes/sub-packages that is provided by the
+    # bootclasspath_fragment.
+    bcpf: typing.List[str]
+
+    # The list of classes/sub-packages that is provided by other modules on the
+    # bootclasspath.
+    other: typing.List[str]
+
+
+@dataclasses.dataclass()
 class Result:
     """Encapsulates the result of the analysis."""
 
     # The diffs in the flags.
     diffs: typing.Optional[FlagDiffs] = None
 
+    # A map from package name to the reason why it belongs in the
+    # split_packages property.
+    split_packages: typing.Dict[str, PackagePropertyReason] = dataclasses.field(
+        default_factory=dict)
+
+    # A map from package name to the reason why it belongs in the
+    # single_packages property.
+    single_packages: typing.Dict[str,
+                                 PackagePropertyReason] = dataclasses.field(
+                                     default_factory=dict)
+
+    # The list of packages to add to the package_prefixes property.
+    package_prefixes: typing.List[str] = dataclasses.field(default_factory=list)
+
     # The bootclasspath_fragment hidden API properties changes.
     property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field(
         default_factory=list)
@@ -394,13 +430,17 @@
     def reformat_report_test(text):
         return re.sub(r"(.)\n([^\s])", r"\1 \2", text)
 
-    def report(self, text, **kwargs):
+    def report(self, text="", **kwargs):
         # Concatenate lines that are not separated by a blank line together to
         # eliminate formatting applied to the supplied text to adhere to python
         # line length limitations.
         text = self.reformat_report_test(text)
         logging.info("%s", text, **kwargs)
 
+    def report_dedent(self, text, **kwargs):
+        text = textwrap.dedent(text)
+        self.report(text, **kwargs)
+
     def run_command(self, cmd, *args, **kwargs):
         cmd_line = " ".join(cmd)
         logging.debug("Running %s", cmd_line)
@@ -442,9 +482,7 @@
     def load_module_info(self):
         module_info_file = os.path.join(self.product_out_dir,
                                         "module-info.json")
-        self.report(f"""
-Making sure that {module_info_file} is up to date.
-""")
+        self.report(f"\nMaking sure that {module_info_file} is up to date.\n")
         output = self.build_file_read_output(module_info_file)
         lines = output.lines()
         for line in lines:
@@ -496,61 +534,62 @@
         optimizations that can be applied.
         """
         self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}")
-        self.report(f"""
-Run this tool to help initialize a bootclasspath_fragment module. Before you
-start make sure that:
+        self.report_dedent(f"""
+            Run this tool to help initialize a bootclasspath_fragment module.
+            Before you start make sure that:
 
-1. The current checkout is up to date.
+            1. The current checkout is up to date.
 
-2. The environment has been initialized using lunch, e.g.
-   lunch aosp_arm64-userdebug
+            2. The environment has been initialized using lunch, e.g.
+               lunch aosp_arm64-userdebug
 
-3. You have added a bootclasspath_fragment module to the appropriate Android.bp
-file. Something like this:
+            3. You have added a bootclasspath_fragment module to the appropriate
+            Android.bp file. Something like this:
 
-   bootclasspath_fragment {{
-     name: "{self.bcpf}",
-     contents: [
-       "...",
-     ],
+               bootclasspath_fragment {{
+                 name: "{self.bcpf}",
+                 contents: [
+                   "...",
+                 ],
+            
+                 // The bootclasspath_fragments that provide APIs on which this
+                 // depends.
+                 fragments: [
+                   {{
+                     apex: "com.android.art",
+                     module: "art-bootclasspath-fragment",
+                   }},
+                 ],
+               }}
+            
+            4. You have added it to the platform_bootclasspath module in
+            frameworks/base/boot/Android.bp. Something like this:
 
-     // The bootclasspath_fragments that provide APIs on which this depends.
-     fragments: [
-       {{
-         apex: "com.android.art",
-         module: "art-bootclasspath-fragment",
-       }},
-     ],
-   }}
+               platform_bootclasspath {{
+                 name: "platform-bootclasspath",
+                 fragments: [
+                   ...
+                   {{
+                     apex: "{self.apex}",
+                     module: "{self.bcpf}",
+                   }},
+                 ],
+               }}
 
-4. You have added it to the platform_bootclasspath module in
-frameworks/base/boot/Android.bp. Something like this:
+            5. You have added an sdk module. Something like this:
 
-   platform_bootclasspath {{
-     name: "platform-bootclasspath",
-     fragments: [
-       ...
-       {{
-         apex: "{self.apex}",
-         module: "{self.bcpf}",
-       }},
-     ],
-   }}
-
-5. You have added an sdk module. Something like this:
-
-   sdk {{
-     name: "{self.sdk}",
-     bootclasspath_fragments: ["{self.bcpf}"],
-   }}
-""")
+               sdk {{
+                 name: "{self.sdk}",
+                 bootclasspath_fragments: ["{self.bcpf}"],
+               }}
+            """)
 
         # Make sure that the module-info.json file is up to date.
         self.load_module_info()
 
-        self.report("""
-Cleaning potentially stale files.
-""")
+        self.report_dedent("""
+            Cleaning potentially stale files.
+            """)
         # Remove the out/soong/hiddenapi files.
         shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True)
 
@@ -605,26 +644,26 @@
 
         if result.file_changes:
             if self.fix:
-                file_change_message = """
-The following files were modified by this script:"""
+                file_change_message = textwrap.dedent("""
+                    The following files were modified by this script:
+                    """)
             else:
-                file_change_message = """
-The following modifications need to be made:"""
+                file_change_message = textwrap.dedent("""
+                    The following modifications need to be made:
+                    """)
 
-            self.report(f"""
-{file_change_message}""")
+            self.report(file_change_message)
             result.file_changes.sort()
             for file_change in result.file_changes:
-                self.report(f"""
-    {file_change.path}
-        {file_change.description}
-""".lstrip("\n"))
+                self.report(f"    {file_change.path}")
+                self.report(f"        {file_change.description}")
+                self.report()
 
             if not self.fix:
-                self.report("""
-Run the command again with the --fix option to automatically make the above
-changes.
-""".lstrip())
+                self.report_dedent("""
+                    Run the command again with the --fix option to automatically
+                    make the above changes.
+                    """.lstrip("\n"))
 
     def new_file_change(self, file, description):
         return FileChange(
@@ -635,11 +674,10 @@
         if not (module_line.startswith("< ") and
                 monolithic_line.startswith("> ") and not separator_line):
             # Something went wrong.
-            self.report(f"""Invalid build output detected:
-  module_line: "{module_line}"
-  monolithic_line: "{monolithic_line}"
-  separator_line: "{separator_line}"
-""")
+            self.report("Invalid build output detected:")
+            self.report(f"  module_line: '{module_line}'")
+            self.report(f"  monolithic_line: '{monolithic_line}'")
+            self.report(f"  separator_line: '{separator_line}'")
             sys.exit(1)
 
         if significant:
@@ -698,10 +736,9 @@
 
             if module_signature != monolithic_signature:
                 # Something went wrong.
-                self.report(f"""Inconsistent signatures detected:
-  module_signature: "{module_signature}"
-  monolithic_signature: "{monolithic_signature}"
-""")
+                self.report("Inconsistent signatures detected:")
+                self.report(f"  module_signature: '{module_signature}'")
+                self.report(f"  monolithic_signature: '{monolithic_signature}'")
                 sys.exit(1)
 
             diffs[module_signature] = (module_flags, monolithic_flags)
@@ -749,85 +786,91 @@
         return diffs
 
     def build_monolithic_stubs_flags(self):
-        self.report(f"""
-Attempting to build {_STUB_FLAGS_FILE} to verify that the
-bootclasspath_fragment has the correct API stubs available...
-""")
+        self.report_dedent(f"""
+            Attempting to build {_STUB_FLAGS_FILE} to verify that the
+            bootclasspath_fragment has the correct API stubs available...
+            """)
 
         # Build the hiddenapi-stubs-flags.txt file.
         diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE)
         if diffs:
-            self.report(f"""
-There is a discrepancy between the stub API derived flags created by the
-bootclasspath_fragment and the platform_bootclasspath. See preceding error
-messages to see which flags are inconsistent. The inconsistencies can occur for
-a couple of reasons:
+            self.report_dedent(f"""
+                There is a discrepancy between the stub API derived flags
+                created by the bootclasspath_fragment and the
+                platform_bootclasspath. See preceding error messages to see
+                which flags are inconsistent. The inconsistencies can occur for
+                a couple of reasons:
 
-If you are building against prebuilts of the Android SDK, e.g. by using
-TARGET_BUILD_APPS then the prebuilt versions of the APIs this
-bootclasspath_fragment depends upon are out of date and need updating. See
-go/update-prebuilts for help.
+                If you are building against prebuilts of the Android SDK, e.g.
+                by using TARGET_BUILD_APPS then the prebuilt versions of the
+                APIs this bootclasspath_fragment depends upon are out of date
+                and need updating. See go/update-prebuilts for help.
 
-Otherwise, this is happening because there are some stub APIs that are either
-provided by or used by the contents of the bootclasspath_fragment but which are
-not available to it. There are 4 ways to handle this:
+                Otherwise, this is happening because there are some stub APIs
+                that are either provided by or used by the contents of the
+                bootclasspath_fragment but which are not available to it. There
+                are 4 ways to handle this:
 
-1. A java_sdk_library in the contents property will automatically make its stub
-   APIs available to the bootclasspath_fragment so nothing needs to be done.
+                1. A java_sdk_library in the contents property will
+                automatically make its stub APIs available to the
+                bootclasspath_fragment so nothing needs to be done.
 
-2. If the API provided by the bootclasspath_fragment is created by an api_only
-   java_sdk_library (or a java_library that compiles files generated by a
-   separate droidstubs module then it cannot be added to the contents and
-   instead must be added to the api.stubs property, e.g.
+                2. If the API provided by the bootclasspath_fragment is created
+                by an api_only java_sdk_library (or a java_library that compiles
+                files generated by a separate droidstubs module then it cannot
+                be added to the contents and instead must be added to the
+                api.stubs property, e.g.
 
-   bootclasspath_fragment {{
-     name: "{self.bcpf}",
-     ...
-     api: {{
-       stubs: ["$MODULE-api-only"],"
-     }},
-   }}
+                   bootclasspath_fragment {{
+                     name: "{self.bcpf}",
+                     ...
+                     api: {{
+                       stubs: ["$MODULE-api-only"],"
+                     }},
+                   }}
 
-3. If the contents use APIs provided by another bootclasspath_fragment then
-   it needs to be added to the fragments property, e.g.
+                3. If the contents use APIs provided by another
+                bootclasspath_fragment then it needs to be added to the
+                fragments property, e.g.
+                
+                   bootclasspath_fragment {{
+                     name: "{self.bcpf}",
+                     ...
+                     // The bootclasspath_fragments that provide APIs on which this depends.
+                     fragments: [
+                       ...
+                       {{
+                         apex: "com.android.other",
+                         module: "com.android.other-bootclasspath-fragment",
+                       }},
+                     ],
+                   }}
+                
+                4. If the contents use APIs from a module that is not part of
+                another bootclasspath_fragment then it must be added to the
+                additional_stubs property, e.g.
 
-   bootclasspath_fragment {{
-     name: "{self.bcpf}",
-     ...
-     // The bootclasspath_fragments that provide APIs on which this depends.
-     fragments: [
-       ...
-       {{
-         apex: "com.android.other",
-         module: "com.android.other-bootclasspath-fragment",
-       }},
-     ],
-   }}
+                   bootclasspath_fragment {{
+                     name: "{self.bcpf}",
+                     ...
+                     additional_stubs: ["android-non-updatable"],
+                   }}
 
-4. If the contents use APIs from a module that is not part of another
-   bootclasspath_fragment then it must be added to the additional_stubs
-   property, e.g.
+                   Like the api.stubs property these are typically
+                   java_sdk_library modules but can be java_library too.
 
-   bootclasspath_fragment {{
-     name: "{self.bcpf}",
-     ...
-     additional_stubs: ["android-non-updatable"],
-   }}
-
-   Like the api.stubs property these are typically java_sdk_library modules but
-   can be java_library too.
-
-   Note: The "android-non-updatable" is treated as if it was a java_sdk_library
-   which it is not at the moment but will be in future.
-""")
+                   Note: The "android-non-updatable" is treated as if it was a
+                   java_sdk_library which it is not at the moment but will be in
+                   future.
+                """)
 
         return diffs
 
     def build_monolithic_flags(self, result):
-        self.report(f"""
-Attempting to build {_FLAGS_FILE} to verify that the
-bootclasspath_fragment has the correct hidden API flags...
-""")
+        self.report_dedent(f"""
+            Attempting to build {_FLAGS_FILE} to verify that the
+            bootclasspath_fragment has the correct hidden API flags...
+            """)
 
         # Build the hiddenapi-flags.csv file and extract any differences in
         # the flags between this bootclasspath_fragment and the monolithic
@@ -838,32 +881,34 @@
         self.load_all_flags()
 
         if result.diffs:
-            self.report(f"""
-There is a discrepancy between the hidden API flags created by the
-bootclasspath_fragment and the platform_bootclasspath. See preceding error
-messages to see which flags are inconsistent. The inconsistencies can occur for
-a couple of reasons:
+            self.report_dedent(f"""
+                There is a discrepancy between the hidden API flags created by
+                the bootclasspath_fragment and the platform_bootclasspath. See
+                preceding error messages to see which flags are inconsistent.
+                The inconsistencies can occur for a couple of reasons:
 
-If you are building against prebuilts of this bootclasspath_fragment then the
-prebuilt version of the sdk snapshot (specifically the hidden API flag files)
-are inconsistent with the prebuilt version of the apex {self.apex}. Please
-ensure that they are both updated from the same build.
+                If you are building against prebuilts of this
+                bootclasspath_fragment then the prebuilt version of the sdk
+                snapshot (specifically the hidden API flag files) are
+                inconsistent with the prebuilt version of the apex {self.apex}.
+                Please ensure that they are both updated from the same build.
 
-1. There are custom hidden API flags specified in the one of the files in
-   frameworks/base/boot/hiddenapi which apply to the bootclasspath_fragment but
-   which are not supplied to the bootclasspath_fragment module.
+                1. There are custom hidden API flags specified in the one of the
+                files in frameworks/base/boot/hiddenapi which apply to the
+                bootclasspath_fragment but which are not supplied to the
+                bootclasspath_fragment module.
 
-2. The bootclasspath_fragment specifies invalid "package_prefixes" or
-   "split_packages" properties that match packages and classes that it does not
-   provide.
-
-""")
+                2. The bootclasspath_fragment specifies invalid
+                "split_packages", "single_packages" and/of "package_prefixes"
+                properties that match packages and classes that it does not
+                provide.
+                """)
 
             # Check to see if there are any hiddenapi related properties that
             # need to be added to the
-            self.report("""
-Checking custom hidden API flags....
-""")
+            self.report_dedent("""
+                Checking custom hidden API flags....
+                """)
             self.check_frameworks_base_boot_hidden_api_files(result)
 
     def report_hidden_api_flag_file_changes(self, result, property_name,
@@ -1044,13 +1089,16 @@
         """).strip("\n")
 
     def analyze_hiddenapi_package_properties(self, result):
-        split_packages, single_packages, package_prefixes = \
-            self.compute_hiddenapi_package_properties()
+        self.compute_hiddenapi_package_properties(result)
+
+        def indent_lines(lines):
+            return "\n".join([f"        {cls}" for cls in lines])
 
         # TODO(b/202154151): Find those classes in split packages that are not
         #  part of an API, i.e. are an internal implementation class, and so
         #  can, and should, be safely moved out of the split packages.
 
+        split_packages = result.split_packages.keys()
         result.property_changes.append(
             HiddenApiPropertyChange(
                 property_name="split_packages",
@@ -1060,26 +1108,40 @@
             ))
 
         if split_packages:
-            self.report(f"""
-bootclasspath_fragment {self.bcpf} contains classes in packages that also
-contain classes provided by other sources, those packages are called split
-packages. Split packages should be avoided where possible but are often
-unavoidable when modularizing existing code.
+            self.report_dedent(f"""
+                bootclasspath_fragment {self.bcpf} contains classes in packages
+                that also contain classes provided by other bootclasspath
+                modules. Those packages are called split packages. Split
+                packages should be avoided where possible but are often
+                unavoidable when modularizing existing code.
 
-The hidden api processing needs to know which packages are split (and conversely
-which are not) so that it can optimize the hidden API flags to remove
-unnecessary implementation details.
+                The hidden api processing needs to know which packages are split
+                (and conversely which are not) so that it can optimize the
+                hidden API flags to remove unnecessary implementation details.
+
+                By default (for backwards compatibility) the
+                bootclasspath_fragment assumes that all packages are split
+                unless one of the package_prefixes or split_packages properties
+                are specified. While that is safe it is not optimal and can lead
+                to unnecessary implementation details leaking into the hidden
+                API flags. Adding an empty split_packages property allows the
+                flags to be optimized and remove any unnecessary implementation
+                details.
+                """)
+
+            for package in split_packages:
+                reason = result.split_packages[package]
+                self.report(f"""
+    Package {package} is split because while this bootclasspath_fragment
+    provides the following classes:
+{indent_lines(reason.bcpf)}
+
+    Other module(s) on the bootclasspath provides the following classes in
+    that package:
+{indent_lines(reason.other)}
 """)
 
-        self.report("""
-By default (for backwards compatibility) the bootclasspath_fragment assumes that
-all packages are split unless one of the package_prefixes or split_packages
-properties are specified. While that is safe it is not optimal and can lead to
-unnecessary implementation details leaking into the hidden API flags. Adding an
-empty split_packages property allows the flags to be optimized and remove any
-unnecessary implementation details.
-""")
-
+        single_packages = result.single_packages.keys()
         if single_packages:
             result.property_changes.append(
                 HiddenApiPropertyChange(
@@ -1091,10 +1153,34 @@
                     contain classes from other bootclasspath modules. Packages
                     should only be listed here when necessary for legacy
                     purposes, new packages should match a package prefix.
-                """),
+                    """),
                     action=PropertyChangeAction.REPLACE,
                 ))
 
+            self.report_dedent(f"""
+                bootclasspath_fragment {self.bcpf} contains classes from
+                packages that has sub-packages which contain classes provided by
+                other bootclasspath modules. Those packages are called single
+                packages. Single packages should be avoided where possible but
+                are often unavoidable when modularizing existing code.
+
+                Because some sub-packages contains classes from other
+                bootclasspath modules it is not possible to use the package as a
+                package prefix as that treats the package and all its
+                sub-packages as being provided by this module.  
+                """)
+            for package in single_packages:
+                reason = result.single_packages[package]
+                self.report(f"""
+    Package {package} is not a package prefix because while this
+    bootclasspath_fragment provides the following sub-packages:
+{indent_lines(reason.bcpf)}
+
+    Other module(s) on the bootclasspath provide the following sub-packages:
+{indent_lines(reason.other)}
+""")
+
+        package_prefixes = result.package_prefixes
         if package_prefixes:
             result.property_changes.append(
                 HiddenApiPropertyChange(
@@ -1111,38 +1197,40 @@
             signature_patterns_files = signature_patterns_files.removeprefix(
                 self.top_dir)
 
-            self.report(f"""
-The purpose of the hiddenapi split_packages and package_prefixes properties is
-to allow the removal of implementation details from the hidden API flags to
-reduce the coupling between sdk snapshots and the APEX runtime. It cannot
-eliminate that coupling completely though. Doing so may require changes to the
-code.
+            self.report_dedent(f"""
+                The purpose of the hiddenapi split_packages and package_prefixes
+                properties is to allow the removal of implementation details
+                from the hidden API flags to reduce the coupling between sdk
+                snapshots and the APEX runtime. It cannot eliminate that
+                coupling completely though. Doing so may require changes to the
+                code.
 
-This tool provides support for managing those properties but it cannot decide
-whether the set of package prefixes suggested is appropriate that needs the
-input of the developer.
+                This tool provides support for managing those properties but it
+                cannot decide whether the set of package prefixes suggested is
+                appropriate that needs the input of the developer.
 
-Please run the following command:
-    m {signature_patterns_files}
+                Please run the following command:
+                    m {signature_patterns_files}
 
-And then check the '{signature_patterns_files}' for any mention of
-implementation classes and packages (i.e. those classes/packages that do not
-contain any part of an API surface, including the hidden API). If they are
-found then the code should ideally be moved to a package unique to this module
-that is contained within a package that is part of an API surface.
+                And then check the '{signature_patterns_files}' for any mention
+                of implementation classes and packages (i.e. those
+                classes/packages that do not contain any part of an API surface,
+                including the hidden API). If they are found then the code
+                should ideally be moved to a package unique to this module that
+                is contained within a package that is part of an API surface.
 
-The format of the file is a list of patterns:
+                The format of the file is a list of patterns:
 
-* Patterns for split packages will list every class in that package.
+                * Patterns for split packages will list every class in that package.
 
-* Patterns for package prefixes will end with .../**.
+                * Patterns for package prefixes will end with .../**.
 
-* Patterns for packages which are not split but cannot use a package prefix
-because there are sub-packages which are provided by another module will end
-with .../*.
-""")
+                * Patterns for packages which are not split but cannot use a
+                package prefix because there are sub-packages which are provided
+                by another module will end with .../*.
+                """)
 
-    def compute_hiddenapi_package_properties(self):
+    def compute_hiddenapi_package_properties(self, result):
         trie = signature_trie()
         # Populate the trie with the classes that are provided by the
         # bootclasspath_fragment tagging them to make it clear where they
@@ -1151,6 +1239,7 @@
         for class_name in sorted_classes:
             trie.add(class_name + _FAKE_MEMBER, ClassProvider.BCPF)
 
+        # Now the same for monolithic classes.
         monolithic_classes = set()
         abs_flags_file = os.path.join(self.top_dir, _FLAGS_FILE)
         with open(abs_flags_file, "r", encoding="utf8") as f:
@@ -1165,15 +1254,54 @@
                         only_if_matches=True)
                     monolithic_classes.add(class_name)
 
-        split_packages = []
-        single_packages = []
-        package_prefixes = []
-        self.recurse_hiddenapi_packages_trie(trie, split_packages,
-                                             single_packages, package_prefixes)
-        return split_packages, single_packages, package_prefixes
+        self.recurse_hiddenapi_packages_trie(trie, result)
 
-    def recurse_hiddenapi_packages_trie(self, node, split_packages,
-                                        single_packages, package_prefixes):
+    @staticmethod
+    def selector_to_java_reference(node):
+        return node.selector.replace("/", ".")
+
+    @staticmethod
+    def determine_reason_for_single_package(node):
+        bcpf_packages = []
+        other_packages = []
+
+        def recurse(n):
+            if n.type != "package":
+                return
+
+            providers = n.get_matching_rows("*")
+            package_ref = BcpfAnalyzer.selector_to_java_reference(n)
+            if ClassProvider.BCPF in providers:
+                bcpf_packages.append(package_ref)
+            else:
+                other_packages.append(package_ref)
+
+            children = n.child_nodes()
+            if children:
+                for child in children:
+                    recurse(child)
+
+        recurse(node)
+        return PackagePropertyReason(bcpf=bcpf_packages, other=other_packages)
+
+    @staticmethod
+    def determine_reason_for_split_package(node):
+        bcpf_classes = []
+        other_classes = []
+        for child in node.child_nodes():
+            if child.type != "class":
+                continue
+
+            providers = child.values(lambda _: True)
+            class_ref = BcpfAnalyzer.selector_to_java_reference(child)
+            if ClassProvider.BCPF in providers:
+                bcpf_classes.append(class_ref)
+            else:
+                other_classes.append(class_ref)
+
+        return PackagePropertyReason(bcpf=bcpf_classes, other=other_classes)
+
+    def recurse_hiddenapi_packages_trie(self, node, result):
         nodes = node.child_nodes()
         if nodes:
             for child in nodes:
@@ -1181,7 +1309,7 @@
                 if child.type != "package":
                     continue
 
-                package = child.selector.replace("/", ".")
+                package = self.selector_to_java_reference(child)
 
                 providers = set(child.get_matching_rows("**"))
                 if not providers:
@@ -1192,7 +1320,7 @@
                     # The package and all its sub packages only contain
                     # classes provided by the bootclasspath_fragment.
                     logging.debug("Package '%s.**' is not split", package)
-                    package_prefixes.append(package)
+                    result.package_prefixes.append(package)
                     # There is no point traversing into the sub packages.
                     continue
                 elif providers == {ClassProvider.OTHER}:
@@ -1217,8 +1345,17 @@
                 elif providers == {ClassProvider.BCPF}:
                     # The package only contains classes provided by the
                     # bootclasspath_fragment.
-                    logging.debug("Package '%s.*' is not split", package)
-                    single_packages.append(package)
+                    logging.debug(
+                        "Package '%s.*' is not split but does have "
+                        "sub-packages from other modules", package)
+
+                    # Partition the sub-packages into those that are provided by
+                    # this bootclasspath_fragment and those provided by other
+                    # modules. They can be used to explain the reason for the
+                    # single package to developers.
+                    reason = self.determine_reason_for_single_package(child)
+                    result.single_packages[package] = reason
+
                 elif providers == {ClassProvider.OTHER}:
                     # The package contains no classes provided by the
                     # bootclasspath_fragment. Child nodes make contain such
@@ -1229,11 +1366,15 @@
                     # The package contains classes provided by both the
                     # bootclasspath_fragment and some other source.
                     logging.debug("Package '%s.*' is split", package)
-                    split_packages.append(package)
 
-                self.recurse_hiddenapi_packages_trie(child, split_packages,
-                                                     single_packages,
-                                                     package_prefixes)
+                    # Partition the classes in this split package into those
+                    # that come from this bootclasspath_fragment and those that
+                    # come from other modules. That can be used to explain the
+                    # reason for the split package to developers.
+                    reason = self.determine_reason_for_split_package(child)
+                    result.split_packages[package] = reason
+
+                self.recurse_hiddenapi_packages_trie(child, result)
 
 
 def newline_stripping_iter(iterator):
diff --git a/scripts/hiddenapi/analyze_bcpf_test.py b/scripts/hiddenapi/analyze_bcpf_test.py
index 650dd54..a32ffd0 100644
--- a/scripts/hiddenapi/analyze_bcpf_test.py
+++ b/scripts/hiddenapi/analyze_bcpf_test.py
@@ -377,6 +377,7 @@
 La/b/c/D;->m()V
 La/b/c/E;->m()V
 La/b/c/d/E;->m()V
+La/b/c/d/e/F;->m()V
 Lb/c/D;->m()V
 Lb/c/E;->m()V
 Lb/c/d/E;->m()V
@@ -385,11 +386,21 @@
         analyzer = self.create_analyzer_for_test(fs)
         analyzer.load_all_flags()
 
-        split_packages, single_packages, package_prefixes = \
-            analyzer.compute_hiddenapi_package_properties()
-        self.assertEqual(["a.b"], split_packages)
-        self.assertEqual(["a.b.c"], single_packages)
-        self.assertEqual(["b"], package_prefixes)
+        result = ab.Result()
+        analyzer.compute_hiddenapi_package_properties(result)
+        self.assertEqual(["a.b"], list(result.split_packages.keys()))
+
+        reason = result.split_packages["a.b"]
+        self.assertEqual(["a.b.C"], reason.bcpf)
+        self.assertEqual(["a.b.D", "a.b.E"], reason.other)
+
+        self.assertEqual(["a.b.c"], list(result.single_packages.keys()))
+
+        reason = result.single_packages["a.b.c"]
+        self.assertEqual(["a.b.c"], reason.bcpf)
+        self.assertEqual(["a.b.c.d", "a.b.c.d.e"], reason.other)
+
+        self.assertEqual(["b"], result.package_prefixes)
 
 
 class TestHiddenApiPropertyChange(unittest.TestCase):
diff --git a/scripts/hiddenapi/signature_trie.py b/scripts/hiddenapi/signature_trie.py
index e813a97..3650fa1 100644
--- a/scripts/hiddenapi/signature_trie.py
+++ b/scripts/hiddenapi/signature_trie.py
@@ -45,7 +45,9 @@
         :return: A list of iterables of all the values associated with
             this node and its children.
         """
-        raise NotImplementedError("Please Implement this method")
+        values = []
+        self.append_values(values, selector)
+        return values
 
     def append_values(self, values, selector):
         """Append the values associated with this node and its children.
@@ -313,12 +315,8 @@
                 node = node.nodes[element]
             else:
                 return []
-        return chain.from_iterable(node.values(selector))
 
-    def values(self, selector):
-        values = []
-        self.append_values(values, selector)
-        return values
+        return node.values(selector)
 
     def append_values(self, values, selector):
         for key, node in self.nodes.items():
@@ -336,11 +334,8 @@
     # The value associated with this leaf.
     value: typing.Any
 
-    def values(self, selector):
-        return [[self.value]]
-
     def append_values(self, values, selector):
-        values.append([self.value])
+        values.append(self.value)
 
     def child_nodes(self):
         return []
diff --git a/scripts/hiddenapi/signature_trie_test.py b/scripts/hiddenapi/signature_trie_test.py
index 1295691..6d4e660 100755
--- a/scripts/hiddenapi/signature_trie_test.py
+++ b/scripts/hiddenapi/signature_trie_test.py
@@ -150,6 +150,27 @@
             str(context.exception))
 
 
+class TestValues(unittest.TestCase):
+    def test_add_then_get(self):
+        trie = signature_trie()
+        trie.add("La/b/C;->l()", 1)
+        trie.add("La/b/C$D;->m()", "A")
+        trie.add("La/b/C$D;->n()", {})
+
+        package_a_node = next(iter(trie.child_nodes()))
+        self.assertEqual("package", package_a_node.type)
+        self.assertEqual("a", package_a_node.selector)
+
+        package_b_node = next(iter(package_a_node.child_nodes()))
+        self.assertEqual("package", package_b_node.type)
+        self.assertEqual("a/b", package_b_node.selector)
+
+        class_c_node = next(iter(package_b_node.child_nodes()))
+        self.assertEqual("class", class_c_node.type)
+        self.assertEqual("a/b/C", class_c_node.selector)
+
+        self.assertEqual([1, "A", {}], class_c_node.values(lambda _: True))
+
 class TestGetMatchingRows(unittest.TestCase):
     extractInput = """
 Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;