Merge "bp2build: add converted modules to codegen metrics, and remove the compat layer."
diff --git a/android/config.go b/android/config.go
index 3e41fbb..993aaa7 100644
--- a/android/config.go
+++ b/android/config.go
@@ -1659,6 +1659,20 @@
 	return ConfiguredJarList{apexes, jars}
 }
 
+// Append a list of (apex, jar) pairs to the list.
+func (l *ConfiguredJarList) AppendList(other ConfiguredJarList) ConfiguredJarList {
+	apexes := make([]string, 0, l.Len()+other.Len())
+	jars := make([]string, 0, l.Len()+other.Len())
+
+	apexes = append(apexes, l.apexes...)
+	jars = append(jars, l.jars...)
+
+	apexes = append(apexes, other.apexes...)
+	jars = append(jars, other.jars...)
+
+	return ConfiguredJarList{apexes, jars}
+}
+
 // RemoveList filters out a list of (apex, jar) pairs from the receiving list of pairs.
 func (l *ConfiguredJarList) RemoveList(list ConfiguredJarList) ConfiguredJarList {
 	apexes := make([]string, 0, l.Len())
diff --git a/android/deapexer.go b/android/deapexer.go
index de3f635..9290481 100644
--- a/android/deapexer.go
+++ b/android/deapexer.go
@@ -73,7 +73,7 @@
 	// exported file name is the apex relative path, e.g. javalib/core-libart.jar.
 	//
 	// See Prebuilt.ApexInfoMutator for more information.
-	exports map[string]Path
+	exports map[string]WritablePath
 }
 
 // PrebuiltExportPath provides the path, or nil if not available, of a file exported from the
@@ -82,7 +82,7 @@
 // The exported file is identified by the apex relative path, e.g. "javalib/core-libart.jar".
 //
 // See apex/deapexer.go for more information.
-func (i DeapexerInfo) PrebuiltExportPath(apexRelativePath string) Path {
+func (i DeapexerInfo) PrebuiltExportPath(apexRelativePath string) WritablePath {
 	path := i.exports[apexRelativePath]
 	return path
 }
@@ -95,7 +95,7 @@
 // for use with a prebuilt_apex module.
 //
 // See apex/deapexer.go for more information.
-func NewDeapexerInfo(exports map[string]Path) DeapexerInfo {
+func NewDeapexerInfo(exports map[string]WritablePath) DeapexerInfo {
 	return DeapexerInfo{
 		exports: exports,
 	}
diff --git a/android/testing.go b/android/testing.go
index bd2faa2..7a89fc4 100644
--- a/android/testing.go
+++ b/android/testing.go
@@ -491,6 +491,66 @@
 	ctx.preSingletons = append(ctx.preSingletons, newPreSingleton(name, factory))
 }
 
+// ModuleVariantForTests selects a specific variant of the module with the given
+// name by matching the variations map against the variations of each module
+// variant. A module variant matches the map if every variation that exists in
+// both have the same value. Both the module and the map are allowed to have
+// extra variations that the other doesn't have. Panics if not exactly one
+// module variant matches.
+func (ctx *TestContext) ModuleVariantForTests(name string, matchVariations map[string]string) TestingModule {
+	modules := []Module{}
+	ctx.VisitAllModules(func(m blueprint.Module) {
+		if ctx.ModuleName(m) == name {
+			am := m.(Module)
+			amMut := am.base().commonProperties.DebugMutators
+			amVar := am.base().commonProperties.DebugVariations
+			matched := true
+			for i, mut := range amMut {
+				if wantedVar, found := matchVariations[mut]; found && amVar[i] != wantedVar {
+					matched = false
+					break
+				}
+			}
+			if matched {
+				modules = append(modules, am)
+			}
+		}
+	})
+
+	if len(modules) == 0 {
+		// Show all the modules or module variants that do exist.
+		var allModuleNames []string
+		var allVariants []string
+		ctx.VisitAllModules(func(m blueprint.Module) {
+			allModuleNames = append(allModuleNames, ctx.ModuleName(m))
+			if ctx.ModuleName(m) == name {
+				allVariants = append(allVariants, m.(Module).String())
+			}
+		})
+
+		if len(allVariants) == 0 {
+			panic(fmt.Errorf("failed to find module %q. All modules:\n  %s",
+				name, strings.Join(SortedUniqueStrings(allModuleNames), "\n  ")))
+		} else {
+			sort.Strings(allVariants)
+			panic(fmt.Errorf("failed to find module %q matching %v. All variants:\n  %s",
+				name, matchVariations, strings.Join(allVariants, "\n  ")))
+		}
+	}
+
+	if len(modules) > 1 {
+		moduleStrings := []string{}
+		for _, m := range modules {
+			moduleStrings = append(moduleStrings, m.String())
+		}
+		sort.Strings(moduleStrings)
+		panic(fmt.Errorf("module %q has more than one variant that match %v:\n  %s",
+			name, matchVariations, strings.Join(moduleStrings, "\n  ")))
+	}
+
+	return newTestingModule(ctx.config, modules[0])
+}
+
 func (ctx *TestContext) ModuleForTests(name, variant string) TestingModule {
 	var module Module
 	ctx.VisitAllModules(func(m blueprint.Module) {
diff --git a/apex/apex.go b/apex/apex.go
index e3edc68..2d153e2 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -1569,6 +1569,11 @@
 	af.jacocoReportClassesFile = module.JacocoReportClassesFile()
 	af.lintDepSets = module.LintDepSets()
 	af.customStem = module.Stem() + ".jar"
+	if dexpreopter, ok := module.(java.DexpreopterInterface); ok {
+		for _, install := range dexpreopter.DexpreoptBuiltInstalledForApex() {
+			af.requiredModuleNames = append(af.requiredModuleNames, install.FullModuleName())
+		}
+	}
 	return af
 }
 
diff --git a/apex/deapexer.go b/apex/deapexer.go
index c70da15..2c1835a 100644
--- a/apex/deapexer.go
+++ b/apex/deapexer.go
@@ -97,7 +97,7 @@
 	// Create and remember the directory into which the .apex file's contents will be unpacked.
 	deapexerOutput := android.PathForModuleOut(ctx, "deapexer")
 
-	exports := make(map[string]android.Path)
+	exports := make(map[string]android.WritablePath)
 
 	// Create mappings from apex relative path to the extracted file's path.
 	exportedPaths := make(android.Paths, 0, len(exports))
diff --git a/apex/prebuilt.go b/apex/prebuilt.go
index c4794dc..4833a64 100644
--- a/apex/prebuilt.go
+++ b/apex/prebuilt.go
@@ -178,13 +178,19 @@
 			// If the exported java module provides a dex jar path then add it to the list of apexFiles.
 			path := child.(interface{ DexJarBuildPath() android.Path }).DexJarBuildPath()
 			if path != nil {
-				p.apexFilesForAndroidMk = append(p.apexFilesForAndroidMk, apexFile{
+				af := apexFile{
 					module:              child,
 					moduleDir:           ctx.OtherModuleDir(child),
 					androidMkModuleName: name,
 					builtFile:           path,
 					class:               javaSharedLib,
-				})
+				}
+				if module, ok := child.(java.DexpreopterInterface); ok {
+					for _, install := range module.DexpreoptBuiltInstalledForApex() {
+						af.requiredModuleNames = append(af.requiredModuleNames, install.FullModuleName())
+					}
+				}
+				p.apexFilesForAndroidMk = append(p.apexFilesForAndroidMk, af)
 			}
 		} else if tag == exportedBootclasspathFragmentTag {
 			// Visit the children of the bootclasspath_fragment.
@@ -195,6 +201,14 @@
 	})
 }
 
+func (p *prebuiltCommon) addRequiredModules(entries *android.AndroidMkEntries) {
+	for _, fi := range p.apexFilesForAndroidMk {
+		entries.AddStrings("LOCAL_REQUIRED_MODULES", fi.requiredModuleNames...)
+		entries.AddStrings("LOCAL_TARGET_REQUIRED_MODULES", fi.targetRequiredModuleNames...)
+		entries.AddStrings("LOCAL_HOST_REQUIRED_MODULES", fi.hostRequiredModuleNames...)
+	}
+}
+
 func (p *prebuiltCommon) AndroidMkEntries() []android.AndroidMkEntries {
 	entriesList := []android.AndroidMkEntries{
 		{
@@ -213,6 +227,7 @@
 					if len(postInstallCommands) > 0 {
 						entries.SetString("LOCAL_POST_INSTALL_CMD", strings.Join(postInstallCommands, " && "))
 					}
+					p.addRequiredModules(entries)
 				},
 			},
 		},
diff --git a/bazel/cquery/request_type.go b/bazel/cquery/request_type.go
index 131f0ec..d731f3e 100644
--- a/bazel/cquery/request_type.go
+++ b/bazel/cquery/request_type.go
@@ -25,6 +25,7 @@
 	// be a subset of OutputFiles. (or shared libraries, this will be equal to OutputFiles,
 	// but general cc_library will also have dynamic libraries in output files).
 	RootDynamicLibraries []string
+	TocFile              string
 }
 
 type getOutputFilesRequestType struct{}
