Handle configuration splits when creating the class loader context

Configuration splits have no dependencies which can lead to exceptions
when computing their class loader context.

In general, we do not need to compute the class loader context for apks
without code.

This CL addresses the issue by ignoring "code" paths with no actual code.

Bug: 65159159
Test: adb install-multiple config_splits
      runtest -x
services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java

Change-Id: Ida1eb901eecba4a4266de73022f6ee4659367873
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 2aa3d09..6792b6b 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -718,6 +718,10 @@
      * Cycles do not exist because they are illegal and screened for during installation.
      *
      * May be null if no splits are installed, or if no dependencies exist between them.
+     *
+     * NOTE: Any change to the way split dependencies are stored must update the logic that
+     *       creates the class loader context for dexopt (DexoptUtils#getClassLoaderContexts).
+     *
      * @hide
      */
     public SparseArray<int[]> splitDependencies;
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 698d387..0f580d8 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -147,8 +147,13 @@
         // Get the class loader context dependencies.
         // For each code path in the package, this array contains the class loader context that
         // needs to be passed to dexopt in order to ensure correct optimizations.
+        boolean[] pathsWithCode = new boolean[paths.size()];
+        pathsWithCode[0] = (pkg.applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0;
+        for (int i = 1; i < paths.size(); i++) {
+            pathsWithCode[i] = (pkg.splitFlags[i - 1] & ApplicationInfo.FLAG_HAS_CODE) != 0;
+        }
         String[] classLoaderContexts = DexoptUtils.getClassLoaderContexts(
-                pkg.applicationInfo, sharedLibraries);
+                pkg.applicationInfo, sharedLibraries, pathsWithCode);
 
         // Sanity check that we do not call dexopt with inconsistent data.
         if (paths.size() != classLoaderContexts.length) {
@@ -164,10 +169,15 @@
         int result = DEX_OPT_SKIPPED;
         for (int i = 0; i < paths.size(); i++) {
             // Skip paths that have no code.
-            if ((i == 0 && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) == 0) ||
-                    (i != 0 && (pkg.splitFlags[i - 1] & ApplicationInfo.FLAG_HAS_CODE) == 0)) {
+            if (!pathsWithCode[i]) {
                 continue;
             }
+            if (classLoaderContexts[i] == null) {
+                throw new IllegalStateException("Inconsistent information in the "
+                        + "package structure. A split is marked to contain code "
+                        + "but has no dependency listed. Index=" + i + " path=" + paths.get(i));
+            }
+
             // Append shared libraries with split dependencies for this split.
             String path = paths.get(i);
             if (options.getSplitName() != null) {
diff --git a/services/core/java/com/android/server/pm/dex/DexoptUtils.java b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
index 0196212..ef2ee4a 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptUtils.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
@@ -35,7 +35,9 @@
     /**
      * Creates the class loader context dependencies for each of the application code paths.
      * The returned array contains the class loader contexts that needs to be passed to dexopt in
-     * order to ensure correct optimizations.
+     * order to ensure correct optimizations. "Code" paths with no actual code, as specified by
+     * {@param pathsWithCode}, are ignored and will have null as their context in the returned array
+     * (configuration splits are an example of paths without code).
      *
      * A class loader context describes how the class loader chain should be built by dex2oat
      * in order to ensure that classes are resolved during compilation as they would be resolved
@@ -60,7 +62,8 @@
      * {@link android.app.LoadedApk#makePaths(
      * android.app.ActivityThread, boolean, ApplicationInfo, List, List)}.
      */
-    public static String[] getClassLoaderContexts(ApplicationInfo info, String[] sharedLibraries) {
+    public static String[] getClassLoaderContexts(ApplicationInfo info,
+            String[] sharedLibraries, boolean[] pathsWithCode) {
         // The base class loader context contains only the shared library.
         String sharedLibrariesClassPath = encodeClasspath(sharedLibraries);
         String baseApkContextClassLoader = encodeClassLoader(
@@ -86,7 +89,7 @@
         // Index 0 is the class loaded context for the base apk.
         // Index `i` is the class loader context encoding for split `i`.
         String[] classLoaderContexts = new String[/*base apk*/ 1 + splitRelativeCodePaths.length];
-        classLoaderContexts[0] = baseApkContextClassLoader;
+        classLoaderContexts[0] = pathsWithCode[0] ? baseApkContextClassLoader : null;
 
         if (!info.requestsIsolatedSplitLoading() || info.splitDependencies == null) {
             // If the app didn't request for the splits to be loaded in isolation or if it does not
@@ -94,7 +97,15 @@
             // apk class loader (in the order of their definition).
             String classpath = sharedLibrariesAndBaseClassPath;
             for (int i = 1; i < classLoaderContexts.length; i++) {
-                classLoaderContexts[i] = encodeClassLoader(classpath, info.classLoaderName);
+                classLoaderContexts[i] = pathsWithCode[i]
+                        ? encodeClassLoader(classpath, info.classLoaderName) : null;
+                // Note that the splits with no code are not removed from the classpath computation.
+                // i.e. split_n might get the split_n-1 in its classpath dependency even
+                // if split_n-1 has no code.
+                // The splits with no code do not matter for the runtime which ignores
+                // apks without code when doing the classpath checks. As such we could actually
+                // filter them but we don't do it in order to keep consistency with how the apps
+                // are loaded.
                 classpath = encodeClasspath(classpath, splitRelativeCodePaths[i - 1]);
             }
         } else {
@@ -116,9 +127,17 @@
             String splitDependencyOnBase = encodeClassLoader(
                     sharedLibrariesAndBaseClassPath, info.classLoaderName);
             SparseArray<int[]> splitDependencies = info.splitDependencies;
+
+            // Note that not all splits have dependencies (e.g. configuration splits)
+            // The splits without dependencies will have classLoaderContexts[config_split_index]
+            // set to null after this step.
             for (int i = 1; i < splitDependencies.size(); i++) {
-                getParentDependencies(splitDependencies.keyAt(i), splitClassLoaderEncodingCache,
-                        splitDependencies, classLoaderContexts, splitDependencyOnBase);
+                int splitIndex = splitDependencies.keyAt(i);
+                if (pathsWithCode[splitIndex]) {
+                    // Compute the class loader context only for the splits with code.
+                    getParentDependencies(splitIndex, splitClassLoaderEncodingCache,
+                            splitDependencies, classLoaderContexts, splitDependencyOnBase);
+                }
             }
 
             // At this point classLoaderContexts contains only the parent dependencies.
@@ -126,8 +145,17 @@
             // come first in the context.
             for (int i = 1; i < classLoaderContexts.length; i++) {
                 String splitClassLoader = encodeClassLoader("", info.splitClassLoaderNames[i - 1]);
-                classLoaderContexts[i] = encodeClassLoaderChain(
-                        splitClassLoader, classLoaderContexts[i]);
+                if (pathsWithCode[i]) {
+                    // If classLoaderContexts[i] is null it means that the split does not have
+                    // any dependency. In this case its context equals its declared class loader.
+                    classLoaderContexts[i] = classLoaderContexts[i] == null
+                            ? splitClassLoader
+                            : encodeClassLoaderChain(splitClassLoader, classLoaderContexts[i]);
+                } else {
+                    // This is a split without code, it has no dependency and it is not compiled.
+                    // Its context will be null.
+                    classLoaderContexts[i] = null;
+                }
             }
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
index 2c56a82..c8c8eb1 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
@@ -46,22 +46,35 @@
     private static final String DELEGATE_LAST_CLASS_LOADER_NAME =
             DelegateLastClassLoader.class.getName();
 
-    private ApplicationInfo createMockApplicationInfo(String baseClassLoader, boolean addSplits,
+    private static class TestData {
+        ApplicationInfo info;
+        boolean[] pathsWithCode;
+    }
+
+    private TestData createMockApplicationInfo(String baseClassLoader, boolean addSplits,
             boolean addSplitDependencies) {
         ApplicationInfo ai = new ApplicationInfo();
         String codeDir = "/data/app/mock.android.com";
         ai.setBaseCodePath(codeDir + "/base.dex");
         ai.classLoaderName = baseClassLoader;
         ai.privateFlags = ai.privateFlags | ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING;
+        boolean[] pathsWithCode;
+        if (!addSplits) {
+            pathsWithCode = new boolean[] {true};
+        } else {
+            pathsWithCode = new boolean[9];
+            Arrays.fill(pathsWithCode, true);
+            pathsWithCode[7] = false;  // config split
 
-        if (addSplits) {
             ai.setSplitCodePaths(new String[]{
                     codeDir + "/base-1.dex",
                     codeDir + "/base-2.dex",
                     codeDir + "/base-3.dex",
                     codeDir + "/base-4.dex",
                     codeDir + "/base-5.dex",
-                    codeDir + "/base-6.dex"});
+                    codeDir + "/base-6.dex",
+                    codeDir + "/config-split-7.dex",
+                    codeDir + "/feature-no-deps.dex"});
 
             ai.splitClassLoaderNames = new String[]{
                     DELEGATE_LAST_CLASS_LOADER_NAME,
@@ -69,7 +82,9 @@
                     PATH_CLASS_LOADER_NAME,
                     DEX_CLASS_LOADER_NAME,
                     PATH_CLASS_LOADER_NAME,
-                    null};  // A null class loader name should default to PathClassLoader.
+                    null,   // A null class loader name should default to PathClassLoader.
+                    null,   // The config split gets a null class loader.
+                    null};  // The feature split with no dependency and no specified class loader.
             if (addSplitDependencies) {
                 ai.splitDependencies = new SparseArray<>(ai.splitClassLoaderNames.length + 1);
                 ai.splitDependencies.put(0, new int[] {-1}); // base has no dependency
@@ -79,18 +94,24 @@
                 ai.splitDependencies.put(4, new int[] {0}); // split 4 depends on base
                 ai.splitDependencies.put(5, new int[] {0}); // split 5 depends on base
                 ai.splitDependencies.put(6, new int[] {5}); // split 6 depends on 5
+                // Do not add the config split to the dependency list.
+                // Do not add the feature split with no dependency to the dependency list.
             }
         }
-        return ai;
+        TestData data = new TestData();
+        data.info = ai;
+        data.pathsWithCode = pathsWithCode;
+        return data;
     }
 
     @Test
     public void testSplitChain() {
-        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true);
+        TestData data = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
-        assertEquals(7, contexts.length);
+        assertEquals(9, contexts.length);
         assertEquals("PCL[a.dex:b.dex]", contexts[0]);
         assertEquals("DLC[];DLC[base-2.dex];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]",
                 contexts[1]);
@@ -99,15 +120,18 @@
         assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]);
         assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]);
         assertEquals("PCL[];PCL[base-5.dex];PCL[a.dex:b.dex:base.dex]", contexts[6]);
+        assertEquals(null, contexts[7]);  // config split
+        assertEquals("PCL[]", contexts[8]);  // feature split with no dependency
     }
 
     @Test
     public void testSplitChainNoSplitDependencies() {
-        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, false);
+        TestData data = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, false);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
-        assertEquals(7, contexts.length);
+        assertEquals(9, contexts.length);
         assertEquals("PCL[a.dex:b.dex]", contexts[0]);
         assertEquals("PCL[a.dex:b.dex:base.dex]", contexts[1]);
         assertEquals("PCL[a.dex:b.dex:base.dex:base-1.dex]", contexts[2]);
@@ -119,15 +143,21 @@
         assertEquals(
                 "PCL[a.dex:b.dex:base.dex:base-1.dex:base-2.dex:base-3.dex:base-4.dex:base-5.dex]",
                 contexts[6]);
+        assertEquals(null, contexts[7]);  // config split
+        assertEquals(
+                "PCL[a.dex:b.dex:base.dex:base-1.dex:base-2.dex:base-3.dex:base-4.dex:base-5.dex:base-6.dex:config-split-7.dex]",
+                contexts[8]);  // feature split with no dependency
     }
 
     @Test
     public void testSplitChainNoIsolationNoSharedLibrary() {
-        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true);
-        ai.privateFlags = ai.privateFlags & (~ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING);
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+        TestData data = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true);
+        data.info.privateFlags = data.info.privateFlags
+                & (~ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, null, data.pathsWithCode);
 
