Support secondary dex compilation.

Bug: 249984283
Test: atest ArtServiceTests
Test: m test-art-host-gtest-art_artd_tests
Test: m test-art-host-gtest-art_runtime_tests
Test: adb shell pm art optimize-package --secondary-dex -m speed-profile -f com.google.android.gms
Ignore-AOSP-First: ART Services.
Change-Id: I7ebe2aa745d0da31242034a27f92b24dbdb08740
diff --git a/artd/artd.cc b/artd/artd.cc
index ce42a86..32c89c3 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -228,7 +228,7 @@
     if (se_context.has_value()) {
       res = selinux_android_restorecon_pkgdir(path.c_str(),
                                               se_context->seInfo.c_str(),
-                                              se_context->packageUid,
+                                              se_context->uid,
                                               SELINUX_ANDROID_RESTORECON_RECURSE);
     } else {
       res = selinux_android_restorecon(path.c_str(), SELINUX_ANDROID_RESTORECON_RECURSE);
@@ -543,6 +543,13 @@
   return ScopedAStatus::ok();
 }
 
+ndk::ScopedAStatus Artd::getDexFileVisibility(const std::string& in_dexFile,
+                                              FileVisibility* _aidl_return) {
+  OR_RETURN_FATAL(ValidateDexPath(in_dexFile));
+  *_aidl_return = OR_RETURN_NON_FATAL(GetFileVisibility(in_dexFile));
+  return ScopedAStatus::ok();
+}
+
 ndk::ScopedAStatus Artd::mergeProfiles(const std::vector<ProfilePath>& in_profiles,
                                        const std::optional<ProfilePath>& in_referenceProfile,
                                        OutputProfile* in_outputProfile,
@@ -642,7 +649,7 @@
 
 ndk::ScopedAStatus Artd::getDexoptNeeded(const std::string& in_dexFile,
                                          const std::string& in_instructionSet,
-                                         const std::string& in_classLoaderContext,
+                                         const std::optional<std::string>& in_classLoaderContext,
                                          const std::string& in_compilerFilter,
                                          int32_t in_dexoptTrigger,
                                          GetDexoptNeededResult* _aidl_return) {
@@ -653,9 +660,9 @@
 
   std::unique_ptr<ClassLoaderContext> context;
   std::string error_msg;
-  auto oat_file_assistant = OatFileAssistant::Create(in_dexFile.c_str(),
-                                                     in_instructionSet.c_str(),
-                                                     in_classLoaderContext.c_str(),
+  auto oat_file_assistant = OatFileAssistant::Create(in_dexFile,
+                                                     in_instructionSet,
+                                                     in_classLoaderContext,
                                                      /*load_executable=*/false,
                                                      /*only_load_trusted_executable=*/true,
                                                      ofa_context.value(),
@@ -680,7 +687,7 @@
     const OutputArtifacts& in_outputArtifacts,
     const std::string& in_dexFile,
     const std::string& in_instructionSet,
-    const std::string& in_classLoaderContext,
+    const std::optional<std::string>& in_classLoaderContext,
     const std::string& in_compilerFilter,
     const std::optional<ProfilePath>& in_profile,
     const std::optional<VdexPath>& in_inputVdex,
@@ -701,10 +708,12 @@
   ArtdCancellationSignal* cancellation_signal =
       OR_RETURN_FATAL(ToArtdCancellationSignal(in_cancellationSignal.get()));
 
-  std::unique_ptr<ClassLoaderContext> context =
-      ClassLoaderContext::Create(in_classLoaderContext.c_str());
-  if (context == nullptr) {
-    return Fatal("Class loader context '{}' is invalid"_format(in_classLoaderContext));
+  std::unique_ptr<ClassLoaderContext> context = nullptr;
+  if (in_classLoaderContext.has_value()) {
+    context = ClassLoaderContext::Create(in_classLoaderContext->c_str());
+    if (context == nullptr) {
+      return Fatal("Class loader context '{}' is invalid"_format(in_classLoaderContext.value()));
+    }
   }
 
   OR_RETURN_NON_FATAL(PrepareArtifactsDirs(in_outputArtifacts));
@@ -754,21 +763,23 @@
   args.Add("--zip-fd=%d", dex_file->Fd()).Add("--zip-location=%s", in_dexFile);
   fd_logger.Add(*dex_file);
 
-  std::vector<std::string> flattened_context = context->FlattenDexPaths();
-  std::string dex_dir = Dirname(in_dexFile.c_str());
   std::vector<std::unique_ptr<File>> context_files;
-  std::vector<int> context_fds;
-  for (const std::string& context_element : flattened_context) {
-    std::string context_path = std::filesystem::path(dex_dir).append(context_element);
-    OR_RETURN_FATAL(ValidateDexPath(context_path));
-    std::unique_ptr<File> context_file = OR_RETURN_NON_FATAL(OpenFileForReading(context_path));
-    context_fds.push_back(context_file->Fd());
-    fd_logger.Add(*context_file);
-    context_files.push_back(std::move(context_file));
+  if (context != nullptr) {
+    std::vector<std::string> flattened_context = context->FlattenDexPaths();
+    std::string dex_dir = Dirname(in_dexFile.c_str());
+    std::vector<int> context_fds;
+    for (const std::string& context_element : flattened_context) {
+      std::string context_path = std::filesystem::path(dex_dir).append(context_element);
+      OR_RETURN_FATAL(ValidateDexPath(context_path));
+      std::unique_ptr<File> context_file = OR_RETURN_NON_FATAL(OpenFileForReading(context_path));
+      context_fds.push_back(context_file->Fd());
+      fd_logger.Add(*context_file);
+      context_files.push_back(std::move(context_file));
+    }
+    args.AddIfNonEmpty("--class-loader-context-fds=%s", Join(context_fds, /*separator=*/':'))
+        .Add("--class-loader-context=%s", in_classLoaderContext.value())
+        .Add("--classpath-dir=%s", dex_dir);
   }
-  args.Add("--class-loader-context-fds=%s", Join(context_fds, /*separator=*/':'))
-      .Add("--class-loader-context=%s", in_classLoaderContext)
-      .Add("--classpath-dir=%s", dex_dir);
 
   std::unique_ptr<File> input_vdex_file = nullptr;
   if (in_inputVdex.has_value()) {
diff --git a/artd/artd.h b/artd/artd.h
index c775c4a..1230ee8 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -24,6 +24,7 @@
 #include <functional>
 #include <memory>
 #include <mutex>
+#include <optional>
 #include <string>
 #include <unordered_map>
 #include <unordered_set>
@@ -115,10 +116,14 @@
       const aidl::com::android::server::art::ArtifactsPath& in_artifactsPath,
       aidl::com::android::server::art::FileVisibility* _aidl_return) override;
 
+  ndk::ScopedAStatus getDexFileVisibility(
+      const std::string& in_dexFile,
+      aidl::com::android::server::art::FileVisibility* _aidl_return) override;
+
   ndk::ScopedAStatus getDexoptNeeded(
       const std::string& in_dexFile,
       const std::string& in_instructionSet,
-      const std::string& in_classLoaderContext,
+      const std::optional<std::string>& in_classLoaderContext,
       const std::string& in_compilerFilter,
       int32_t in_dexoptTrigger,
       aidl::com::android::server::art::GetDexoptNeededResult* _aidl_return) override;
@@ -127,7 +132,7 @@
       const aidl::com::android::server::art::OutputArtifacts& in_outputArtifacts,
       const std::string& in_dexFile,
       const std::string& in_instructionSet,
-      const std::string& in_classLoaderContext,
+      const std::optional<std::string>& in_classLoaderContext,
       const std::string& in_compilerFilter,
       const std::optional<aidl::com::android::server::art::ProfilePath>& in_profile,
       const std::optional<aidl::com::android::server::art::VdexPath>& in_inputVdex,
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 76cba88..68d23db 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -357,7 +357,7 @@
   OutputArtifacts output_artifacts_;
   std::string clc_1_;
   std::string clc_2_;
-  std::string class_loader_context_;
+  std::optional<std::string> class_loader_context_;
   std::string compiler_filter_;
   std::optional<VdexPath> vdex_path_;
   PriorityClass priority_class_ = PriorityClass::BACKGROUND;
@@ -526,6 +526,22 @@
   RunDexopt();
 }
 
+TEST_F(ArtdTest, dexoptClassLoaderContextNull) {
+  class_loader_context_ = std::nullopt;
+
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy("--",
+                                      _,
+                                      AllOf(Not(Contains(Flag("--class-loader-context-fds=", _))),
+                                            Not(Contains(Flag("--class-loader-context=", _))),
+                                            Not(Contains(Flag("--classpath-dir=", _))))),
+                          _,
+                          _))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
 TEST_F(ArtdTest, dexoptNoInputVdex) {
   EXPECT_CALL(*mock_exec_utils_,
               DoExecAndReturnCode(WhenSplitBy("--",
@@ -1227,6 +1243,45 @@
   EXPECT_THAT(status.getMessage(), ContainsRegex(R"re(Failed to get status of .*b\.odex)re"));
 }
 
+TEST_F(ArtdTest, getDexFileVisibilityOtherReadable) {
+  CreateFile(dex_file_);
+  std::filesystem::permissions(
+      dex_file_, std::filesystem::perms::others_read, std::filesystem::perm_options::add);
+
+  FileVisibility result;
+  ASSERT_TRUE(artd_->getDexFileVisibility(dex_file_, &result).isOk());
+  EXPECT_EQ(result, FileVisibility::OTHER_READABLE);
+}
+
+TEST_F(ArtdTest, getDexFileVisibilityNotOtherReadable) {
+  CreateFile(dex_file_);
+  std::filesystem::permissions(
+      dex_file_, std::filesystem::perms::others_read, std::filesystem::perm_options::remove);
+
+  FileVisibility result;
+  ASSERT_TRUE(artd_->getDexFileVisibility(dex_file_, &result).isOk());
+  EXPECT_EQ(result, FileVisibility::NOT_OTHER_READABLE);
+}
+
+TEST_F(ArtdTest, getDexFileVisibilityNotFound) {
+  FileVisibility result;
+  ASSERT_TRUE(artd_->getDexFileVisibility(dex_file_, &result).isOk());
+  EXPECT_EQ(result, FileVisibility::NOT_FOUND);
+}
+
+TEST_F(ArtdTest, getDexFileVisibilityPermissionDenied) {
+  CreateFile(dex_file_);
+
+  auto scoped_inaccessible = ScopedInaccessible(std::filesystem::path(dex_file_).parent_path());
+  auto scoped_unroot = ScopedUnroot();
+
+  FileVisibility result;
+  ndk::ScopedAStatus status = artd_->getDexFileVisibility(dex_file_, &result);
+  EXPECT_FALSE(status.isOk());
+  EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
+  EXPECT_THAT(status.getMessage(), ContainsRegex(R"re(Failed to get status of .*/a/b\.apk)re"));
+}
+
 TEST_F(ArtdTest, mergeProfiles) {
   const TmpProfilePath& reference_profile_path = profile_path_->get<ProfilePath::tmpProfilePath>();
   std::string reference_profile_file = OR_FATAL(BuildTmpProfilePath(reference_profile_path));
diff --git a/artd/binder/com/android/server/art/FileVisibility.aidl b/artd/binder/com/android/server/art/FileVisibility.aidl
index c8d1455..ceaa818 100644
--- a/artd/binder/com/android/server/art/FileVisibility.aidl
+++ b/artd/binder/com/android/server/art/FileVisibility.aidl
@@ -26,6 +26,7 @@
  *
  * @hide
  */
+@Backing(type="int")
 enum FileVisibility {
     NOT_FOUND = 0,
     OTHER_READABLE = 1,
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 782334b..d575adf 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -103,6 +103,13 @@
             in com.android.server.art.ArtifactsPath artifactsPath);
 
     /**
+     * Returns the visibility of the dex file.
+     *
+     * Throws fatal and non-fatal errors.
+     */
+    com.android.server.art.FileVisibility getDexFileVisibility(@utf8InCpp String dexFile);
+
+    /**
      * Returns true if dexopt is needed. `dexoptTrigger` is a bit field that consists of values
      * defined in `com.android.server.art.DexoptTrigger`.
      *
@@ -110,7 +117,7 @@
      */
     com.android.server.art.GetDexoptNeededResult getDexoptNeeded(
             @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
-            @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
+            @nullable @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
             int dexoptTrigger);
 
     /**
@@ -121,7 +128,7 @@
     com.android.server.art.DexoptResult dexopt(
             in com.android.server.art.OutputArtifacts outputArtifacts,
             @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
-            @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
+            @nullable @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
             in @nullable com.android.server.art.ProfilePath profile,
             in @nullable com.android.server.art.VdexPath inputVdex,
             com.android.server.art.PriorityClass priorityClass,
diff --git a/artd/binder/com/android/server/art/OutputArtifacts.aidl b/artd/binder/com/android/server/art/OutputArtifacts.aidl
index 20ed475..af3e3ce 100644
--- a/artd/binder/com/android/server/art/OutputArtifacts.aidl
+++ b/artd/binder/com/android/server/art/OutputArtifacts.aidl
@@ -40,8 +40,8 @@
             /** The seinfo tag in SELinux policy. */
             @utf8InCpp String seInfo;
 
-            /** The package uid. */
-            int packageUid;
+            /** The uid that represents the combination of the user id and the app id. */
+            int uid;
         }
 
         /**
diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java
index f7df305..21563aea 100644
--- a/libartservice/service/java/com/android/server/art/AidlUtils.java
+++ b/libartservice/service/java/com/android/server/art/AidlUtils.java
@@ -170,6 +170,14 @@
     }
 
     @NonNull
+    public static SeContext buildSeContext(@NonNull String seInfo, int uid) {
+        var seContext = new SeContext();
+        seContext.seInfo = seInfo;
+        seContext.uid = uid;
+        return seContext;
+    }
+
+    @NonNull
     public static String toString(@NonNull PrimaryRefProfilePath profile) {
         return String.format(
                 "[packageName = %s, profileName = %s]", profile.packageName, profile.profileName);
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index b9485a0..2c90fb6 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -90,6 +90,11 @@
                             case "-f":
                                 paramsBuilder.setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE);
                                 break;
+                            case "--secondary-dex":
+                                paramsBuilder.setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
+                                        ArtFlags.FLAG_FOR_PRIMARY_DEX
+                                                | ArtFlags.FLAG_FOR_SECONDARY_DEX);
+                                break;
                             default:
                                 pw.println("Error: Unknown option: " + opt);
                                 return 1;
@@ -226,6 +231,7 @@
         pw.println("    Options:");
         pw.println("      -m Set the compiler filter.");
         pw.println("      -f Force compilation.");
+        pw.println("      --secondary-dex Only compile secondary dex.");
         pw.println("  cancel JOB_ID");
         pw.println("    Cancel a job.");
         pw.println("  dex-use-notify PACKAGE_NAME DEX_PATH CLASS_LOADER_CONTEXT");
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
index c87d71b..bf84e48 100644
--- a/libartservice/service/java/com/android/server/art/DexOptHelper.java
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -109,9 +109,13 @@
             }
 
             if ((params.getFlags() & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) {
-                // TODO(jiakaiz): Implement this.
-                throw new UnsupportedOperationException(
-                        "Optimizing secondary dex'es is not implemented yet");
+                results.addAll(
+                        mInjector
+                                .getSecondaryDexOptimizer(pkgState, pkg, params, cancellationSignal)
+                                .dexopt());
+                if (hasCancelledResult.get()) {
+                    return createResult.get();
+                }
             }
 
             if ((params.getFlags() & ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES) != 0) {
@@ -166,6 +170,13 @@
         }
 
         @NonNull
+        SecondaryDexOptimizer getSecondaryDexOptimizer(@NonNull PackageState pkgState,
+                @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+                @NonNull CancellationSignal cancellationSignal) {
+            return new SecondaryDexOptimizer(mContext, pkgState, pkg, params, cancellationSignal);
+        }
+
+        @NonNull
         public AppHibernationManager getAppHibernationManager() {
             return mContext.getSystemService(AppHibernationManager.class);
         }
diff --git a/libartservice/service/java/com/android/server/art/DexOptimizer.java b/libartservice/service/java/com/android/server/art/DexOptimizer.java
index 078f698..8cfe8ad 100644
--- a/libartservice/service/java/com/android/server/art/DexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/DexOptimizer.java
@@ -86,11 +86,6 @@
     public final List<DexContainerFileOptimizeResult> dexopt() throws RemoteException {
         List<DexContainerFileOptimizeResult> results = new ArrayList<>();
 
-        String targetCompilerFilter = adjustCompilerFilter(mParams.getCompilerFilter());
-        if (targetCompilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
-            return results;
-        }
-
         for (DexInfoType dexInfo : getDexInfoList()) {
             ProfilePath profile = null;
             boolean succeeded = true;
@@ -99,7 +94,10 @@
                     continue;
                 }
 
-                String compilerFilter = targetCompilerFilter;
+                String compilerFilter = adjustCompilerFilter(mParams.getCompilerFilter(), dexInfo);
+                if (compilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
+                    continue;
+                }
 
                 boolean needsToBeShared = needsToBeShared(dexInfo);
                 boolean isOtherReadable = true;
@@ -138,7 +136,8 @@
                         DexFile.isProfileGuidedCompilerFilter(compilerFilter);
                 Utils.check(isProfileGuidedCompilerFilter == (profile != null));
 
-                boolean canBePublic = !isProfileGuidedCompilerFilter || isOtherReadable;
+                boolean canBePublic = (!isProfileGuidedCompilerFilter || isOtherReadable)
+                        && isDexFilePublic(dexInfo);
                 Utils.check(Utils.implies(needsToBeShared, canBePublic));
                 PermissionSettings permissionSettings = getPermissionSettings(dexInfo, canBePublic);
 
@@ -245,7 +244,8 @@
     }
 
     @NonNull
-    private String adjustCompilerFilter(@NonNull String targetCompilerFilter) {
+    private String adjustCompilerFilter(
+            @NonNull String targetCompilerFilter, @NonNull DexInfoType dexInfo) {
         if (mInjector.isSystemUiPackage(mPkgState.getPackageName())) {
             String systemUiCompilerFilter = getSystemUiCompilerFilter();
             if (!systemUiCompilerFilter.isEmpty()) {
@@ -264,6 +264,12 @@
             return DexFile.getSafeModeCompilerFilter(targetCompilerFilter);
         }
 
+        // We cannot do AOT compilation if we don't have a valid class loader context.
+        if (dexInfo.classLoaderContext() == null
+                && DexFile.isOptimizedCompilerFilter(targetCompilerFilter)) {
+            return "verify";
+        }
+
         return targetCompilerFilter;
     }
 
@@ -346,6 +352,9 @@
 
         // The result should come from artd even if all the bits of `dexoptTrigger` are set
         // because the result also contains information about the usable VDEX file.
+        // Note that the class loader context can be null. In that case, we intentionally pass the
+        // null value down to lower levels to indicate that the class loader context check should be
+        // skipped because we are only going to verify the dex code (see `adjustCompilerFilter`).
         GetDexoptNeededResult result = mInjector.getArtd().getDexoptNeeded(
                 target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(),
                 target.compilerFilter(), dexoptTrigger);
@@ -470,12 +479,21 @@
     @NonNull protected abstract List<DexInfoType> getDexInfoList();
 
     /** Returns true if the given dex file should be optimized. */
-    protected abstract boolean isOptimizable(@NonNull DexInfoType dexInfo) throws RemoteException;
+    protected abstract boolean isOptimizable(@NonNull DexInfoType dexInfo);
 
-    /** Returns true if the artifacts should be shared with other apps. */
+    /**
+     * Returns true if the artifacts should be shared with other apps. Note that this must imply
+     * {@link #isDexFilePublic(DexInfoType)}.
+     */
     protected abstract boolean needsToBeShared(@NonNull DexInfoType dexInfo);
 
     /**
+     * Returns true if the filesystem permission of the dex file has the "read" bit for "others"
+     * (S_IROTH).
+     */
+    protected abstract boolean isDexFilePublic(@NonNull DexInfoType dexInfo);
+
+    /**
      * Returns a reference profile initialized from an external profile (e.g., a DM profile) if
      * one exists, or null otherwise.
      */
@@ -552,30 +570,30 @@
      *
      * @hide
      */
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
     public static class Injector {
         @NonNull private final Context mContext;
 
-        Injector(@NonNull Context context) {
+        public Injector(@NonNull Context context) {
             mContext = context;
         }
 
-        boolean isSystemUiPackage(@NonNull String packageName) {
+        public boolean isSystemUiPackage(@NonNull String packageName) {
             return packageName.equals(mContext.getString(R.string.config_systemUi));
         }
 
         @NonNull
-        UserManager getUserManager() {
+        public UserManager getUserManager() {
             return Objects.requireNonNull(mContext.getSystemService(UserManager.class));
         }
 
         @NonNull
-        DexUseManager getDexUseManager() {
+        public DexUseManager getDexUseManager() {
             return DexUseManager.getInstance();
         }
 
         @NonNull
-        IArtd getArtd() {
+        public IArtd getArtd() {
             return Utils.getArtd();
         }
     }
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
index 9cbca54..e51c3bf 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManager.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.UserHandle;
 import android.util.Log;
 
@@ -72,6 +74,8 @@
 
     @GuardedBy("DexUseManager.class") @Nullable private static DexUseManager sInstance = null;
 
+    @NonNull private final Injector mInjector;
+
     @GuardedBy("this") @NonNull private DexUse mDexUse = new DexUse();
 
     @NonNull
@@ -82,6 +86,15 @@
         return sInstance;
     }
 
+    private DexUseManager() {
+        this(new Injector());
+    }
+
+    @VisibleForTesting
+    public DexUseManager(@NonNull Injector injector) {
+        mInjector = injector;
+    }
+
     /** Returns all entities that load the given primary dex file owned by the given package. */
     @VisibleForTesting
     @NonNull
@@ -104,22 +117,65 @@
         return isUsedByOtherApps(getPrimaryDexLoaders(packageName, dexPath), packageName);
     }
 
-    /** Returns information about all secondary dex files owned by the given package. */
-    public synchronized @NonNull List<SecondaryDexInfo> getSecondaryDexInfo(
+    /**
+     * Returns the basic information about all secondary dex files owned by the given package. This
+     * method doesn't take dex file visibility into account, so it can only be used for debugging
+     * purpose, such as dumpsys.
+     *
+     * @see #getFilteredDetailedSecondaryDexInfo(String)
+     */
+    public @NonNull List<? extends SecondaryDexInfo> getSecondaryDexInfo(
             @NonNull String packageName) {
+        return getSecondaryDexInfoImpl(packageName, false /* checkDexFile */);
+    }
+
+    /**
+     * Same as above, but requires disk IO, and returns the detailed information, including dex file
+     * visibility, filtered by dex file existence and visibility.
+     */
+    public @NonNull List<DetailedSecondaryDexInfo> getFilteredDetailedSecondaryDexInfo(
+            @NonNull String packageName) {
+        return getSecondaryDexInfoImpl(packageName, true /* checkDexFile */);
+    }
+
+    /**
+     * @param checkDexFile if true, check the existence and visibility of the dex files, and filter
+     *         the results accordingly. Note that the value of the {@link
+     *         DetailedSecondaryDexInfo#isDexFilePublic()} field is undefined if this argument is
+     *         false.
+     */
+    private synchronized @NonNull List<DetailedSecondaryDexInfo> getSecondaryDexInfoImpl(
+            @NonNull String packageName, boolean checkDexFile) {
         PackageDexUse packageDexUse = mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
         if (packageDexUse == null) {
             return List.of();
         }
-        var results = new ArrayList<SecondaryDexInfo>();
+        var results = new ArrayList<DetailedSecondaryDexInfo>();
         for (var entry : packageDexUse.mSecondaryDexUseByDexFile.entrySet()) {
             String dexPath = entry.getKey();
             SecondaryDexUse secondaryDexUse = entry.getValue();
-            if (secondaryDexUse.mRecordByLoader.isEmpty()) {
+
+            @FileVisibility
+            int visibility =
+                    checkDexFile ? getDexFileVisibility(dexPath) : FileVisibility.OTHER_READABLE;
+            if (visibility == FileVisibility.NOT_FOUND) {
+                continue;
+            }
+
+            Map<DexLoader, SecondaryDexUseRecord> filteredRecordByLoader;
+            if (visibility == FileVisibility.OTHER_READABLE) {
+                filteredRecordByLoader = secondaryDexUse.mRecordByLoader;
+            } else {
+                // Only keep the entry that belongs to the same app.
+                DexLoader sameApp = DexLoader.create(packageName, false /* isolatedProcess */);
+                SecondaryDexUseRecord record = secondaryDexUse.mRecordByLoader.get(sameApp);
+                filteredRecordByLoader = record != null ? Map.of(sameApp, record) : Map.of();
+            }
+            if (filteredRecordByLoader.isEmpty()) {
                 continue;
             }
             List<String> distinctClcList =
-                    secondaryDexUse.mRecordByLoader.values()
+                    filteredRecordByLoader.values()
                             .stream()
                             .map(record -> Utils.assertNonEmpty(record.mClassLoaderContext))
                             .filter(clc
@@ -140,14 +196,15 @@
             // need to take apps with unsupported CLCs into account because the vdex file is still
             // usable to them.
             Set<String> distinctAbiNames =
-                    secondaryDexUse.mRecordByLoader.values()
+                    filteredRecordByLoader.values()
                             .stream()
                             .map(record -> Utils.assertNonEmpty(record.mAbiName))
                             .collect(Collectors.toSet());
-            Set<DexLoader> loaders = Set.copyOf(secondaryDexUse.mRecordByLoader.keySet());
-            results.add(SecondaryDexInfo.create(dexPath,
+            Set<DexLoader> loaders = Set.copyOf(filteredRecordByLoader.keySet());
+            results.add(DetailedSecondaryDexInfo.create(dexPath,
                     Objects.requireNonNull(secondaryDexUse.mUserHandle), clc, distinctAbiNames,
-                    loaders, isUsedByOtherApps(loaders, packageName)));
+                    loaders, isUsedByOtherApps(loaders, packageName),
+                    visibility == FileVisibility.OTHER_READABLE));
         }
         return Collections.unmodifiableList(results);
     }
@@ -338,13 +395,21 @@
         // TODO(b/253570365): Make the validation more strict.
     }
 
+    private @FileVisibility int getDexFileVisibility(@NonNull String dexPath) {
+        try {
+            return mInjector.getArtd().getDexFileVisibility(dexPath);
+        } catch (ServiceSpecificException | RemoteException e) {
+            Log.e(TAG, "Failed to get visibility of " + dexPath, e);
+            return FileVisibility.NOT_FOUND;
+        }
+    }
+
     /**
-     * Detailed information about a secondary dex file (an APK or JAR file that an app adds to its
+     * Basic information about a secondary dex file (an APK or JAR file that an app adds to its
      * own data directory and loads dynamically).
      */
     @Immutable
-    @AutoValue
-    public abstract static class SecondaryDexInfo implements DetailedDexInfo {
+    public abstract static class SecondaryDexInfo {
         // Special encoding used to denote a foreign ClassLoader was found when trying to encode
         // class loader contexts for each classpath element in a ClassLoader.
         // Must be in sync with `kUnsupportedClassLoaderContextEncoding` in
@@ -358,14 +423,6 @@
         @VisibleForTesting
         public static final String VARYING_CLASS_LOADER_CONTEXTS = "=VaryingClassLoaderContexts=";
 
-        static SecondaryDexInfo create(@NonNull String dexPath, @NonNull UserHandle userHandle,
-                @Nullable String classLoaderContext, @NonNull Set<String> abiNames,
-                @NonNull Set<DexLoader> loaders, boolean isUsedByOtherApps) {
-            return new AutoValue_DexUseManager_SecondaryDexInfo(dexPath, userHandle,
-                    classLoaderContext, Collections.unmodifiableSet(abiNames),
-                    Collections.unmodifiableSet(loaders), isUsedByOtherApps);
-        }
-
         /** The absolute path to the dex file within the user's app data directory. */
         public abstract @NonNull String dexPath();
 
@@ -379,24 +436,52 @@
          * A string describing the structure of the class loader that the dex file is loaded with,
          * or {@link #UNSUPPORTED_CLASS_LOADER_CONTEXT} or {@link #VARYING_CLASS_LOADER_CONTEXTS}.
          */
-        public abstract @NonNull String classLoaderContext();
+        public abstract @NonNull String displayClassLoaderContext();
 
-        /** The set of ABIs of the dex file is loaded with. */
+        /**
+         * A string describing the structure of the class loader that the dex file is loaded with,
+         * or null if the class loader context is invalid.
+         */
+        public @Nullable String classLoaderContext() {
+            return !displayClassLoaderContext().equals(UNSUPPORTED_CLASS_LOADER_CONTEXT)
+                            && !displayClassLoaderContext().equals(VARYING_CLASS_LOADER_CONTEXTS)
+                    ? displayClassLoaderContext()
+                    : null;
+        }
+
+        /** The set of ABIs of the dex file is loaded with. Guaranteed to be non-empty. */
         public abstract @NonNull Set<String> abiNames();
 
-        /** The set of entities that load the dex file. */
+        /** The set of entities that load the dex file. Guaranteed to be non-empty. */
         public abstract @NonNull Set<DexLoader> loaders();
 
         /** Returns whether the dex file is used by apps other than the app that owns it. */
         public abstract boolean isUsedByOtherApps();
+    }
+
+    /**
+     * Detailed information about a secondary dex file (an APK or JAR file that an app adds to its
+     * own data directory and loads dynamically). It contains the visibility of the dex file in
+     * addition to what is in {@link SecondaryDexInfo}, but producing it requires disk IO.
+     */
+    @Immutable
+    @AutoValue
+    public abstract static class DetailedSecondaryDexInfo
+            extends SecondaryDexInfo implements DetailedDexInfo {
+        static DetailedSecondaryDexInfo create(@NonNull String dexPath,
+                @NonNull UserHandle userHandle, @NonNull String displayClassLoaderContext,
+                @NonNull Set<String> abiNames, @NonNull Set<DexLoader> loaders,
+                boolean isUsedByOtherApps, boolean isDexFilePublic) {
+            return new AutoValue_DexUseManager_DetailedSecondaryDexInfo(dexPath, userHandle,
+                    displayClassLoaderContext, Collections.unmodifiableSet(abiNames),
+                    Collections.unmodifiableSet(loaders), isUsedByOtherApps, isDexFilePublic);
+        }
 
         /**
-         * Returns true if the class loader context is suitable for compilation.
+         * Returns true if the filesystem permission of the dex file has the "read" bit for "others"
+         * (S_IROTH).
          */
-        public boolean isClassLoaderContextValid() {
-            return !classLoaderContext().equals(UNSUPPORTED_CLASS_LOADER_CONTEXT)
-                    && !classLoaderContext().equals(VARYING_CLASS_LOADER_CONTEXTS);
-        }
+        public abstract boolean isDexFilePublic();
     }
 
     private static class DexUse {
@@ -543,4 +628,17 @@
             mAbiName = Utils.assertNonEmpty(proto.getAbiName());
         }
     }
+
+    /**
+     * Injector pattern for testing purpose.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static class Injector {
+        @NonNull
+        public IArtd getArtd() {
+            return Utils.getArtd();
+        }
+    }
 }
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index ea1b20a..032f2ae 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -98,6 +98,13 @@
     }
 
     @Override
+    protected boolean isDexFilePublic(@NonNull DetailedPrimaryDexInfo dexInfo) {
+        // The filesystem permission of a primary dex file always has the S_IROTH bit. In practice,
+        // the accessibility is enforced by Application Sandbox, not filesystem permission.
+        return true;
+    }
+
+    @Override
     @Nullable
     protected ProfilePath initReferenceProfile(@NonNull DetailedPrimaryDexInfo dexInfo)
             throws RemoteException {
@@ -170,6 +177,7 @@
     protected boolean isAppImageAllowed() {
         // Disable app images if the app requests for the splits to be loaded in isolation because
         // app images are unsupported for multiple class loaders (b/72696798).
+        // TODO(jiakaiz): Investigate whether this is still the best choice today.
         return !PrimaryDexUtils.isIsolatedSplitLoading(mPkg);
     }
 
@@ -178,8 +186,8 @@
     protected OutputProfile buildOutputProfile(
             @NonNull DetailedPrimaryDexInfo dexInfo, boolean isPublic) {
         String profileName = getProfileName(dexInfo.splitName());
-        return AidlUtils.buildOutputProfileForPrimary(mPkgState.getPackageName(), profileName,
-                mPkgState.getAppId(), mSharedGid, isPublic);
+        return AidlUtils.buildOutputProfileForPrimary(
+                mPkgState.getPackageName(), profileName, Process.SYSTEM_UID, mSharedGid, isPublic);
     }
 
     @Override
diff --git a/libartservice/service/java/com/android/server/art/SecondaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/SecondaryDexOptimizer.java
new file mode 100644
index 0000000..95b9ddb
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/SecondaryDexOptimizer.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art;
+
+import static com.android.server.art.DexUseManager.DetailedSecondaryDexInfo;
+import static com.android.server.art.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
+import static com.android.server.art.Utils.Abi;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.CancellationSignal;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
+
+import java.util.List;
+
+/** @hide */
+public class SecondaryDexOptimizer extends DexOptimizer<DetailedSecondaryDexInfo> {
+    private static final String TAG = "SecondaryDexOptimizer";
+
+    public SecondaryDexOptimizer(@NonNull Context context, @NonNull PackageState pkgState,
+            @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+            @NonNull CancellationSignal cancellationSignal) {
+        this(new Injector(context), pkgState, pkg, params, cancellationSignal);
+    }
+
+    @VisibleForTesting
+    public SecondaryDexOptimizer(@NonNull Injector injector, @NonNull PackageState pkgState,
+            @NonNull AndroidPackage pkg, @NonNull OptimizeParams params,
+            @NonNull CancellationSignal cancellationSignal) {
+        super(injector, pkgState, pkg, params, cancellationSignal);
+    }
+
+    @Override
+    protected boolean isInDalvikCache() {
+        // A secondary dex file is added by the app, so it's always in a writable location and hence
+        // never uses dalvik-cache.
+        return false;
+    }
+
+    @Override
+    @NonNull
+    protected List<DetailedSecondaryDexInfo> getDexInfoList() {
+        return mInjector.getDexUseManager().getFilteredDetailedSecondaryDexInfo(
+                mPkgState.getPackageName());
+    }
+
+    @Override
+    protected boolean isOptimizable(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return true;
+    }
+
+    @Override
+    protected boolean needsToBeShared(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return dexInfo.isUsedByOtherApps();
+    }
+
+    @Override
+    protected boolean isDexFilePublic(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return dexInfo.isDexFilePublic();
+    }
+
+    @Override
+    @Nullable
+    protected ProfilePath initReferenceProfile(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        // A secondary dex file doesn't have any external profile to use.
+        return null;
+    }
+
+    @Override
+    @NonNull
+    protected PermissionSettings getPermissionSettings(
+            @NonNull DetailedSecondaryDexInfo dexInfo, boolean canBePublic) {
+        int uid = getUid(dexInfo);
+        // We don't need the "read" bit for "others" on the directories because others
+        // only need to access the files in the directories, but they don't need to "ls"
+        // the directories.
+        FsPermission dirFsPermission = AidlUtils.buildFsPermission(
+                uid, uid, false /* isOtherReadable */, canBePublic /* isOtherExecutable */);
+        FsPermission fileFsPermission = AidlUtils.buildFsPermission(uid, uid, canBePublic);
+        SeContext seContext = AidlUtils.buildSeContext(
+                new com.android.server.art.wrapper.PackageState(mPkgState).getSeInfo(), uid);
+        return AidlUtils.buildPermissionSettings(dirFsPermission, fileFsPermission, seContext);
+    }
+
+    @Override
+    @NonNull
+    protected List<Abi> getAllAbis(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return Utils.getAllAbisForNames(dexInfo.abiNames(), mPkgState);
+    }
+
+    @Override
+    @NonNull
+    protected ProfilePath buildRefProfilePath(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return AidlUtils.buildProfilePathForSecondaryRef(dexInfo.dexPath());
+    }
+
+    @Override
+    protected boolean isAppImageAllowed() {
+        // The runtime can only load the app image of the base APK.
+        return false;
+    }
+
+    @Override
+    @NonNull
+    protected OutputProfile buildOutputProfile(
+            @NonNull DetailedSecondaryDexInfo dexInfo, boolean isPublic) {
+        int uid = getUid(dexInfo);
+        return AidlUtils.buildOutputProfileForSecondary(dexInfo.dexPath(), uid, uid, isPublic);
+    }
+
+    @Override
+    @NonNull
+    protected List<ProfilePath> getCurProfiles(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        // A secondary dex file can only be loaded by one user, so there is only one profile.
+        return List.of(AidlUtils.buildProfilePathForSecondaryCur(dexInfo.dexPath()));
+    }
+
+    private int getUid(@NonNull DetailedSecondaryDexInfo dexInfo) {
+        return dexInfo.userHandle().getUid(mPkgState.getAppId());
+    }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 2bd642a..8136b0a 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -36,6 +36,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /** @hide */
 public final class Utils {
@@ -62,6 +64,7 @@
         return array == null || array.length == 0;
     }
 
+    /** Returns the ABI information for the package. */
     @NonNull
     public static List<Abi> getAllAbis(@NonNull PackageState pkgState) {
         List<Abi> abis = new ArrayList<>();
@@ -82,6 +85,18 @@
         return abis;
     }
 
+    /** Returns the ABI information for the ABIs with the given names. */
+    @NonNull
+    public static List<Abi> getAllAbisForNames(
+            @NonNull Set<String> abiNames, @NonNull PackageState pkgState) {
+        Abi pkgPrimaryAbi = getPrimaryAbi(pkgState);
+        return abiNames.stream()
+                .map(name
+                        -> Abi.create(name, VMRuntime.getInstructionSet(name),
+                                name.equals(pkgPrimaryAbi.name())))
+                .collect(Collectors.toList());
+    }
+
     @NonNull
     public static Abi getPrimaryAbi(@NonNull PackageState pkgState) {
         String primaryCpuAbi = pkgState.getPrimaryCpuAbi();
diff --git a/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java b/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java
index 0f9a20e..932813f 100644
--- a/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java
+++ b/libartservice/service/java/com/android/server/art/model/DetailedDexInfo.java
@@ -17,6 +17,7 @@
 package com.android.server.art.model;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 import com.android.internal.annotations.Immutable;
 
@@ -31,7 +32,8 @@
     @NonNull String dexPath();
 
     /**
-     * A string describing the structure of the class loader that the dex file is loaded with.
+     * A string describing the structure of the class loader that the dex file is loaded with, or
+     * null if the class loader context is invalid.
      */
-    @NonNull String classLoaderContext();
+    @Nullable String classLoaderContext();
 }
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
index fafe8ca..3f1e871 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
@@ -19,12 +19,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.server.pm.pkg.AndroidPackage;
 
 /** @hide */
 public class PackageState {
-    @NonNull private final Object mPkgState;
+    @NonNull private final com.android.server.pm.pkg.PackageState mPkgState;
 
-    public PackageState(@NonNull Object pkgState) {
+    public PackageState(@NonNull com.android.server.pm.pkg.PackageState pkgState) {
         mPkgState = pkgState;
     }
 
@@ -36,4 +39,32 @@
             throw new RuntimeException(e);
         }
     }
+
+    @Nullable
+    public String getSeInfo() {
+        try {
+            Object pkgStateUnserialized =
+                    mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState);
+            String seInfo = (String) pkgStateUnserialized.getClass()
+                                    .getMethod("getOverrideSeInfo")
+                                    .invoke(pkgStateUnserialized);
+            if (!TextUtils.isEmpty(seInfo)) {
+                return seInfo;
+            }
+
+            // Default to the information in `AndroidPackage`. The defaulting behavior will
+            // eventually be done by `PackageState` internally.
+            AndroidPackage pkg = mPkgState.getAndroidPackage();
+            if (pkg == null) {
+                // This should never happen because we check the existence of the package at the
+                // beginning of each ART Services method.
+                throw new IllegalStateException("Unable to get package "
+                        + mPkgState.getPackageName() + ". This should never happen.");
+            }
+
+            return (String) pkg.getClass().getMethod("getSeInfo").invoke(pkg);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index aebc502..f83d1bc 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.art;
 
+import static com.android.server.art.DexUseManager.DetailedSecondaryDexInfo;
 import static com.android.server.art.DexUseManager.DexLoader;
 import static com.android.server.art.DexUseManager.SecondaryDexInfo;
 
@@ -79,8 +80,17 @@
 
     private final UserHandle mUserHandle = Binder.getCallingUserHandle();
 
+    /**
+     * The default value of `isDexFilePublic` returned by `getSecondaryDexInfo`. The value doesn't
+     * matter because it's undefined, but it's needed for deep equality check, to make the test
+     * simpler.
+     */
+    private final boolean mDefaultIsDexFilePublic = true;
+
     @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
-    private DexUseManager mDexUseManager = DexUseManager.getInstance();
+    @Mock private DexUseManager.Injector mInjector;
+    @Mock private IArtd mArtd;
+    private DexUseManager mDexUseManager;
     private String mCeDir;
     private String mDeDir;
 
@@ -129,7 +139,9 @@
                                  Binder.getCallingUserHandle().getIdentifier(), OWNING_PKG_NAME)
                          .toString();
 
-        mDexUseManager.clear();
+        lenient().when(mInjector.getArtd()).thenReturn(mArtd);
+
+        mDexUseManager = new DexUseManager(mInjector);
     }
 
     @Test
@@ -236,13 +248,14 @@
     public void testSecondaryDexOwned() {
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle, "CLC",
-                        Set.of("arm64-v8a"),
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        "CLC", Set.of("arm64-v8a"),
                         Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */)),
-                        false /* isUsedByOtherApps */));
-        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+                        false /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+        assertThat(dexInfoList.get(0).classLoaderContext()).isEqualTo("CLC");
     }
 
     @Test
@@ -250,26 +263,28 @@
         when(Process.isIsolated(anyInt())).thenReturn(true);
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mDeDir + "/foo.apk", "CLC"));
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mDeDir + "/foo.apk", mUserHandle, "CLC",
-                        Set.of("arm64-v8a"),
+                .containsExactly(DetailedSecondaryDexInfo.create(mDeDir + "/foo.apk", mUserHandle,
+                        "CLC", Set.of("arm64-v8a"),
                         Set.of(DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */)),
-                        true /* isUsedByOtherApps */));
-        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+                        true /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+        assertThat(dexInfoList.get(0).classLoaderContext()).isEqualTo("CLC");
     }
 
     @Test
     public void testSecondaryDexOthers() {
         mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle, "CLC",
-                        Set.of("armeabi-v7a"),
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        "CLC", Set.of("armeabi-v7a"),
                         Set.of(DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
-                        true /* isUsedByOtherApps */));
-        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isTrue();
+                        true /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+        assertThat(dexInfoList.get(0).classLoaderContext()).isEqualTo("CLC");
     }
 
     @Test
@@ -277,13 +292,14 @@
         mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME,
                 Map.of(mCeDir + "/foo.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
                         SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT, Set.of("armeabi-v7a"),
                         Set.of(DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
-                        true /* isUsedByOtherApps */));
-        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isFalse();
+                        true /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+        assertThat(dexInfoList.get(0).classLoaderContext()).isNull();
     }
 
     @Test
@@ -291,15 +307,16 @@
         mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
         mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC2"));
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
                         SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS,
                         Set.of("arm64-v8a", "armeabi-v7a"),
                         Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
                                 DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
-                        true /* isUsedByOtherApps */));
-        assertThat(dexInfoList.get(0).isClassLoaderContextValid()).isFalse();
+                        true /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+        assertThat(dexInfoList.get(0).classLoaderContext()).isNull();
     }
 
     /** Checks that it ignores and dedups things correctly. */