@@ -100,14 +101,15 @@
 func (g getCcInfoType) StarlarkFunctionBody() string {
 	return `
 outputFiles = [f.path for f in target.files.to_list()]
+cc_info = providers(target)["CcInfo"]
 
-includes = providers(target)["CcInfo"].compilation_context.includes.to_list()
-system_includes = providers(target)["CcInfo"].compilation_context.system_includes.to_list()
+includes = cc_info.compilation_context.includes.to_list()
+system_includes = cc_info.compilation_context.system_includes.to_list()
 
 ccObjectFiles = []
 staticLibraries = []
 rootStaticArchives = []
-linker_inputs = providers(target)["CcInfo"].linking_context.linker_inputs.to_list()
+linker_inputs = cc_info.linking_context.linker_inputs.to_list()
 
 for linker_input in linker_inputs:
   for library in linker_input.libraries:
@@ -120,11 +122,17 @@
 
 rootDynamicLibraries = []
 
-if "@rules_cc//examples:experimental_cc_shared_library.bzl%CcSharedLibraryInfo" in providers(target):
-  shared_info = providers(target)["@rules_cc//examples:experimental_cc_shared_library.bzl%CcSharedLibraryInfo"]
+shared_info_tag = "@rules_cc//examples:experimental_cc_shared_library.bzl%CcSharedLibraryInfo"
+if shared_info_tag in providers(target):
+  shared_info = providers(target)[shared_info_tag]
   for lib in shared_info.linker_input.libraries:
     rootDynamicLibraries += [lib.dynamic_library.path]
 
+toc_file = ""
+toc_file_tag = "//build/bazel/rules:generate_toc.bzl%CcTocInfo"
+if toc_file_tag in providers(target):
+  toc_file = providers(target)[toc_file_tag].toc.path
+
 returns = [
   outputFiles,
   staticLibraries,
@@ -132,10 +140,10 @@
   includes,
   system_includes,
   rootStaticArchives,
-  rootDynamicLibraries
+  rootDynamicLibraries,
 ]
 
-return "|".join([", ".join(r) for r in returns])`
+return "|".join([", ".join(r) for r in returns] + [toc_file])`
 }
 
 // ParseResult returns a value obtained by parsing the result of the request's Starlark function.
@@ -146,7 +154,7 @@
 	var ccObjects []string
 
 	splitString := strings.Split(rawString, "|")
-	if expectedLen := 7; len(splitString) != expectedLen {
+	if expectedLen := 8; len(splitString) != expectedLen {
 		return CcInfo{}, fmt.Errorf("Expected %d items, got %q", expectedLen, splitString)
 	}
 	outputFilesString := splitString[0]
@@ -159,6 +167,7 @@
 	systemIncludes := splitOrEmpty(splitString[4], ", ")
 	rootStaticArchives := splitOrEmpty(splitString[5], ", ")
 	rootDynamicLibraries := splitOrEmpty(splitString[6], ", ")
+	tocFile := splitString[7] // NOTE: Will be the empty string if there wasn't
 	return CcInfo{
 		OutputFiles:          outputFiles,
 		CcObjectFiles:        ccObjects,
@@ -167,6 +176,7 @@
 		SystemIncludes:       systemIncludes,
 		RootStaticArchives:   rootStaticArchives,
 		RootDynamicLibraries: rootDynamicLibraries,
+		TocFile:              tocFile,
 	}, nil
 }
 
diff --git a/bazel/cquery/request_type_test.go b/bazel/cquery/request_type_test.go
index 49019ab..34d0832 100644
--- a/bazel/cquery/request_type_test.go
+++ b/bazel/cquery/request_type_test.go
@@ -71,7 +71,7 @@
 	}{
 		{
 			description: "no result",
-			input:       "||||||",
+			input:       "|||||||",
 			expectedOutput: CcInfo{
 				OutputFiles:          []string{},
 				CcObjectFiles:        []string{},
@@ -80,11 +80,12 @@
 				SystemIncludes:       []string{},
 				RootStaticArchives:   []string{},
 				RootDynamicLibraries: []string{},
+				TocFile:              "",
 			},
 		},
 		{
 			description: "only output",
-			input:       "test||||||",
+			input:       "test|||||||",
 			expectedOutput: CcInfo{
 				OutputFiles:          []string{"test"},
 				CcObjectFiles:        []string{},
@@ -93,11 +94,12 @@
 				SystemIncludes:       []string{},
 				RootStaticArchives:   []string{},
 				RootDynamicLibraries: []string{},
+				TocFile:              "",
 			},
 		},
 		{
 			description: "all items set",
-			input:       "out1, out2|static_lib1, static_lib2|object1, object2|., dir/subdir|system/dir, system/other/dir|rootstaticarchive1|rootdynamiclibrary1",
+			input:       "out1, out2|static_lib1, static_lib2|object1, object2|., dir/subdir|system/dir, system/other/dir|rootstaticarchive1|rootdynamiclibrary1|lib.so.toc",
 			expectedOutput: CcInfo{
 				OutputFiles:          []string{"out1", "out2"},
 				CcObjectFiles:        []string{"object1", "object2"},
@@ -106,19 +108,20 @@
 				SystemIncludes:       []string{"system/dir", "system/other/dir"},
 				RootStaticArchives:   []string{"rootstaticarchive1"},
 				RootDynamicLibraries: []string{"rootdynamiclibrary1"},
+				TocFile:              "lib.so.toc",
 			},
 		},
 		{
 			description:          "too few result splits",
 			input:                "|",
 			expectedOutput:       CcInfo{},
-			expectedErrorMessage: fmt.Sprintf("Expected %d items, got %q", 7, []string{"", ""}),
+			expectedErrorMessage: fmt.Sprintf("Expected %d items, got %q", 8, []string{"", ""}),
 		},
 		{
 			description:          "too many result splits",
 			input:                strings.Repeat("|", 8),
 			expectedOutput:       CcInfo{},
-			expectedErrorMessage: fmt.Sprintf("Expected %d items, got %q", 7, make([]string, 9)),
+			expectedErrorMessage: fmt.Sprintf("Expected %d items, got %q", 8, make([]string, 9)),
 		},
 	}
 	for _, tc := range testCases {
diff --git a/cc/builder.go b/cc/builder.go
index b07acf1..4b0a4b6 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -199,8 +199,18 @@
 	// Rule for invoking clang-tidy (a clang-based linter).
 	clangTidy, clangTidyRE = pctx.RemoteStaticRules("clangTidy",
 		blueprint.RuleParams{
-			Command:     "rm -f $out && $tidyVars $reTemplate${config.ClangBin}/clang-tidy $tidyFlags $in -- $cFlags && touch $out",
-			CommandDeps: []string{"${config.ClangBin}/clang-tidy"},
+			Depfile: "${out}.d",
+			Deps:    blueprint.DepsGCC,
+			// Pick bash because some machines with old /bin/sh cannot handle arrays.
+			// All $cFlags and $tidyFlags should have single quotes escaped.
+			// Assume no single quotes in other parameters like $in, $out, $ccCmd.
+			Command: "/bin/bash -c 'SRCF=$in; TIDYF=$out; CLANGFLAGS=($cFlags); " +
+				"rm -f $$TIDYF $${TIDYF}.d && " +
+				"${config.CcWrapper}$ccCmd \"$${CLANGFLAGS[@]}\" -E -o /dev/null $$SRCF " +
+				"-MQ $$TIDYF -MD -MF $${TIDYF}.d && " +
+				"$tidyVars $reTemplate${config.ClangBin}/clang-tidy $tidyFlags $$SRCF " +
+				"-- \"$${CLANGFLAGS[@]}\" && touch $$TIDYF'",
+			CommandDeps: []string{"${config.ClangBin}/clang-tidy", "$ccCmd"},
 		},
 		&remoteexec.REParams{
 			Labels:               map[string]string{"type": "lint", "tool": "clang-tidy", "lang": "cpp"},
@@ -214,7 +224,7 @@
 			// (1) New timestamps trigger clang and clang-tidy compilations again.
 			// (2) Changing source files caused concurrent clang or clang-tidy jobs to crash.
 			Platform: map[string]string{remoteexec.PoolKey: "${config.REClangTidyPool}"},
-		}, []string{"cFlags", "tidyFlags", "tidyVars"}, []string{})
+		}, []string{"ccCmd", "cFlags", "tidyFlags", "tidyVars"}, []string{})
 
 	_ = pctx.SourcePathVariable("yasmCmd", "prebuilts/misc/${config.HostPrebuiltTag}/yasm/yasm")
 
@@ -437,6 +447,12 @@
 	}
 }
 
+func escapeSingleQuotes(s string) string {
+	// Replace single quotes to work when embedded in a single quoted string for bash.
+	// Relying on string concatenation of bash to get A'B from quoted 'A'\''B'.
+	return strings.Replace(s, `'`, `'\''`, -1)
+}
+
 // Generate rules for compiling multiple .c, .cpp, or .S files to individual .o files
 func transformSourceToObj(ctx android.ModuleContext, subdir string, srcFiles android.Paths,
 	flags builderFlags, pathDeps android.Paths, cFlagsDeps android.Paths) Objects {
@@ -672,14 +688,12 @@
 				Description: "clang-tidy " + srcFile.Rel(),
 				Output:      tidyFile,
 				Input:       srcFile,
-				// We must depend on objFile, since clang-tidy doesn't
-				// support exporting dependencies.
-				Implicit:  objFile,
-				Implicits: cFlagsDeps,
-				OrderOnly: pathDeps,
+				Implicits:   cFlagsDeps,
+				OrderOnly:   pathDeps,
 				Args: map[string]string{
-					"cFlags":    shareFlags("cFlags", moduleToolingFlags),
-					"tidyFlags": shareFlags("tidyFlags", config.TidyFlagsForSrcFile(srcFile, flags.tidyFlags)),
+					"ccCmd":     ccCmd,
+					"cFlags":    shareFlags("cFlags", escapeSingleQuotes(moduleToolingFlags)),
+					"tidyFlags": shareFlags("tidyFlags", escapeSingleQuotes(config.TidyFlagsForSrcFile(srcFile, flags.tidyFlags))),
 					"tidyVars":  tidyVars, // short and not shared
 				},
 			})
diff --git a/cc/library.go b/cc/library.go
index 8a663ff..703d57f 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -370,6 +370,7 @@
 	module, library := NewLibrary(android.HostAndDeviceSupported)
 	library.BuildOnlyShared()
 	module.sdkMemberTypes = []android.SdkMemberType{sharedLibrarySdkMemberType}
+	module.bazelHandler = &ccLibraryBazelHandler{module: module}
 	return module.Init()
 }
 
@@ -603,7 +604,10 @@
 
 	handler.module.linker.(*libraryDecorator).unstrippedOutputFile = outputFilePath
 
-	tocFile := getTocFile(ctx, label, ccInfo.OutputFiles)
+	var tocFile android.OptionalPath
+	if len(ccInfo.TocFile) > 0 {
+		tocFile = android.OptionalPathForPath(android.PathForBazelOut(ctx, ccInfo.TocFile))
+	}
 	handler.module.linker.(*libraryDecorator).tocFile = tocFile
 
 	ctx.SetProvider(SharedLibraryInfoProvider, SharedLibraryInfo{
@@ -616,25 +620,6 @@
 	return true
 }
 
-// getTocFile looks for the .so.toc file in the target's output files, if any. The .so.toc file
-// contains the table of contents of all symbols of a shared object.
-func getTocFile(ctx android.ModuleContext, label string, outputFiles []string) android.OptionalPath {
-	var tocFile string
-	for _, file := range outputFiles {
-		if strings.HasSuffix(file, ".so.toc") {
-			if tocFile != "" {
-				ctx.ModuleErrorf("The %s target cannot produce more than 1 .toc file.", label)
-			}
-			tocFile = file
-			// Don't break to validate that there are no multiple .toc files per .so.
-		}
-	}
-	if tocFile == "" {
-		return android.OptionalPath{}
-	}
-	return android.OptionalPathForPath(android.PathForBazelOut(ctx, tocFile))
-}
-
 func (handler *ccLibraryBazelHandler) GenerateBazelBuildActions(ctx android.ModuleContext, label string) bool {
 	bazelCtx := ctx.Config().BazelContext
 	ccInfo, ok, err := bazelCtx.GetCcInfo(label, ctx.Arch().ArchType)
diff --git a/cc/library_test.go b/cc/library_test.go
index 6b349b6..7ddfaa7 100644
--- a/cc/library_test.go
+++ b/cc/library_test.go
@@ -320,3 +320,48 @@
 		libfoo.Args["ldFlags"], "-Wl,--dynamic-list,foo.dynamic.txt")
 
 }
+
+func TestCcLibrarySharedWithBazel(t *testing.T) {
+	bp := `
+cc_library_shared {
+	name: "foo",
+	srcs: ["foo.cc"],
+	bazel_module: { label: "//foo/bar:bar" },
+}`
+	config := TestConfig(t.TempDir(), android.Android, nil, bp, nil)
+	config.BazelContext = android.MockBazelContext{
+		OutputBaseDir: "outputbase",
+		LabelToCcInfo: map[string]cquery.CcInfo{
+			"//foo/bar:bar": cquery.CcInfo{
+				CcObjectFiles:        []string{"foo.o"},
+				Includes:             []string{"include"},
+				SystemIncludes:       []string{"system_include"},
+				RootDynamicLibraries: []string{"foo.so"},
+				TocFile:              "foo.so.toc",
+			},
+		},
+	}
+	ctx := testCcWithConfig(t, config)
+
+	sharedFoo := ctx.ModuleForTests("foo", "android_arm_armv7-a-neon_shared").Module()
+	producer := sharedFoo.(android.OutputFileProducer)
+	outputFiles, err := producer.OutputFiles("")
+	if err != nil {
+		t.Errorf("Unexpected error getting cc_object outputfiles %s", err)
+	}
+	expectedOutputFiles := []string{"outputbase/execroot/__main__/foo.so"}
+	android.AssertDeepEquals(t, "output files", expectedOutputFiles, outputFiles.Strings())
+
+	tocFilePath := sharedFoo.(*Module).Toc()
+	if !tocFilePath.Valid() {
+		t.Errorf("Invalid tocFilePath: %s", tocFilePath)
+	}
+	tocFile := tocFilePath.Path()
+	expectedToc := "outputbase/execroot/__main__/foo.so.toc"
+	android.AssertStringEquals(t, "toc file", expectedToc, tocFile.String())
+
+	entries := android.AndroidMkEntriesForTest(t, ctx, sharedFoo)[0]
+	expectedFlags := []string{"-Ioutputbase/execroot/__main__/include", "-isystem outputbase/execroot/__main__/system_include"}
+	gotFlags := entries.EntryMap["LOCAL_EXPORT_CFLAGS"]
+	android.AssertDeepEquals(t, "androidmk exported cflags", expectedFlags, gotFlags)
+}
diff --git a/dexpreopt/dexpreopt.go b/dexpreopt/dexpreopt.go
index 1401c75..7733c1b 100644
--- a/dexpreopt/dexpreopt.go
+++ b/dexpreopt/dexpreopt.go
@@ -110,17 +110,12 @@
 		return true
 	}
 
-	// Don't preopt system server jars that are updatable.
-	if global.ApexSystemServerJars.ContainsJar(module.Name) {
-		return true
-	}
-
 	// If OnlyPreoptBootImageAndSystemServer=true and module is not in boot class path skip
 	// Also preopt system server jars since selinux prevents system server from loading anything from
 	// /data. If we don't do this they will need to be extracted which is not favorable for RAM usage
 	// or performance. If PreoptExtractedApk is true, we ignore the only preopt boot image options.
 	if global.OnlyPreoptBootImageAndSystemServer && !global.BootJars.ContainsJar(module.Name) &&
-		!global.SystemServerJars.ContainsJar(module.Name) && !module.PreoptExtractedApk {
+		!AllSystemServerJars(ctx, global).ContainsJar(module.Name) && !module.PreoptExtractedApk {
 		return true
 	}
 
@@ -201,6 +196,14 @@
 	return profilePath
 }
 
+// Returns the dex location of a system server java library.
+func GetSystemServerDexLocation(global *GlobalConfig, lib string) string {
+	if apex := global.ApexSystemServerJars.ApexOfJar(lib); apex != "" {
+		return fmt.Sprintf("/apex/%s/javalib/%s.jar", apex, lib)
+	}
+	return fmt.Sprintf("/system/framework/%s.jar", lib)
+}
+
 func dexpreoptCommand(ctx android.PathContext, globalSoong *GlobalSoongConfig, global *GlobalConfig,
 	module *ModuleConfig, rule *android.RuleBuilder, archIdx int, profile android.WritablePath,
 	appImage bool, generateDM bool) {
@@ -216,6 +219,13 @@
 	}
 
 	toOdexPath := func(path string) string {
+		if global.ApexSystemServerJars.ContainsJar(module.Name) {
+			return filepath.Join(
+				"/system/framework/oat",
+				arch.String(),
+				strings.ReplaceAll(path[1:], "/", "@")+"@classes.odex")
+		}
+
 		return filepath.Join(
 			filepath.Dir(path),
 			"oat",
@@ -234,20 +244,21 @@
 
 	invocationPath := odexPath.ReplaceExtension(ctx, "invocation")
 
-	systemServerJars := NonApexSystemServerJars(ctx, global)
+	systemServerJars := AllSystemServerJars(ctx, global)
 
 	rule.Command().FlagWithArg("mkdir -p ", filepath.Dir(odexPath.String()))
 	rule.Command().FlagWithOutput("rm -f ", odexPath)
 
-	if jarIndex := android.IndexList(module.Name, systemServerJars); jarIndex >= 0 {
+	if jarIndex := systemServerJars.IndexOfJar(module.Name); jarIndex >= 0 {
 		// System server jars should be dexpreopted together: class loader context of each jar
 		// should include all preceding jars on the system server classpath.
 
 		var clcHost android.Paths
 		var clcTarget []string
-		for _, lib := range systemServerJars[:jarIndex] {
+		for i := 0; i < jarIndex; i++ {
+			lib := systemServerJars.Jar(i)
 			clcHost = append(clcHost, SystemServerDexJarHostPath(ctx, lib))
-			clcTarget = append(clcTarget, filepath.Join("/system/framework", lib+".jar"))
+			clcTarget = append(clcTarget, GetSystemServerDexLocation(global, lib))
 		}
 
 		// Copy the system server jar to a predefined location where dex2oat will find it.
@@ -362,7 +373,7 @@
 
 	if !android.PrefixInList(preoptFlags, "--compiler-filter=") {
 		var compilerFilter string
-		if global.SystemServerJars.ContainsJar(module.Name) {
+		if systemServerJars.ContainsJar(module.Name) {
 			// Jars of system server, use the product option if it is set, speed otherwise.
 			if global.SystemServerCompilerFilter != "" {
 				compilerFilter = global.SystemServerCompilerFilter
@@ -416,7 +427,7 @@
 
 	// PRODUCT_SYSTEM_SERVER_DEBUG_INFO overrides WITH_DEXPREOPT_DEBUG_INFO.
 	// PRODUCT_OTHER_JAVA_DEBUG_INFO overrides WITH_DEXPREOPT_DEBUG_INFO.
-	if global.SystemServerJars.ContainsJar(module.Name) {
+	if systemServerJars.ContainsJar(module.Name) {
 		if global.AlwaysSystemServerDebugInfo {
 			debugInfo = true
 		} else if global.NeverSystemServerDebugInfo {
@@ -518,14 +529,15 @@
 	}
 }
 
-var nonApexSystemServerJarsKey = android.NewOnceKey("nonApexSystemServerJars")
+var allSystemServerJarsKey = android.NewOnceKey("allSystemServerJars")
 
 // TODO: eliminate the superficial global config parameter by moving global config definition
 // from java subpackage to dexpreopt.
-func NonApexSystemServerJars(ctx android.PathContext, global *GlobalConfig) []string {
-	return ctx.Config().Once(nonApexSystemServerJarsKey, func() interface{} {
-		return android.RemoveListFromList(global.SystemServerJars.CopyOfJars(), global.ApexSystemServerJars.CopyOfJars())
-	}).([]string)
+func AllSystemServerJars(ctx android.PathContext, global *GlobalConfig) *android.ConfiguredJarList {
+	return ctx.Config().Once(allSystemServerJarsKey, func() interface{} {
+		allSystemServerJars := global.SystemServerJars.AppendList(global.ApexSystemServerJars)
+		return &allSystemServerJars
+	}).(*android.ConfiguredJarList)
 }
 
 // A predefined location for the system server dex jars. This is needed in order to generate
@@ -551,12 +563,12 @@
 	mctx, isModule := ctx.(android.ModuleContext)
 	if isModule {
 		config := GetGlobalConfig(ctx)
-		jars := NonApexSystemServerJars(ctx, config)
+		jars := AllSystemServerJars(ctx, config)
 		mctx.WalkDeps(func(dep android.Module, parent android.Module) bool {
-			depIndex := android.IndexList(dep.Name(), jars)
+			depIndex := jars.IndexOfJar(dep.Name())
 			if jarIndex < depIndex && !config.BrokenSuboptimalOrderOfSystemServerJars {
-				jar := jars[jarIndex]
-				dep := jars[depIndex]
+				jar := jars.Jar(jarIndex)
+				dep := jars.Jar(depIndex)
 				mctx.ModuleErrorf("non-optimal order of jars on the system server classpath:"+
 					" '%s' precedes its dependency '%s', so dexpreopt is unable to resolve any"+
 					" references from '%s' to '%s'.\n", jar, dep, jar, dep)
diff --git a/dexpreopt/dexpreopt_test.go b/dexpreopt/dexpreopt_test.go
index 4ee61b6..798d776 100644
--- a/dexpreopt/dexpreopt_test.go
+++ b/dexpreopt/dexpreopt_test.go
@@ -33,17 +33,35 @@
 }
 
 func testModuleConfig(ctx android.PathContext, name, partition string) *ModuleConfig {
+	return createTestModuleConfig(
+		name,
+		fmt.Sprintf("/%s/app/test/%s.apk", partition, name),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/%s.apk", name, name)),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/dex/%s.jar", name, name)),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/enforce_uses_libraries.status", name)))
+}
+
+func testApexModuleConfig(ctx android.PathContext, name, apexName string) *ModuleConfig {
+	return createTestModuleConfig(
+		name,
+		fmt.Sprintf("/apex/%s/javalib/%s.jar", apexName, name),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/dexpreopt/%s.jar", name, name)),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/aligned/%s.jar", name, name)),
+		android.PathForOutput(ctx, fmt.Sprintf("%s/enforce_uses_libraries.status", name)))
+}
+
+func createTestModuleConfig(name, dexLocation string, buildPath, dexPath, enforceUsesLibrariesStatusFile android.OutputPath) *ModuleConfig {
 	return &ModuleConfig{
 		Name:                            name,
-		DexLocation:                     fmt.Sprintf("/%s/app/test/%s.apk", partition, name),
-		BuildPath:                       android.PathForOutput(ctx, fmt.Sprintf("%s/%s.apk", name, name)),
-		DexPath:                         android.PathForOutput(ctx, fmt.Sprintf("%s/dex/%s.jar", name, name)),
+		DexLocation:                     dexLocation,
+		BuildPath:                       buildPath,
+		DexPath:                         dexPath,
 		UncompressedDex:                 false,
 		HasApkLibraries:                 false,
 		PreoptFlags:                     nil,
 		ProfileClassListing:             android.OptionalPath{},
 		ProfileIsTextListing:            false,
-		EnforceUsesLibrariesStatusFile:  android.PathForOutput(ctx, fmt.Sprintf("%s/enforce_uses_libraries.status", name)),
+		EnforceUsesLibrariesStatusFile:  enforceUsesLibrariesStatusFile,
 		EnforceUsesLibraries:            false,
 		ClassLoaderContexts:             nil,
 		Archs:                           []android.ArchType{android.Arm},
@@ -140,6 +158,29 @@
 
 }
 
+func TestDexPreoptApexSystemServerJars(t *testing.T) {
+	config := android.TestConfig("out", nil, "", nil)
+	ctx := android.BuilderContextForTesting(config)
+	globalSoong := globalSoongConfigForTests()
+	global := GlobalConfigForTests(ctx)
+	module := testApexModuleConfig(ctx, "service-A", "com.android.apex1")
+
+	global.ApexSystemServerJars = android.CreateTestConfiguredJarList(
+		[]string{"com.android.apex1:service-A"})
+
+	rule, err := GenerateDexpreoptRule(ctx, globalSoong, global, module)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	wantInstalls := android.RuleBuilderInstalls{
+		{android.PathForOutput(ctx, "service-A/dexpreopt/oat/arm/javalib.odex"), "/system/framework/oat/arm/apex@com.android.apex1@javalib@service-A.jar@classes.odex"},
+		{android.PathForOutput(ctx, "service-A/dexpreopt/oat/arm/javalib.vdex"), "/system/framework/oat/arm/apex@com.android.apex1@javalib@service-A.jar@classes.vdex"},
+	}
+
+	android.AssertStringEquals(t, "installs", wantInstalls.String(), rule.Installs().String())
+}
+
 func TestDexPreoptProfile(t *testing.T) {
 	config := android.TestConfig("out", nil, "", nil)
 	ctx := android.BuilderContextForTesting(config)
diff --git a/java/androidmk.go b/java/androidmk.go
index 68ccd82..71370c9 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -61,7 +61,13 @@
 	var entriesList []android.AndroidMkEntries
 
 	if library.hideApexVariantFromMake {
-		// For a java library built for an APEX we don't need Make module
+		// For a java library built for an APEX, we don't need a Make module for itself. Otherwise, it
+		// will conflict with the platform variant because they have the same module name in the
+		// makefile. However, we need to add its dexpreopt outputs as sub-modules, if it is preopted.
+		dexpreoptEntries := library.dexpreopter.AndroidMkEntriesForApex()
+		if len(dexpreoptEntries) > 0 {
+			entriesList = append(entriesList, dexpreoptEntries...)
+		}
 		entriesList = append(entriesList, android.AndroidMkEntries{Disabled: true})
 	} else if !library.ApexModuleBase.AvailableFor(android.AvailableToPlatform) {
 		// Platform variant.  If not available for the platform, we don't need Make module.
diff --git a/java/dexpreopt.go b/java/dexpreopt.go
index 0faae36..e9dc982 100644
--- a/java/dexpreopt.go
+++ b/java/dexpreopt.go
@@ -15,13 +15,46 @@
 package java
 
 import (
+	"path/filepath"
+	"strings"
+
 	"android/soong/android"
 	"android/soong/dexpreopt"
 )
 
-type dexpreopterInterface interface {
+type DexpreopterInterface interface {
 	IsInstallable() bool // Structs that embed dexpreopter must implement this.
 	dexpreoptDisabled(ctx android.BaseModuleContext) bool
+	DexpreoptBuiltInstalledForApex() []dexpreopterInstall
+	AndroidMkEntriesForApex() []android.AndroidMkEntries
+}
+
+type dexpreopterInstall struct {
+	// A unique name to distinguish an output from others for the same java library module. Usually in
+	// the form of `<arch>-<encoded-path>.odex/vdex/art`.
+	name string
+
+	// The name of the input java module.
+	moduleName string
+
+	// The path to the dexpreopt output on host.
+	outputPathOnHost android.Path
+
+	// The directory on the device for the output to install to.
+	installDirOnDevice android.InstallPath
+
+	// The basename (the last segment of the path) for the output to install as.
+	installFileOnDevice string
+}
+
+// The full module name of the output in the makefile.
+func (install *dexpreopterInstall) FullModuleName() string {
+	return install.moduleName + install.SubModuleName()
+}
+
+// The sub-module name of the output in the makefile (the name excluding the java module name).
+func (install *dexpreopterInstall) SubModuleName() string {
+	return "-dexpreopt-" + install.name
 }
 
 type dexpreopter struct {
@@ -39,7 +72,9 @@
 	enforceUsesLibs     bool
 	classLoaderContexts dexpreopt.ClassLoaderContextMap
 
-	builtInstalled string
+	// See the `dexpreopt` function for details.
+	builtInstalled        string
+	builtInstalledForApex []dexpreopterInstall
 
 	// The config is used for two purposes:
 	// - Passing dexpreopt information about libraries from Soong to Make. This is needed when
@@ -74,6 +109,17 @@
 	dexpreopt.DexpreoptRunningInSoong = true
 }
 
+func isApexVariant(ctx android.BaseModuleContext) bool {
+	apexInfo := ctx.Provider(android.ApexInfoProvider).(android.ApexInfo)
+	return !apexInfo.IsForPlatform()
+}
+
+func moduleName(ctx android.BaseModuleContext) string {
+	// Remove the "prebuilt_" prefix if the module is from a prebuilt because the prefix is not
+	// expected by dexpreopter.
+	return android.RemoveOptionalPrebuiltPrefix(ctx.ModuleName())
+}
+
 func (d *dexpreopter) dexpreoptDisabled(ctx android.BaseModuleContext) bool {
 	global := dexpreopt.GetGlobalConfig(ctx)
 
@@ -81,7 +127,7 @@
 		return true
 	}
 
-	if inList(ctx.ModuleName(), global.DisablePreoptModules) {
+	if inList(moduleName(ctx), global.DisablePreoptModules) {
 		return true
 	}
 
@@ -93,7 +139,7 @@
 		return true
 	}
 
-	if !ctx.Module().(dexpreopterInterface).IsInstallable() {
+	if !ctx.Module().(DexpreopterInterface).IsInstallable() {
 		return true
 	}
 
@@ -101,8 +147,20 @@
 		return true
 	}
 
-	// Don't preopt APEX variant module
-	if apexInfo := ctx.Provider(android.ApexInfoProvider).(android.ApexInfo); !apexInfo.IsForPlatform() {
+	if isApexVariant(ctx) {
+		// Don't preopt APEX variant module unless the module is an APEX system server jar and we are
+		// building the entire system image.
+		if !global.ApexSystemServerJars.ContainsJar(moduleName(ctx)) || ctx.Config().UnbundledBuild() {
+			return true
+		}
+	} else {
+		// Don't preopt the platform variant of an APEX system server jar to avoid conflicts.
+		if global.ApexSystemServerJars.ContainsJar(moduleName(ctx)) {
+			return true
+		}
+	}
+
+	if !android.IsModulePreferred(ctx.Module()) {
 		return true
 	}
 
@@ -112,17 +170,40 @@
 }
 
 func dexpreoptToolDepsMutator(ctx android.BottomUpMutatorContext) {
-	if d, ok := ctx.Module().(dexpreopterInterface); !ok || d.dexpreoptDisabled(ctx) {
+	if d, ok := ctx.Module().(DexpreopterInterface); !ok || d.dexpreoptDisabled(ctx) {
 		return
 	}
 	dexpreopt.RegisterToolDeps(ctx)
 }
 
-func odexOnSystemOther(ctx android.ModuleContext, installPath android.InstallPath) bool {
-	return dexpreopt.OdexOnSystemOtherByName(ctx.ModuleName(), android.InstallPathToOnDevicePath(ctx, installPath), dexpreopt.GetGlobalConfig(ctx))
+func (d *dexpreopter) odexOnSystemOther(ctx android.ModuleContext, installPath android.InstallPath) bool {
+	return dexpreopt.OdexOnSystemOtherByName(moduleName(ctx), android.InstallPathToOnDevicePath(ctx, installPath), dexpreopt.GetGlobalConfig(ctx))
+}
+
+// Returns the install path of the dex jar of a module.
+//
+// Do not rely on `ApexInfo.ApexVariationName` because it can be something like "apex1000", rather
+// than the `name` in the path `/apex/<name>` as suggested in its comment.
+//
+// This function is on a best-effort basis. It cannot handle the case where an APEX jar is not a
+// system server jar, which is fine because we currently only preopt system server jars for APEXes.
+func (d *dexpreopter) getInstallPath(
+	ctx android.ModuleContext, defaultInstallPath android.InstallPath) android.InstallPath {
+	global := dexpreopt.GetGlobalConfig(ctx)
+	if global.ApexSystemServerJars.ContainsJar(moduleName(ctx)) {
+		dexLocation := dexpreopt.GetSystemServerDexLocation(global, moduleName(ctx))
+		return android.PathForModuleInPartitionInstall(ctx, "", strings.TrimPrefix(dexLocation, "/"))
+	}
+	if !d.dexpreoptDisabled(ctx) && isApexVariant(ctx) &&
+		filepath.Base(defaultInstallPath.PartitionDir()) != "apex" {
+		ctx.ModuleErrorf("unable to get the install path of the dex jar for dexpreopt")
+	}
+	return defaultInstallPath
 }
 
 func (d *dexpreopter) dexpreopt(ctx android.ModuleContext, dexJarFile android.WritablePath) {
+	global := dexpreopt.GetGlobalConfig(ctx)
+
 	// TODO(b/148690468): The check on d.installPath is to bail out in cases where
 	// the dexpreopter struct hasn't been fully initialized before we're called,
 	// e.g. in aar.go. This keeps the behaviour that dexpreopting is effectively
@@ -133,7 +214,7 @@
 
 	dexLocation := android.InstallPathToOnDevicePath(ctx, d.installPath)
 
-	providesUsesLib := ctx.ModuleName()
+	providesUsesLib := moduleName(ctx)
 	if ulib, ok := ctx.Module().(ProvidesUsesLib); ok {
 		name := ulib.ProvidesUsesLib()
 		if name != nil {
@@ -147,17 +228,15 @@
 		return
 	}
 
-	global := dexpreopt.GetGlobalConfig(ctx)
-
-	isSystemServerJar := global.SystemServerJars.ContainsJar(ctx.ModuleName())
+	isSystemServerJar := global.SystemServerJars.ContainsJar(moduleName(ctx)) ||
+		global.ApexSystemServerJars.ContainsJar(moduleName(ctx))
 
 	bootImage := defaultBootImageConfig(ctx)
 	if global.UseArtImage {
 		bootImage = artBootImageConfig(ctx)
 	}
 
-	// System server jars are an exception: they are dexpreopted without updatable bootclasspath.
-	dexFiles, dexLocations := bcpForDexpreopt(ctx, global.PreoptWithUpdatableBcp && !isSystemServerJar)
+	dexFiles, dexLocations := bcpForDexpreopt(ctx, global.PreoptWithUpdatableBcp)
 
 	targets := ctx.MultiTargets()
 	if len(targets) == 0 {
@@ -199,15 +278,15 @@
 			profileIsTextListing = true
 		} else if global.ProfileDir != "" {
 			profileClassListing = android.ExistentPathForSource(ctx,
-				global.ProfileDir, ctx.ModuleName()+".prof")
+				global.ProfileDir, moduleName(ctx)+".prof")
 		}
 	}
 
 	// Full dexpreopt config, used to create dexpreopt build rules.
 	dexpreoptConfig := &dexpreopt.ModuleConfig{
-		Name:            ctx.ModuleName(),
+		Name:            moduleName(ctx),
 		DexLocation:     dexLocation,
-		BuildPath:       android.PathForModuleOut(ctx, "dexpreopt", ctx.ModuleName()+".jar").OutputPath,
+		BuildPath:       android.PathForModuleOut(ctx, "dexpreopt", moduleName(ctx)+".jar").OutputPath,
 		DexPath:         dexJarFile,
 		ManifestPath:    android.OptionalPathForPath(d.manifestFile),
 		UncompressedDex: d.uncompressedDex,
@@ -256,5 +335,53 @@
 
 	dexpreoptRule.Build("dexpreopt", "dexpreopt")
 
-	d.builtInstalled = dexpreoptRule.Installs().String()
+	if global.ApexSystemServerJars.ContainsJar(moduleName(ctx)) {
+		// APEX variants of java libraries are hidden from Make, so their dexpreopt outputs need special
+		// handling. Currently, for APEX variants of java libraries, only those in the system server
+		// classpath are handled here. Preopting of boot classpath jars in the ART APEX are handled in
+		// java/dexpreopt_bootjars.go, and other APEX jars are not preopted.
+		for _, install := range dexpreoptRule.Installs() {
+			// Remove the "/" prefix because the path should be relative to $ANDROID_PRODUCT_OUT.
+			installDir := strings.TrimPrefix(filepath.Dir(install.To), "/")
+			installBase := filepath.Base(install.To)
+			arch := filepath.Base(installDir)
+			installPath := android.PathForModuleInPartitionInstall(ctx, "", installDir)
+			// The installs will be handled by Make as sub-modules of the java library.
+			d.builtInstalledForApex = append(d.builtInstalledForApex, dexpreopterInstall{
+				name:                arch + "-" + installBase,
+				moduleName:          moduleName(ctx),
+				outputPathOnHost:    install.From,
+				installDirOnDevice:  installPath,
+				installFileOnDevice: installBase,
+			})
+		}
+	} else {
+		// The installs will be handled by Make as LOCAL_SOONG_BUILT_INSTALLED of the java library
+		// module.
+		d.builtInstalled = dexpreoptRule.Installs().String()
+	}
+}
+
+func (d *dexpreopter) DexpreoptBuiltInstalledForApex() []dexpreopterInstall {
+	return d.builtInstalledForApex
+}
+
+func (d *dexpreopter) AndroidMkEntriesForApex() []android.AndroidMkEntries {
+	var entries []android.AndroidMkEntries
+	for _, install := range d.builtInstalledForApex {
+		install := install
+		entries = append(entries, android.AndroidMkEntries{
+			Class:      "ETC",
+			SubName:    install.SubModuleName(),
+			OutputFile: android.OptionalPathForPath(install.outputPathOnHost),
+			ExtraEntries: []android.AndroidMkExtraEntriesFunc{
+				func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
+					entries.SetString("LOCAL_MODULE_PATH", install.installDirOnDevice.ToMakePath().String())
+					entries.SetString("LOCAL_INSTALLED_MODULE_STEM", install.installFileOnDevice)
+					entries.SetString("LOCAL_NOT_AVAILABLE_FOR_PLATFORM", "false")
+				},
+			},
+		})
+	}
+	return entries
 }
diff --git a/java/dexpreopt_test.go b/java/dexpreopt_test.go
index 8dc7b79..1c1070a 100644
--- a/java/dexpreopt_test.go
+++ b/java/dexpreopt_test.go
@@ -17,6 +17,7 @@
 import (
 	"fmt"
 	"runtime"
+	"strings"
 	"testing"
 
 	"android/soong/android"
@@ -24,11 +25,17 @@
 	"android/soong/dexpreopt"
 )
 
+func init() {
+	RegisterFakeRuntimeApexMutator()
+}
+
 func TestDexpreoptEnabled(t *testing.T) {
 	tests := []struct {
-		name    string
-		bp      string
-		enabled bool
+		name        string
+		bp          string
+		moduleName  string
+		apexVariant bool
+		enabled     bool
 	}{
 		{
 			name: "app",
@@ -148,13 +155,81 @@
 				}`,
 			enabled: true,
 		},