-        assertEquals(7, contexts.length);
+        assertEquals(9, contexts.length);
         assertEquals("PCL[]", contexts[0]);
         assertEquals("PCL[base.dex]", contexts[1]);
         assertEquals("PCL[base.dex:base-1.dex]", contexts[2]);
@@ -137,14 +167,20 @@
         assertEquals(
                 "PCL[base.dex:base-1.dex:base-2.dex:base-3.dex:base-4.dex:base-5.dex]",
                 contexts[6]);
+        assertEquals(null, contexts[7]);  // config split
+        assertEquals(
+                "PCL[base.dex:base-1.dex:base-2.dex:base-3.dex:base-4.dex:base-5.dex:base-6.dex:config-split-7.dex]",
+                contexts[8]);  // feature split with no dependency
     }
+
     @Test
     public void testSplitChainNoSharedLibraries() {
-        ApplicationInfo ai = createMockApplicationInfo(
+        TestData data = createMockApplicationInfo(
                 DELEGATE_LAST_CLASS_LOADER_NAME, true, true);
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, null, data.pathsWithCode);
 
-        assertEquals(7, contexts.length);
+        assertEquals(9, contexts.length);
         assertEquals("DLC[]", contexts[0]);
         assertEquals("DLC[];DLC[base-2.dex];PCL[base-4.dex];DLC[base.dex]", contexts[1]);
         assertEquals("DLC[];PCL[base-4.dex];DLC[base.dex]", contexts[2]);
@@ -152,16 +188,19 @@
         assertEquals("PCL[];DLC[base.dex]", contexts[4]);
         assertEquals("PCL[];DLC[base.dex]", contexts[5]);
         assertEquals("PCL[];PCL[base-5.dex];DLC[base.dex]", contexts[6]);
+        assertEquals(null, contexts[7]);  // config split
+        assertEquals("PCL[]", contexts[8]);  // feature split with no dependency
     }
 
     @Test
     public void testSplitChainWithNullPrimaryClassLoader() {
         // A null classLoaderName should mean PathClassLoader.
-        ApplicationInfo ai = createMockApplicationInfo(null, true, true);
+        TestData data = createMockApplicationInfo(null, true, true);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
-        assertEquals(7, contexts.length);
+        assertEquals(9, contexts.length);
         assertEquals("PCL[a.dex:b.dex]", contexts[0]);
         assertEquals("DLC[];DLC[base-2.dex];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[1]);
         assertEquals("DLC[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[2]);
@@ -169,13 +208,16 @@
         assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]);
         assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]);
         assertEquals("PCL[];PCL[base-5.dex];PCL[a.dex:b.dex:base.dex]", contexts[6]);