@@ -350,9 +367,10 @@
             mDexUseManager.load(tempFile.getPath());
         }
 
-        List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
+        List<? extends SecondaryDexInfo> dexInfoList =
+                mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
         assertThat(dexInfoList)
-                .containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
                                          "UpdatedCLC", Set.of("arm64-v8a", "armeabi-v7a"),
                                          Set.of(DexLoader.create(OWNING_PKG_NAME,
                                                         false /* isolatedProcess */),
@@ -360,21 +378,77 @@
                                                          true /* isolatedProcess */),
                                                  DexLoader.create(LOADING_PKG_NAME,
                                                          false /* isolatedProcess */)),
-                                         true /* isUsedByOtherApps */),
-                        SecondaryDexInfo.create(mCeDir + "/bar.apk", mUserHandle,
+                                         true /* isUsedByOtherApps */, mDefaultIsDexFilePublic),
+                        DetailedSecondaryDexInfo.create(mCeDir + "/bar.apk", mUserHandle,
                                 SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS,
                                 Set.of("arm64-v8a", "armeabi-v7a"),
                                 Set.of(DexLoader.create(
                                                OWNING_PKG_NAME, false /* isolatedProcess */),
                                         DexLoader.create(
                                                 LOADING_PKG_NAME, false /* isolatedProcess */)),
-                                true /* isUsedByOtherApps */),
-                        SecondaryDexInfo.create(mCeDir + "/baz.apk", mUserHandle,
+                                true /* isUsedByOtherApps */, mDefaultIsDexFilePublic),
+                        DetailedSecondaryDexInfo.create(mCeDir + "/baz.apk", mUserHandle,
                                 SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT,
                                 Set.of("arm64-v8a"),
                                 Set.of(DexLoader.create(
                                         OWNING_PKG_NAME, false /* isolatedProcess */)),
-                                false /* isUsedByOtherApps */));
+                                false /* isUsedByOtherApps */, mDefaultIsDexFilePublic));
+    }
+
+    @Test
+    public void testFilteredDetailedSecondaryDexPublic() throws Exception {
+        when(mArtd.getDexFileVisibility(mCeDir + "/foo.apk"))
+                .thenReturn(FileVisibility.OTHER_READABLE);
+
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        assertThat(mDexUseManager.getFilteredDetailedSecondaryDexInfo(OWNING_PKG_NAME))
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        "CLC", Set.of("arm64-v8a", "armeabi-v7a"),
+                        Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
+                                DexLoader.create(LOADING_PKG_NAME, false /* isolatedProcess */)),
+                        true /* isUsedByOtherApps */, true /* isDexFilePublic */));
+    }
+
+    @Test
+    public void testFilteredDetailedSecondaryDexPrivate() throws Exception {
+        when(mArtd.getDexFileVisibility(mCeDir + "/foo.apk"))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        assertThat(mDexUseManager.getFilteredDetailedSecondaryDexInfo(OWNING_PKG_NAME))
+                .containsExactly(DetailedSecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
+                        "CLC", Set.of("arm64-v8a"),
+                        Set.of(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */)),
+                        false /* isUsedByOtherApps */, false /* isDexFilePublic */));
+    }
+
+    @Test
+    public void testFilteredDetailedSecondaryDexFilteredDueToVisibility() throws Exception {
+        when(mArtd.getDexFileVisibility(mCeDir + "/foo.apk"))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        mDexUseManager.addDexUse(mSnapshot, LOADING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        when(Process.isIsolated(anyInt())).thenReturn(true);
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        assertThat(mDexUseManager.getFilteredDetailedSecondaryDexInfo(OWNING_PKG_NAME)).isEmpty();
+    }
+
+    @Test
+    public void testFilteredDetailedSecondaryDexFilteredDueToNotFound() throws Exception {
+        when(mArtd.getDexFileVisibility(mCeDir + "/foo.apk")).thenReturn(FileVisibility.NOT_FOUND);
+
+        mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(mCeDir + "/foo.apk", "CLC"));
+
+        assertThat(mDexUseManager.getFilteredDetailedSecondaryDexInfo(OWNING_PKG_NAME)).isEmpty();
     }
 
     @Test(expected = IllegalArgumentException.class)
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
index 2faa59d..5b9264d 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.os.Process;
 import android.os.ServiceSpecificException;
 import android.os.UserHandle;
 