+		{
+			name: "apex variant",
+			bp: `
+				java_library {
+					name: "foo",
+					installable: true,
+					srcs: ["a.java"],
+					apex_available: ["com.android.apex1"],
+				}`,
+			apexVariant: true,
+			enabled:     false,
+		},
+		{
+			name: "apex variant of apex system server jar",
+			bp: `
+				java_library {
+					name: "service-foo",
+					installable: true,
+					srcs: ["a.java"],
+					apex_available: ["com.android.apex1"],
+				}`,
+			moduleName:  "service-foo",
+			apexVariant: true,
+			enabled:     true,
+		},
+		{
+			name: "apex variant of prebuilt apex system server jar",
+			bp: `
+				java_library {
+					name: "prebuilt_service-foo",
+					installable: true,
+					srcs: ["a.java"],
+					apex_available: ["com.android.apex1"],
+				}`,
+			moduleName:  "prebuilt_service-foo",
+			apexVariant: true,
+			enabled:     true,
+		},
+		{
+			name: "platform variant of apex system server jar",
+			bp: `
+				java_library {
+					name: "service-foo",
+					installable: true,
+					srcs: ["a.java"],
+					apex_available: ["com.android.apex1"],
+				}`,
+			moduleName:  "service-foo",
+			apexVariant: false,
+			enabled:     false,
+		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			ctx, _ := testJava(t, test.bp)
+			preparers := android.GroupFixturePreparers(
+				PrepareForTestWithJavaDefaultModules,
+				PrepareForTestWithFakeApexMutator,
+				dexpreopt.FixtureSetApexSystemServerJars("com.android.apex1:service-foo"),
+			)
 
-			dexpreopt := ctx.ModuleForTests("foo", "android_common").MaybeRule("dexpreopt")
+			result := preparers.RunTestWithBp(t, test.bp)
+			ctx := result.TestContext
+
+			moduleName := "foo"
+			if test.moduleName != "" {
+				moduleName = test.moduleName
+			}
+
+			variant := "android_common"
+			if test.apexVariant {
+				variant += "_apex1000"
+			}
+
+			dexpreopt := ctx.ModuleForTests(moduleName, variant).MaybeRule("dexpreopt")
 			enabled := dexpreopt.Rule != nil
 
 			if enabled != test.enabled {
@@ -220,3 +295,145 @@
 	testDex2oatToolDep(true, true, true, prebuiltDex2oatPath)
 	testDex2oatToolDep(false, true, false, prebuiltDex2oatPath)
 }
+
+func TestDexpreoptBuiltInstalledForApex(t *testing.T) {
+	preparers := android.GroupFixturePreparers(
+		PrepareForTestWithJavaDefaultModules,
+		PrepareForTestWithFakeApexMutator,
+		dexpreopt.FixtureSetApexSystemServerJars("com.android.apex1:service-foo"),
+	)
+
+	// An APEX system server jar.
+	result := preparers.RunTestWithBp(t, `
+		java_library {
+			name: "service-foo",
+			installable: true,
+			srcs: ["a.java"],
+			apex_available: ["com.android.apex1"],
+		}`)
+	ctx := result.TestContext
+	module := ctx.ModuleForTests("service-foo", "android_common_apex1000")
+	library := module.Module().(*Library)
+
+	installs := library.dexpreopter.DexpreoptBuiltInstalledForApex()
+
+	android.AssertIntEquals(t, "install count", 2, len(installs))
+
+	android.AssertStringEquals(t, "installs[0] FullModuleName",
+		"service-foo-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.odex",
+		installs[0].FullModuleName())
+
+	android.AssertStringEquals(t, "installs[0] SubModuleName",
+		"-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.odex",
+		installs[0].SubModuleName())
+
+	android.AssertStringEquals(t, "installs[1] FullModuleName",
+		"service-foo-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.vdex",
+		installs[1].FullModuleName())
+
+	android.AssertStringEquals(t, "installs[1] SubModuleName",
+		"-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.vdex",
+		installs[1].SubModuleName())
+
+	// Not an APEX system server jar.
+	result = preparers.RunTestWithBp(t, `
+		java_library {
+			name: "foo",
+			installable: true,
+			srcs: ["a.java"],
+		}`)
+	ctx = result.TestContext
+	module = ctx.ModuleForTests("foo", "android_common")
+	library = module.Module().(*Library)
+
+	installs = library.dexpreopter.DexpreoptBuiltInstalledForApex()
+
+	android.AssertIntEquals(t, "install count", 0, len(installs))
+}
+
+func filterDexpreoptEntriesList(entriesList []android.AndroidMkEntries) []android.AndroidMkEntries {
+	var results []android.AndroidMkEntries
+	for _, entries := range entriesList {
+		if strings.Contains(entries.EntryMap["LOCAL_MODULE"][0], "-dexpreopt-") {
+			results = append(results, entries)
+		}
+	}
+	return results
+}
+
+func verifyEntries(t *testing.T, message string, expectedModule string,
+	expectedPrebuiltModuleFile string, expectedModulePath string, expectedInstalledModuleStem string,
+	entries android.AndroidMkEntries) {
+	android.AssertStringEquals(t, message+" LOCAL_MODULE", expectedModule,
+		entries.EntryMap["LOCAL_MODULE"][0])
+
+	android.AssertStringEquals(t, message+" LOCAL_MODULE_CLASS", "ETC",
+		entries.EntryMap["LOCAL_MODULE_CLASS"][0])
+
+	android.AssertStringDoesContain(t, message+" LOCAL_PREBUILT_MODULE_FILE",
+		entries.EntryMap["LOCAL_PREBUILT_MODULE_FILE"][0], expectedPrebuiltModuleFile)
+
+	android.AssertStringDoesContain(t, message+" LOCAL_MODULE_PATH",
+		entries.EntryMap["LOCAL_MODULE_PATH"][0], expectedModulePath)
+
+	android.AssertStringEquals(t, message+" LOCAL_INSTALLED_MODULE_STEM",
+		expectedInstalledModuleStem, entries.EntryMap["LOCAL_INSTALLED_MODULE_STEM"][0])
+
+	android.AssertStringEquals(t, message+" LOCAL_NOT_AVAILABLE_FOR_PLATFORM",
+		"false", entries.EntryMap["LOCAL_NOT_AVAILABLE_FOR_PLATFORM"][0])
+}
+
+func TestAndroidMkEntriesForApex(t *testing.T) {
+	preparers := android.GroupFixturePreparers(
+		PrepareForTestWithJavaDefaultModules,
+		PrepareForTestWithFakeApexMutator,
+		dexpreopt.FixtureSetApexSystemServerJars("com.android.apex1:service-foo"),
+	)
+
+	// An APEX system server jar.
+	result := preparers.RunTestWithBp(t, `
+		java_library {
+			name: "service-foo",
+			installable: true,
+			srcs: ["a.java"],
+			apex_available: ["com.android.apex1"],
+		}`)
+	ctx := result.TestContext
+	module := ctx.ModuleForTests("service-foo", "android_common_apex1000")
+
+	entriesList := android.AndroidMkEntriesForTest(t, ctx, module.Module())
+	entriesList = filterDexpreoptEntriesList(entriesList)
+
+	android.AssertIntEquals(t, "entries count", 2, len(entriesList))
+
+	verifyEntries(t,
+		"entriesList[0]",
+		"service-foo-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.odex",
+		"/dexpreopt/oat/arm64/javalib.odex",
+		"/system/framework/oat/arm64",
+		"apex@com.android.apex1@javalib@service-foo.jar@classes.odex",
+		entriesList[0])
+
+	verifyEntries(t,
+		"entriesList[1]",
+		"service-foo-dexpreopt-arm64-apex@com.android.apex1@javalib@service-foo.jar@classes.vdex",
+		"/dexpreopt/oat/arm64/javalib.vdex",
+		"/system/framework/oat/arm64",
+		"apex@com.android.apex1@javalib@service-foo.jar@classes.vdex",
+		entriesList[1])
+
+	// Not an APEX system server jar.
+	result = preparers.RunTestWithBp(t, `
+		java_library {
+			name: "foo",
+			installable: true,
+			srcs: ["a.java"],
+		}`)
+	ctx = result.TestContext
+	module = ctx.ModuleForTests("foo", "android_common")
+
+	entriesList = android.AndroidMkEntriesForTest(t, ctx, module.Module())
+	entriesList = filterDexpreoptEntriesList(entriesList)
+
+	android.AssertIntEquals(t, "entries count", 0, len(entriesList))
+}
diff --git a/java/java.go b/java/java.go
index 1a052b4..e2665ef 100644
--- a/java/java.go
+++ b/java/java.go
@@ -487,7 +487,7 @@
 	}
 
 	// Store uncompressed dex files that are preopted on /system.