+        assertEquals(null, contexts[7]);  // config split
+        assertEquals("PCL[]", contexts[8]);  // feature split with no dependency
     }
 
     @Test
     public void tesNoSplits() {
-        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
+        TestData data = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
         assertEquals(1, contexts.length);
         assertEquals("PCL[a.dex:b.dex]", contexts[0]);
@@ -183,9 +225,10 @@
 
     @Test
     public void tesNoSplitsNullClassLoaderName() {
-        ApplicationInfo ai = createMockApplicationInfo(null, false, false);
+        TestData data = createMockApplicationInfo(null, false, false);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
         assertEquals(1, contexts.length);
         assertEquals("PCL[a.dex:b.dex]", contexts[0]);
@@ -193,10 +236,11 @@
 
     @Test
     public void tesNoSplitDelegateLast() {
-        ApplicationInfo ai = createMockApplicationInfo(
+        TestData data = createMockApplicationInfo(
                 DELEGATE_LAST_CLASS_LOADER_NAME, false, false);
         String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
 
         assertEquals(1, contexts.length);
         assertEquals("DLC[a.dex:b.dex]", contexts[0]);
@@ -204,8 +248,9 @@
 
     @Test
     public void tesNoSplitsNoSharedLibraries() {
-        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+        TestData data = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, null, data.pathsWithCode);
 
         assertEquals(1, contexts.length);
         assertEquals("PCL[]", contexts[0]);
@@ -213,15 +258,55 @@
 
     @Test
     public void tesNoSplitDelegateLastNoSharedLibraries() {
-        ApplicationInfo ai = createMockApplicationInfo(
+        TestData data = createMockApplicationInfo(
                 DELEGATE_LAST_CLASS_LOADER_NAME, false, false);
-        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, null, data.pathsWithCode);
 
         assertEquals(1, contexts.length);
         assertEquals("DLC[]", contexts[0]);
     }
 
     @Test
+    public void testContextWithNoCode() {
+        TestData data = createMockApplicationInfo(null, true, false);
+        Arrays.fill(data.pathsWithCode, false);
+
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
+
+        assertEquals(9, contexts.length);
+        assertEquals(null, contexts[0]);
+        assertEquals(null, contexts[1]);
+        assertEquals(null, contexts[2]);
+        assertEquals(null, contexts[3]);
+        assertEquals(null, contexts[4]);
+        assertEquals(null, contexts[5]);
+        assertEquals(null, contexts[6]);
+        assertEquals(null, contexts[7]);
+    }
+
+    @Test
+    public void testContextBaseNoCode() {
+        TestData data = createMockApplicationInfo(null, true, true);
+        data.pathsWithCode[0] = false;
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(
+                data.info, sharedLibrary, data.pathsWithCode);
+
+        assertEquals(9, contexts.length);
+        assertEquals(null, contexts[0]);
+        assertEquals("DLC[];DLC[base-2.dex];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[1]);
+        assertEquals("DLC[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[2]);
+        assertEquals("PCL[];PCL[base-4.dex];PCL[a.dex:b.dex:base.dex]", contexts[3]);
+        assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[4]);
+        assertEquals("PCL[];PCL[a.dex:b.dex:base.dex]", contexts[5]);
+        assertEquals("PCL[];PCL[base-5.dex];PCL[a.dex:b.dex:base.dex]", contexts[6]);
+        assertEquals(null, contexts[7]);
+    }
+
+    @Test
     public void testProcessContextForDexLoad() {
         List<String> classLoaders = Arrays.asList(
                 DELEGATE_LAST_CLASS_LOADER_NAME,