@@ -73,9 +74,9 @@
     private final ProfilePath mPrebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(mDexPath);
     private final ProfilePath mDmProfile = AidlUtils.buildProfilePathForDm(mDexPath);
     private final OutputProfile mPublicOutputProfile = AidlUtils.buildOutputProfileForPrimary(
-            PKG_NAME, "primary", UID, SHARED_GID, true /* isOtherReadable */);
+            PKG_NAME, "primary", Process.SYSTEM_UID, SHARED_GID, true /* isOtherReadable */);
     private final OutputProfile mPrivateOutputProfile = AidlUtils.buildOutputProfileForPrimary(
-            PKG_NAME, "primary", UID, SHARED_GID, false /* isOtherReadable */);
+            PKG_NAME, "primary", Process.SYSTEM_UID, SHARED_GID, false /* isOtherReadable */);
 
     private final String mSplit0DexPath = "/data/app/foo/split_0.apk";
     private final ProfilePath mSplit0RefProfile =
diff --git a/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java
new file mode 100644
index 0000000..7b967e4
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art;
+
+import static com.android.server.art.DexUseManager.DetailedSecondaryDexInfo;
+import static com.android.server.art.DexUseManager.SecondaryDexInfo;
+import static com.android.server.art.GetDexoptNeededResult.ArtifactsLocation;
+import static com.android.server.art.OutputArtifacts.PermissionSettings;
+import static com.android.server.art.model.OptimizeResult.DexContainerFileOptimizeResult;
+import static com.android.server.art.testing.TestingUtils.deepEq;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.isNull;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.CancellationSignal;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.testing.StaticMockitoRule;
+import com.android.server.art.testing.TestingUtils;
+import com.android.server.pm.PackageSetting;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.PackageStateUnserialized;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SecondaryDexOptimizerTest {
+    private static final String PKG_NAME = "com.example.foo";
+    private static final int APP_ID = 12345;
+    private static final UserHandle USER_HANDLE = UserHandle.of(2);
+    private static final int UID = USER_HANDLE.getUid(APP_ID);
+    private static final String APP_DATA_DIR = "/data/user/2/" + PKG_NAME;
+    private static final String DEX_1 = APP_DATA_DIR + "/1.apk";
+    private static final String DEX_2 = APP_DATA_DIR + "/2.apk";
+    private static final String DEX_3 = APP_DATA_DIR + "/3.apk";
+
+    private final OptimizeParams mOptimizeParams =
+            new OptimizeParams.Builder("bg-dexopt")
+                    .setCompilerFilter("speed-profile")
+                    .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX, ArtFlags.FLAG_FOR_SECONDARY_DEX)
+                    .build();
+
+    private final ProfilePath mDex1RefProfile = AidlUtils.buildProfilePathForSecondaryRef(DEX_1);
+    private final ProfilePath mDex1CurProfile = AidlUtils.buildProfilePathForSecondaryCur(DEX_1);
+    private final ProfilePath mDex2RefProfile = AidlUtils.buildProfilePathForSecondaryRef(DEX_2);
+    private final ProfilePath mDex3RefProfile = AidlUtils.buildProfilePathForSecondaryRef(DEX_3);
+    private final OutputProfile mDex1PrivateOutputProfile =
+            AidlUtils.buildOutputProfileForSecondary(DEX_1, UID, UID, false /* isOtherReadable */);
+
+    private final int mDefaultDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+            | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+    private final int mBetterOrSameDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+            | DexoptTrigger.COMPILER_FILTER_IS_SAME
+            | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+
+    @Rule
+    public StaticMockitoRule mockitoRule =
+            new StaticMockitoRule(SystemProperties.class, Constants.class);
+
+    @Mock private SecondaryDexOptimizer.Injector mInjector;
+    @Mock private IArtd mArtd;
+    @Mock private DexUseManager mDexUseManager;
+    private PackageState mPkgState;
+    private AndroidPackage mPkg;
+    private CancellationSignal mCancellationSignal;
+
+    private SecondaryDexOptimizer mSecondaryDexOptimizer;
+
+    @Before
+    public void setUp() throws Exception {
+        lenient()
+                .when(SystemProperties.getBoolean(eq("dalvik.vm.always_debuggable"), anyBoolean()))
+                .thenReturn(false);
+        lenient().when(SystemProperties.get("dalvik.vm.appimageformat")).thenReturn("lz4");
+        lenient().when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed");
+
+        // No ISA translation.
+        lenient()
+                .when(SystemProperties.get(argThat(arg -> arg.startsWith("ro.dalvik.vm.isa."))))
+                .thenReturn("");
+
+        lenient().when(Constants.getPreferredAbi()).thenReturn("arm64-v8a");
+        lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
+        lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
+
+        lenient().when(mInjector.getArtd()).thenReturn(mArtd);
+        lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(false);
+        lenient().when(mInjector.getDexUseManager()).thenReturn(mDexUseManager);
+
+        List<DetailedSecondaryDexInfo> secondaryDexInfo = createSecondaryDexInfo();
+        lenient()
+                .when(mDexUseManager.getFilteredDetailedSecondaryDexInfo(eq(PKG_NAME)))
+                .thenReturn(secondaryDexInfo);
+
+        mPkgState = createPackageState();
+        mPkg = mPkgState.getAndroidPackage();
+        mCancellationSignal = new CancellationSignal();
+
+        prepareProfiles();
+
+        // Dexopt is always needed and successful.
+        lenient()
+                .when(mArtd.getDexoptNeeded(any(), any(), any(), any(), anyInt()))
+                .thenReturn(dexoptIsNeeded());
+        lenient()
+                .when(mArtd.dexopt(
+                        any(), any(), any(), any(), any(), any(), any(), anyInt(), any(), any()))
+                .thenReturn(createDexoptResult());
+
+        lenient()
+                .when(mArtd.createCancellationSignal())
+                .thenReturn(mock(IArtdCancellationSignal.class));
+
+        mSecondaryDexOptimizer = new SecondaryDexOptimizer(
+                mInjector, mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+    }
+
+    @Test
+    public void testDexopt() throws Exception {
+        assertThat(mSecondaryDexOptimizer.dexopt())
+                .comparingElementsUsing(TestingUtils.<DexContainerFileOptimizeResult>deepEquality())
+                .containsExactly(
+                        new DexContainerFileOptimizeResult(DEX_1, true /* isPrimaryAbi */,
+                                "arm64-v8a", "speed-profile", OptimizeResult.OPTIMIZE_PERFORMED,
+                                0 /* dex2oatWallTimeMillis */, 0 /* dex2oatCpuTimeMillis */),
+                        new DexContainerFileOptimizeResult(DEX_2, true /* isPrimaryAbi */,
+                                "arm64-v8a", "speed", OptimizeResult.OPTIMIZE_PERFORMED,
+                                0 /* dex2oatWallTimeMillis */, 0 /* dex2oatCpuTimeMillis */),
+                        new DexContainerFileOptimizeResult(DEX_2, false /* isPrimaryAbi */,
+                                "armeabi-v7a", "speed", OptimizeResult.OPTIMIZE_PERFORMED,
+                                0 /* dex2oatWallTimeMillis */, 0 /* dex2oatCpuTimeMillis */),
+                        new DexContainerFileOptimizeResult(DEX_3, true /* isPrimaryAbi */,
+                                "arm64-v8a", "verify", OptimizeResult.OPTIMIZE_PERFORMED,
+                                0 /* dex2oatWallTimeMillis */, 0 /* dex2oatCpuTimeMillis */));
+
+        // It should use profile for dex 1.
+
+        verify(mArtd).mergeProfiles(deepEq(List.of(mDex1CurProfile)), deepEq(mDex1RefProfile),
+                deepEq(mDex1PrivateOutputProfile), eq(DEX_1));
+
+        verify(mArtd).getDexoptNeeded(
+                eq(DEX_1), eq("arm64"), any(), eq("speed-profile"), eq(mBetterOrSameDexoptTrigger));
+        checkDexoptWithPrivateProfile(verify(mArtd), DEX_1, "arm64",
+                ProfilePath.tmpProfilePath(mDex1PrivateOutputProfile.profilePath), "CLC_FOR_DEX_1");
+
+        verify(mArtd).commitTmpProfile(deepEq(mDex1PrivateOutputProfile.profilePath));
+
+        verify(mArtd).deleteProfile(deepEq(mDex1CurProfile));
+
+        // It should use "speed" for dex 2 for both ISAs and make the artifacts public.
+
+        verify(mArtd, never()).isProfileUsable(deepEq(mDex2RefProfile), any());
+        verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex2RefProfile), any(), any());
+
+        verify(mArtd).getDexoptNeeded(
+                eq(DEX_2), eq("arm64"), any(), eq("speed"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithNoProfile(
+                verify(mArtd), DEX_2, "arm64", "speed", "CLC_FOR_DEX_2", true /* isPublic */);
+
+        verify(mArtd).getDexoptNeeded(
+                eq(DEX_2), eq("arm"), any(), eq("speed"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithNoProfile(
+                verify(mArtd), DEX_2, "arm", "speed", "CLC_FOR_DEX_2", true /* isPublic */);
+
+        // It should use "verify" for dex 3 and make the artifacts private.
+
+        verify(mArtd, never()).isProfileUsable(deepEq(mDex3RefProfile), any());
+        verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex3RefProfile), any(), any());
+
+        verify(mArtd).getDexoptNeeded(
+                eq(DEX_3), eq("arm64"), isNull(), eq("verify"), eq(mDefaultDexoptTrigger));
+        checkDexoptWithNoProfile(verify(mArtd), DEX_3, "arm64", "verify",
+                null /* classLoaderContext */, false /* isPublic */);
+    }
+
+    private AndroidPackage createPackage() {
+        var pkg = mock(AndroidPackage.class);
+        lenient().when(pkg.isVmSafeMode()).thenReturn(false);
+        lenient().when(pkg.isDebuggable()).thenReturn(false);
+        lenient().when(pkg.getTargetSdkVersion()).thenReturn(123);
+        lenient().when(pkg.isSignedWithPlatformKey()).thenReturn(false);
+        lenient().when(pkg.isUsesNonSdkApi()).thenReturn(false);
+        return pkg;
+    }
+
+    private PackageState createPackageState() {
+        // TODO(b/254029037): Change PackageSetting to PackageState.
+        var pkgState = mock(PackageSetting.class);
+        lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME);
+        lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn("arm64-v8a");
+        lenient().when(pkgState.getSecondaryCpuAbi()).thenReturn("armeabi-v7a");
+        lenient().when(pkgState.getAppId()).thenReturn(APP_ID);
+        AndroidPackage pkg = createPackage();
+        lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
+
+        // TODO(b/254029037): Mock the real API instead of the hidden API.
+        var transientState = mock(PackageStateUnserialized.class);
+        lenient().when(transientState.getOverrideSeInfo()).thenReturn("se-info");
+        lenient().when(pkgState.getTransientState()).thenReturn(transientState);
+
+        return pkgState;
+    }
+
+    private List<DetailedSecondaryDexInfo> createSecondaryDexInfo() throws Exception {
+        // This should be compiled with profile.
+        var dex1Info = mock(DetailedSecondaryDexInfo.class);
+        lenient().when(dex1Info.dexPath()).thenReturn(DEX_1);
+        lenient().when(dex1Info.userHandle()).thenReturn(USER_HANDLE);
+        lenient().when(dex1Info.classLoaderContext()).thenReturn("CLC_FOR_DEX_1");
+        lenient().when(dex1Info.abiNames()).thenReturn(Set.of("arm64-v8a"));
+        lenient().when(dex1Info.isUsedByOtherApps()).thenReturn(false);
+        lenient().when(dex1Info.isDexFilePublic()).thenReturn(true);
+
+        // This should be compiled without profile because it's used by other apps.
+        var dex2Info = mock(DetailedSecondaryDexInfo.class);
+        lenient().when(dex2Info.dexPath()).thenReturn(DEX_2);
+        lenient().when(dex2Info.userHandle()).thenReturn(USER_HANDLE);
+        lenient().when(dex2Info.classLoaderContext()).thenReturn("CLC_FOR_DEX_2");
+        lenient().when(dex2Info.abiNames()).thenReturn(Set.of("arm64-v8a", "armeabi-v7a"));
+        lenient().when(dex2Info.isUsedByOtherApps()).thenReturn(true);
+        lenient().when(dex2Info.isDexFilePublic()).thenReturn(true);
+
+        // This should be compiled with verify because the class loader context is invalid.
+        var dex3Info = mock(DetailedSecondaryDexInfo.class);
+        lenient().when(dex3Info.dexPath()).thenReturn(DEX_3);
+        lenient().when(dex3Info.userHandle()).thenReturn(USER_HANDLE);
+        lenient().when(dex3Info.classLoaderContext()).thenReturn(null);
+        lenient().when(dex3Info.abiNames()).thenReturn(Set.of("arm64-v8a"));
+        lenient().when(dex3Info.isUsedByOtherApps()).thenReturn(false);
+        lenient().when(dex3Info.isDexFilePublic()).thenReturn(false);
+
+        return List.of(dex1Info, dex2Info, dex3Info);
+    }
+
+    private void prepareProfiles() throws Exception {
+        // Profile for dex file 1 is usable.
+        lenient().when(mArtd.isProfileUsable(deepEq(mDex1RefProfile), any())).thenReturn(true);
+        lenient()
+                .when(mArtd.getProfileVisibility(deepEq(mDex1RefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        // Profiles for dex file 2 and 3 are also usable, but shouldn't be used.
+        lenient().when(mArtd.isProfileUsable(deepEq(mDex2RefProfile), any())).thenReturn(true);
+        lenient()
+                .when(mArtd.getProfileVisibility(deepEq(mDex2RefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+        lenient().when(mArtd.isProfileUsable(deepEq(mDex3RefProfile), any())).thenReturn(true);
+        lenient()
+                .when(mArtd.getProfileVisibility(deepEq(mDex3RefProfile)))
+                .thenReturn(FileVisibility.NOT_OTHER_READABLE);
+
+        lenient().when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(true);
+    }
+
+    private GetDexoptNeededResult dexoptIsNeeded() {
+        var result = new GetDexoptNeededResult();
+        result.isDexoptNeeded = true;
+        result.artifactsLocation = ArtifactsLocation.NONE_OR_ERROR;
+        result.isVdexUsable = false;
+        return result;
+    }
+
+    private DexoptResult createDexoptResult() {
+        var result = new DexoptResult();
+        result.cancelled = false;
+        result.wallTimeMs = 0;
+        result.cpuTimeMs = 0;
+        return result;
+    }
+
+    private void checkDexoptWithPrivateProfile(IArtd artd, String dexPath, String isa,
+            ProfilePath profile, String classLoaderContext) throws Exception {
+        PermissionSettings permissionSettings = buildPermissionSettings(false /* isPublic */);
+        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(
+                dexPath, isa, false /* isInDalvikCache */, permissionSettings);
+        artd.dexopt(deepEq(outputArtifacts), eq(dexPath), eq(isa), eq(classLoaderContext),
+                eq("speed-profile"), deepEq(profile), any(), anyInt(),
+                argThat(dexoptOptions -> dexoptOptions.generateAppImage == false), any());
+    }
+
+    private void checkDexoptWithNoProfile(IArtd artd, String dexPath, String isa,
+            String compilerFilter, String classLoaderContext, boolean isPublic) throws Exception {
+        PermissionSettings permissionSettings = buildPermissionSettings(isPublic);
+        OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(
+                dexPath, isa, false /* isInDalvikCache */, permissionSettings);
+        artd.dexopt(deepEq(outputArtifacts), eq(dexPath), eq(isa), eq(classLoaderContext),
+                eq(compilerFilter), isNull(), any(), anyInt(),
+                argThat(dexoptOptions -> dexoptOptions.generateAppImage == false), any());
+    }
+
+    private PermissionSettings buildPermissionSettings(boolean isPublic) {
+        return AidlUtils.buildPermissionSettings(
+                AidlUtils.buildFsPermission(UID, UID, false /* isOtherReadable */, isPublic),
+                AidlUtils.buildFsPermission(UID, UID, isPublic),
+                AidlUtils.buildSeContext("se-info", UID));
+    }
+}
diff --git a/runtime/native/dalvik_system_DexFile.cc b/runtime/native/dalvik_system_DexFile.cc
index 7664fe5..a4ccdf8 100644
--- a/runtime/native/dalvik_system_DexFile.cc
+++ b/runtime/native/dalvik_system_DexFile.cc
@@ -617,6 +617,8 @@
     return nullptr;
   }
 
+  // The API doesn't support passing a class loader context, so skip the class loader context check
+  // and assume that it's OK.
   OatFileAssistant oat_file_assistant(filename.c_str(),
                                       target_instruction_set,
                                       /* context= */ nullptr,
diff --git a/runtime/oat_file_assistant.cc b/runtime/oat_file_assistant.cc
index 389479c..9e0c173 100644
--- a/runtime/oat_file_assistant.cc
+++ b/runtime/oat_file_assistant.cc
@@ -226,7 +226,7 @@
 std::unique_ptr<OatFileAssistant> OatFileAssistant::Create(
     const std::string& filename,
     const std::string& isa_str,
-    const std::string& context_str,
+    const std::optional<std::string>& context_str,
     bool load_executable,
     bool only_load_trusted_executable,
     OatFileAssistantContext* ofa_context,
@@ -238,20 +238,23 @@
     return nullptr;
   }
 
-  std::unique_ptr<ClassLoaderContext> tmp_context = ClassLoaderContext::Create(context_str.c_str());
-  if (tmp_context == nullptr) {
-    *error_msg = StringPrintf("Class loader context '%s' is invalid", context_str.c_str());
-    return nullptr;
-  }
+  std::unique_ptr<ClassLoaderContext> tmp_context = nullptr;
+  if (context_str.has_value()) {
+    tmp_context = ClassLoaderContext::Create(context_str->c_str());
+    if (tmp_context == nullptr) {
+      *error_msg = StringPrintf("Class loader context '%s' is invalid", context_str->c_str());
+      return nullptr;
+    }
 
-  if (!tmp_context->OpenDexFiles(android::base::Dirname(filename.c_str()),
-                                 /*context_fds=*/{},
-                                 /*only_read_checksums=*/true)) {
-    *error_msg =
-        StringPrintf("Failed to load class loader context files for '%s' with context '%s'",
-                     filename.c_str(),
-                     context_str.c_str());
-    return nullptr;
+    if (!tmp_context->OpenDexFiles(android::base::Dirname(filename.c_str()),
+                                   /*context_fds=*/{},
+                                   /*only_read_checksums=*/true)) {
+      *error_msg =
+          StringPrintf("Failed to load class loader context files for '%s' with context '%s'",
+                       filename.c_str(),
+                       context_str->c_str());
+      return nullptr;
+    }
   }
 
   auto assistant = std::make_unique<OatFileAssistant>(filename.c_str(),
@@ -1182,6 +1185,11 @@
 }
 
 bool OatFileAssistant::ClassLoaderContextIsOkay(const OatFile& oat_file) const {
+  if (context_ == nullptr) {
+    // The caller requests to skip the check.
+    return true;
+  }
+
   if (oat_file.IsBackedByVdexOnly()) {
     // Only a vdex file, we don't depend on the class loader context.
     return true;
@@ -1193,12 +1201,6 @@
     return true;
   }
 
-  if (context_ == nullptr) {
-    // When no class loader context is provided (which happens for deprecated
-    // DexFile APIs), just assume it is OK.
-    return true;
-  }
-
   ClassLoaderContext::VerificationResult matches = context_->VerifyClassLoaderContextMatch(
       oat_file.GetClassLoaderContext(),
       /*verify_names=*/ true,
diff --git a/runtime/oat_file_assistant.h b/runtime/oat_file_assistant.h
index ce069d2..2d0a150 100644
--- a/runtime/oat_file_assistant.h
+++ b/runtime/oat_file_assistant.h
@@ -146,6 +146,8 @@
   // device. For example, on an arm device, use arm or arm64. An oat file can
   // be loaded executable only if the ISA matches the current runtime.
   //
+  // context should be the class loader context to check against, or null to skip the check.
+  //
   // load_executable should be true if the caller intends to try and load
   // executable code for this dex location.
   //
@@ -182,7 +184,7 @@
   static std::unique_ptr<OatFileAssistant> Create(
       const std::string& filename,
       const std::string& isa_str,
-      const std::string& context_str,
+      const std::optional<std::string>& context_str,
       bool load_executable,
       bool only_load_trusted_executable,
       OatFileAssistantContext* ofa_context,
@@ -523,6 +525,8 @@
 
   std::string dex_location_;
 
+  // The class loader context to check against, or null representing that the check should be
+  // skipped.
   ClassLoaderContext* context_;
 
   // Whether or not the parent directory of the dex file is writable.
diff --git a/runtime/oat_file_assistant_test.cc b/runtime/oat_file_assistant_test.cc
index 2904ecb..090532c 100644
--- a/runtime/oat_file_assistant_test.cc
+++ b/runtime/oat_file_assistant_test.cc
@@ -23,6 +23,7 @@
 #include <functional>
 #include <iterator>
 #include <memory>
+#include <optional>
 #include <string>
 #include <type_traits>
 #include <vector>
@@ -78,33 +79,40 @@
     // Verify the static method (called from PM for dumpsys).
     // This variant does not check class loader context.
     if (!check_context) {
-      std::string compilation_filter1;
-      std::string compilation_reason1;
+      std::string compilation_filter;
+      std::string compilation_reason;
 
       OatFileAssistant::GetOptimizationStatus(file,
                                               kRuntimeISA,
-                                              &compilation_filter1,
-                                              &compilation_reason1,
+                                              &compilation_filter,
+                                              &compilation_reason,
                                               MaybeGetOatFileAssistantContext());
 
-      ASSERT_EQ(expected_filter_name, compilation_filter1);
-      ASSERT_EQ(expected_reason, compilation_reason1);
+      ASSERT_EQ(expected_filter_name, compilation_filter);
+      ASSERT_EQ(expected_reason, compilation_reason);
     }
 
     // Verify the instance methods (called at runtime and from artd).
     OatFileAssistant assistant = CreateOatFileAssistant(file.c_str(), context);
+    VerifyOptimizationStatusWithInstance(
+        &assistant, expected_filter_name, expected_reason, expected_odex_status);
+  }
 
-    std::string odex_location3;  // ignored
-    std::string compilation_filter3;
-    std::string compilation_reason3;
-    std::string odex_status3;
+  void VerifyOptimizationStatusWithInstance(OatFileAssistant* assistant,
+                                            const std::string& expected_filter,
+                                            const std::string& expected_reason,
+                                            const std::string& expected_odex_status) {
+    std::string odex_location;  // ignored
+    std::string compilation_filter;
+    std::string compilation_reason;
+    std::string odex_status;
 
-    assistant.GetOptimizationStatus(
-        &odex_location3, &compilation_filter3, &compilation_reason3, &odex_status3);
+    assistant->GetOptimizationStatus(
+        &odex_location, &compilation_filter, &compilation_reason, &odex_status);
 
-    ASSERT_EQ(expected_filter_name, compilation_filter3);
-    ASSERT_EQ(expected_reason, compilation_reason3);
-    ASSERT_EQ(expected_odex_status, odex_status3);
+    ASSERT_EQ(expected_filter, compilation_filter);
+    ASSERT_EQ(expected_reason, compilation_reason);
+    ASSERT_EQ(expected_odex_status, odex_status);
   }
 
   bool InsertNewBootClasspathEntry(const std::string& src, std::string* error_msg) {
@@ -2377,7 +2385,33 @@
   ASSERT_NE(oat_file_assistant, nullptr);
 
   // Verify that the created instance is usable.
-  VerifyOptimizationStatus(dex_location, default_context_.get(), "speed", "install", "up-to-date");
+  VerifyOptimizationStatusWithInstance(oat_file_assistant.get(), "speed", "install", "up-to-date");
+}
+
+TEST_P(OatFileAssistantTest, CreateWithNullContext) {
+  std::string dex_location = GetScratchDir() + "/OdexUpToDate.jar";
+  std::string odex_location = GetOdexDir() + "/OdexUpToDate.odex";
+  Copy(GetDexSrc1(), dex_location);
+  GenerateOdexForTest(dex_location, odex_location, CompilerFilter::kSpeed, "install");
+
+  auto scoped_maybe_without_runtime = ScopedMaybeWithoutRuntime();
+
+  std::unique_ptr<ClassLoaderContext> context;
+  std::string error_msg;
+  std::unique_ptr<OatFileAssistant> oat_file_assistant =
+      OatFileAssistant::Create(dex_location,
+                               GetInstructionSetString(kRuntimeISA),
+                               /*context_str=*/std::nullopt,
+                               /*load_executable=*/false,
+                               /*only_load_trusted_executable=*/true,
+                               MaybeGetOatFileAssistantContext(),
+                               &context,
+                               &error_msg);
+  ASSERT_NE(oat_file_assistant, nullptr);
+  ASSERT_EQ(context, nullptr);
+
+  // Verify that the created instance is usable.
+  VerifyOptimizationStatusWithInstance(oat_file_assistant.get(), "speed", "install", "up-to-date");
 }
 
 TEST_P(OatFileAssistantTest, ErrorOnInvalidIsaString) {