-	if !dexpreopter.dexpreoptDisabled(ctx) && (ctx.Host() || !odexOnSystemOther(ctx, dexpreopter.installPath)) {
+	if !dexpreopter.dexpreoptDisabled(ctx) && (ctx.Host() || !dexpreopter.odexOnSystemOther(ctx, dexpreopter.installPath)) {
 		return true
 	}
 	if ctx.Config().UncompressPrivAppDex() &&
@@ -508,7 +508,8 @@
 	}
 
 	j.checkSdkVersions(ctx)
-	j.dexpreopter.installPath = android.PathForModuleInstall(ctx, "framework", j.Stem()+".jar")
+	j.dexpreopter.installPath = j.dexpreopter.getInstallPath(
+		ctx, android.PathForModuleInstall(ctx, "framework", j.Stem()+".jar"))
 	j.dexpreopter.isSDKLibrary = j.deviceProperties.IsSDKLibrary
 	if j.dexProperties.Uncompress_dex == nil {
 		// If the value was not force-set by the user, use reasonable default based on the module.
@@ -1368,7 +1369,8 @@
 
 			// Dex compilation
 
-			j.dexpreopter.installPath = android.PathForModuleInstall(ctx, "framework", jarName)
+			j.dexpreopter.installPath = j.dexpreopter.getInstallPath(
+				ctx, android.PathForModuleInstall(ctx, "framework", jarName))
 			if j.dexProperties.Uncompress_dex == nil {
 				// If the value was not force-set by the user, use reasonable default based on the module.
 				j.dexProperties.Uncompress_dex = proptools.BoolPtr(shouldUncompressDex(ctx, &j.dexpreopter))
@@ -1509,7 +1511,7 @@
 	return Bool(j.properties.Installable)
 }
 
-var _ dexpreopterInterface = (*Import)(nil)
+var _ DexpreopterInterface = (*Import)(nil)
 
 // java_import imports one or more `.jar` files into the build graph as if they were built by a java_library module.
 //
@@ -1622,7 +1624,8 @@
 		j.hideApexVariantFromMake = true
 	}
 
-	j.dexpreopter.installPath = android.PathForModuleInstall(ctx, "framework", j.Stem()+".jar")
+	j.dexpreopter.installPath = j.dexpreopter.getInstallPath(
+		ctx, android.PathForModuleInstall(ctx, "framework", j.Stem()+".jar"))
 	j.dexpreopter.uncompressedDex = shouldUncompressDex(ctx, &j.dexpreopter)
 
 	inputJar := ctx.ExpandSource(j.properties.Jars[0], "jars")
diff --git a/java/sdk_library.go b/java/sdk_library.go
index ce8f179..1d39071 100644
--- a/java/sdk_library.go
+++ b/java/sdk_library.go
@@ -1907,6 +1907,7 @@
 	android.SdkBase
 
 	hiddenAPI
+	dexpreopter
 
 	properties sdkLibraryImportProperties
 
@@ -2111,6 +2112,14 @@
 	}
 }
 
+func (module *SdkLibraryImport) AndroidMkEntries() []android.AndroidMkEntries {
+	// For an SDK library imported from a prebuilt APEX, we don't need a Make module for itself, as we
+	// don't need to install it. However, we need to add its dexpreopt outputs as sub-modules, if it
+	// is preopted.
+	dexpreoptEntries := module.dexpreopter.AndroidMkEntriesForApex()
+	return append(dexpreoptEntries, android.AndroidMkEntries{Disabled: true})
+}
+
 var _ android.ApexModule = (*SdkLibraryImport)(nil)
 
 // Implements android.ApexModule
@@ -2208,8 +2217,16 @@
 			di := ctx.OtherModuleProvider(deapexerModule, android.DeapexerProvider).(android.DeapexerInfo)
 			if dexOutputPath := di.PrebuiltExportPath(apexRootRelativePathToJavaLib(module.BaseModuleName())); dexOutputPath != nil {
 				module.dexJarFile = dexOutputPath
-				module.installFile = android.PathForModuleInPartitionInstall(ctx, "apex", ai.ApexVariationName, apexRootRelativePathToJavaLib(module.BaseModuleName()))
+				installPath := android.PathForModuleInPartitionInstall(
+					ctx, "apex", ai.ApexVariationName, apexRootRelativePathToJavaLib(module.BaseModuleName()))
+				module.installFile = installPath
 				module.initHiddenAPI(ctx, dexOutputPath, module.findScopePaths(apiScopePublic).stubsImplPath[0], nil)
+
+				// Dexpreopting.
+				module.dexpreopter.installPath = module.dexpreopter.getInstallPath(ctx, installPath)
+				module.dexpreopter.isSDKLibrary = true
+				module.dexpreopter.uncompressedDex = shouldUncompressDex(ctx, &module.dexpreopter)
+				module.dexpreopt(ctx, dexOutputPath)
 			} else {
 				// This should never happen as a variant for a prebuilt_apex is only created if the
 				// prebuilt_apex has been configured to export the java library dex file.
@@ -2328,6 +2345,11 @@
 	}
 }
 
+// to satisfy java.DexpreopterInterface interface
+func (module *SdkLibraryImport) IsInstallable() bool {
+	return true
+}
+
 var _ android.RequiredFilesFromPrebuiltApex = (*SdkLibraryImport)(nil)
 
 func (module *SdkLibraryImport) RequiredFilesFromPrebuiltApex(ctx android.BaseModuleContext) []string {
diff --git a/java/sdk_library_test.go b/java/sdk_library_test.go
index 938bb28..d6c0946 100644
--- a/java/sdk_library_test.go
+++ b/java/sdk_library_test.go
@@ -598,6 +598,7 @@
 	}
 
 	CheckModuleDependencies(t, result.TestContext, "sdklib", "android_common", []string{
+		`dex2oatd`,
 		`prebuilt_sdklib.stubs`,
 		`prebuilt_sdklib.stubs.source.test`,
 		`prebuilt_sdklib.stubs.system`,
@@ -674,7 +675,6 @@
 		`)
 
 	CheckModuleDependencies(t, result.TestContext, "sdklib", "android_common", []string{
-		`dex2oatd`,
 		`prebuilt_sdklib`,
 		`sdklib.impl`,
 		`sdklib.stubs`,
@@ -683,6 +683,7 @@
 	})
 
 	CheckModuleDependencies(t, result.TestContext, "prebuilt_sdklib", "android_common", []string{
+		`dex2oatd`,
 		`prebuilt_sdklib.stubs`,
 		`sdklib.impl`,
 		`sdklib.xml`,
diff --git a/java/testing.go b/java/testing.go
index 8860b45..d8a77cf 100644
--- a/java/testing.go
+++ b/java/testing.go
@@ -431,3 +431,45 @@
 	output := sourceGlobalCompatConfig.Output(allOutputs[0])
 	android.AssertPathsRelativeToTopEquals(t, message+": inputs", expectedPaths, output.Implicits)
 }
+
+// Register the fake APEX mutator to `android.InitRegistrationContext` as if the real mutator exists
+// at runtime. This must be called in `init()` of a test if the test is going to use the fake APEX
+// mutator. Otherwise, we will be missing the runtime mutator because "soong-apex" is not a
+// dependency, which will cause an inconsistency between testing and runtime mutators.
+func RegisterFakeRuntimeApexMutator() {
+	registerFakeApexMutator(android.InitRegistrationContext)
+}
+
+var PrepareForTestWithFakeApexMutator = android.GroupFixturePreparers(
+	android.FixtureRegisterWithContext(registerFakeApexMutator),
+)
+
+func registerFakeApexMutator(ctx android.RegistrationContext) {
+	ctx.PostDepsMutators(func(ctx android.RegisterMutatorsContext) {
+		ctx.BottomUp("apex", fakeApexMutator).Parallel()
+	})
+}
+
+type apexModuleBase interface {
+	ApexAvailable() []string
+}
+
+var _ apexModuleBase = (*Library)(nil)
+var _ apexModuleBase = (*SdkLibrary)(nil)
+
+// A fake APEX mutator that creates a platform variant and an APEX variant for modules with
+// `apex_available`. It helps us avoid a dependency on the real mutator defined in "soong-apex",
+// which will cause a cyclic dependency, and it provides an easy way to create an APEX variant for
+// testing without dealing with all the complexities in the real mutator.
+func fakeApexMutator(mctx android.BottomUpMutatorContext) {
+	switch mctx.Module().(type) {
+	case *Library, *SdkLibrary:
+		if len(mctx.Module().(apexModuleBase).ApexAvailable()) > 0 {
+			modules := mctx.CreateVariations("", "apex1000")
+			apexInfo := android.ApexInfo{
+				ApexVariationName: "apex1000",
+			}
+			mctx.SetVariationProvider(modules[1], android.ApexInfoProvider, apexInfo)
+		}
+	}
+}
diff --git a/rust/config/allowed_list.go b/rust/config/allowed_list.go
index 63a8f04..47ca3a7 100644
--- a/rust/config/allowed_list.go
+++ b/rust/config/allowed_list.go
@@ -28,6 +28,7 @@
 		"system/librustutils",
 		"system/logging/liblog",
 		"system/logging/rust",
+		"system/nfc",
 		"system/security",
 		"system/tools/aidl",
 		"tools/security/fuzzing/example_rust_fuzzer",