Encode the entire class loader context for dex2oat

Until now the split dependencies were passed as a flatten list to dex2oat.
In the presence of DelegateLastClassLoaders this is no longer enough to
ensure the correctness of the compilation.

This CL encodes the split dependencies together with their declared class
loader in a format accepted by dex2oat.

Bug: 38138251
Test: runtest -x
services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java

Change-Id: Iaabd5d8bd5e6d027a8de5a408777fd517063d9f1
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 2c935f1..41bc7f2 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -27,12 +27,12 @@
 import android.os.WorkSource;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.DexoptOptions;
+import com.android.server.pm.dex.DexoptUtils;
 
 import java.io.File;
 import java.io.IOException;
@@ -146,12 +146,14 @@
         final boolean profileUpdated = options.isCheckForProfileUpdates() &&
                 isProfileUpdated(pkg, sharedGid, compilerFilter);
 
-        final String sharedLibrariesPath = getSharedLibrariesPath(sharedLibraries);
         // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags.
         final int dexoptFlags = getDexFlags(pkg, compilerFilter, options.isBootComplete());
-        // Get the dependencies of each split in the package. For each code path in the package,
-        // this array contains the relative paths of each split it depends on, separated by colons.
-        String[] splitDependencies = getSplitDependencies(pkg);
+
+        // 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.
+        String[] classLoaderContexts = DexoptUtils.getClassLoaderContexts(
+                pkg.applicationInfo, sharedLibraries);
 
         int result = DEX_OPT_SKIPPED;
         for (int i = 0; i < paths.size(); i++) {
@@ -170,17 +172,10 @@
                 }
             }
 
-            String sharedLibrariesPathWithSplits;
-            if (sharedLibrariesPath != null && splitDependencies[i] != null) {
-                sharedLibrariesPathWithSplits = sharedLibrariesPath + ":" + splitDependencies[i];
-            } else {
-                sharedLibrariesPathWithSplits =
-                        splitDependencies[i] != null ? splitDependencies[i] : sharedLibrariesPath;
-            }
             for (String dexCodeIsa : dexCodeInstructionSets) {
-                int newResult = dexOptPath(pkg, path, dexCodeIsa, compilerFilter, profileUpdated,
-                        sharedLibrariesPathWithSplits, dexoptFlags, sharedGid, packageStats,
-                        options.isDowngrade());
+                int newResult = dexOptPath(pkg, path, dexCodeIsa, compilerFilter,
+                        profileUpdated, classLoaderContexts[i], dexoptFlags, sharedGid,
+                        packageStats, options.isDowngrade());
                 // The end result is:
                 //  - FAILED if any path failed,
                 //  - PERFORMED if at least one path needed compilation,
@@ -449,86 +444,6 @@
     }
 
     /**
-     * Computes the shared libraries path that should be passed to dexopt.
-     */
-    private String getSharedLibrariesPath(String[] sharedLibraries) {
-        if (sharedLibraries == null || sharedLibraries.length == 0) {
-            return null;
-        }
-        StringBuilder sb = new StringBuilder();
-        for (String lib : sharedLibraries) {
-            if (sb.length() != 0) {
-                sb.append(":");
-            }
-            sb.append(lib);
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Walks dependency tree and gathers the dependencies for each split in a split apk.
-     * The split paths are stored as relative paths, separated by colons.
-     */
-    private String[] getSplitDependencies(PackageParser.Package pkg) {
-        // Convert all the code paths to relative paths.
-        String baseCodePath = new File(pkg.baseCodePath).getParent();
-        List<String> paths = pkg.getAllCodePaths();
-        String[] splitDependencies = new String[paths.size()];
-        for (int i = 0; i < paths.size(); i++) {
-            File pathFile = new File(paths.get(i));
-            String fileName = pathFile.getName();
-            paths.set(i, fileName);
-
-            // Sanity check that the base paths of the splits are all the same.
-            String basePath = pathFile.getParent();
-            if (!basePath.equals(baseCodePath)) {
-                Slog.wtf(TAG, "Split paths have different base paths: " + basePath + " and " +
-                        baseCodePath);
-            }
-        }
-
-        // If there are no other dependencies, fill in the implicit dependency on the base apk.
-        SparseArray<int[]> dependencies = pkg.applicationInfo.splitDependencies;
-        if (dependencies == null) {
-            for (int i = 1; i < paths.size(); i++) {
-                splitDependencies[i] = paths.get(0);
-            }
-            return splitDependencies;
-        }
-
-        // Fill in the dependencies, skipping the base apk which has no dependencies.
-        for (int i = 1; i < dependencies.size(); i++) {
-            getParentDependencies(dependencies.keyAt(i), paths, dependencies, splitDependencies);
-        }
-
-        return splitDependencies;
-    }
-
-    /**
-     * Recursive method to generate dependencies for a particular split.
-     * The index is a key from the package's splitDependencies.
-     */
-    private String getParentDependencies(int index, List<String> paths,
-            SparseArray<int[]> dependencies, String[] splitDependencies) {
-        // The base apk is always first, and has no dependencies.
-        if (index == 0) {
-            return null;
-        }
-        // Return the result if we've computed the dependencies for this index already.
-        if (splitDependencies[index] != null) {
-            return splitDependencies[index];
-        }
-        // Get the dependencies for the parent of this index and append its path to it.
-        int parent = dependencies.get(index)[0];
-        String parentDependencies =
-                getParentDependencies(parent, paths, dependencies, splitDependencies);
-        String path = parentDependencies == null ? paths.get(parent) :
-                parentDependencies + ":" + paths.get(parent);
-        splitDependencies[index] = path;
-        return path;
-    }
-
-    /**
      * Checks if there is an update on the profile information of the {@code pkg}.
      * If the compiler filter is not profile guided the method returns false.
      *
diff --git a/services/core/java/com/android/server/pm/dex/DexoptUtils.java b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
new file mode 100644
index 0000000..e382d8c
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2017 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.pm.dex;
+
+
+import android.content.pm.ApplicationInfo;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.os.ClassLoaderFactory;
+
+import java.io.File;
+
+public final class DexoptUtils {
+    private static final String TAG = "DexoptUtils";
+
+    private DexoptUtils() {}
+
+    /**
+     * 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.
+     *
+     * 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
+     * at runtime. The context will be encoded in the compiled code. If at runtime the dex file is
+     * loaded in a different context (with a different set of class loaders or a different
+     * classpath), the compiled code will be rejected.
+     *
+     * Note that the class loader context only includes dependencies and not the code path itself.
+     * The contexts are created based on the application split dependency list and
+     * the provided shared libraries.
+     *
+     * All the code paths encoded in the context will be relative to the base directory. This
+     * enables stage compilation where compiler artifacts may be moved around.
+     *
+     * The result is indexed as follows:
+     *   - index 0 contains the context for the base apk
+     *   - index 1 to n contain the context for the splits in the order determined by
+     *     {@code info.getSplitCodePaths()}
+     */
+    public static String[] getClassLoaderContexts(ApplicationInfo info, String[] sharedLibraries) {
+        // The base class loader context contains only the shared library.
+        String sharedLibrariesClassPath = encodeClasspath(sharedLibraries);
+        String baseApkContextClassLoader = encodeClassLoader(
+                sharedLibrariesClassPath, info.classLoaderName);
+
+        String[] splitCodePaths = info.getSplitCodePaths();
+
+        if (splitCodePaths == null) {
+            // The application has no splits.
+            return new String[] {baseApkContextClassLoader};
+        }
+
+        // The application has splits. Compute their class loader contexts.
+
+        // The splits have an implicit dependency on the base apk.
+        // This means that we have to add the base apk file in addition to the shared libraries.
+        String baseApkName = new File(info.getBaseCodePath()).getName();
+        String splitDependencyOnBase = encodeClassLoader(
+                encodeClasspath(sharedLibrariesClassPath, baseApkName),
+                info.classLoaderName);
+
+        // The result is stored in classLoaderContexts.
+        // 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 + splitCodePaths.length];
+        classLoaderContexts[0] = baseApkContextClassLoader;
+
+        SparseArray<int[]> splitDependencies = info.splitDependencies;
+
+        if (splitDependencies == null) {
+            // If there are no inter-split dependencies, populate the result with the implicit
+            // dependency on the base apk.
+            for (int i = 1; i < classLoaderContexts.length; i++) {
+                classLoaderContexts[i] = splitDependencyOnBase;
+            }
+        } else {
+            // In case of inter-split dependencies, we need to walk the dependency chain of each
+            // split. We do this recursively and store intermediate results in classLoaderContexts.
+
+            // First, look at the split class loaders and cache their individual contexts (i.e.
+            // the class loader + the name of the split). This is an optimization to avoid
+            // re-computing them during the recursive call.
+            // The cache is stored in splitClassLoaderEncodingCache. The difference between this and
+            // classLoaderContexts is that the later contains the full chain of class loaders for
+            // a given split while splitClassLoaderEncodingCache only contains a single class loader
+            // encoding.
+            String baseCodePath = new File(info.getBaseCodePath()).getParent();
+            String[] splitClassLoaderEncodingCache = new String[splitCodePaths.length];
+            for (int i = 0; i < splitCodePaths.length; i++) {
+                File pathFile = new File(splitCodePaths[i]);
+                String fileName = pathFile.getName();
+                splitClassLoaderEncodingCache[i] = encodeClassLoader(fileName,
+                        info.splitClassLoaderNames[i]);
+                // Sanity check that the base paths of the splits are all the same.
+                String basePath = pathFile.getParent();
+                if (!basePath.equals(baseCodePath)) {
+                    Slog.wtf(TAG, "Split paths have different base paths: " + basePath + " and " +
+                            baseCodePath);
+                }
+            }
+            for (int i = 1; i < splitDependencies.size(); i++) {
+                getParentDependencies(splitDependencies.keyAt(i), splitClassLoaderEncodingCache,
+                        splitDependencies, classLoaderContexts, splitDependencyOnBase);
+            }
+        }
+
+        // At this point classLoaderContexts contains only the parent dependencies.
+        // We also need to add the class loader of the current split which should
+        // 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]);
+        }
+
+        return classLoaderContexts;
+    }
+
+    /**
+     * Recursive method to generate the class loader context dependencies for the split with the
+     * given index. {@param classLoaderContexts} acts as an accumulator. Upton return
+     * {@code classLoaderContexts[index]} will contain the split dependency.
+     * During computation, the method may resolve the dependencies of other splits as it traverses
+     * the entire parent chain. The result will also be stored in {@param classLoaderContexts}.
+     *
+     * Note that {@code index 0} denotes the base apk and it is special handled. When the
+     * recursive call hits {@code index 0} the method returns {@code splitDependencyOnBase}.
+     * {@code classLoaderContexts[0]} is not modified in this method.
+     *
+     * @param index the index of the split (Note that index 0 denotes the base apk)
+     * @param splitClassLoaderEncodingCache the class loader encoding for the individual splits.
+     *    It contains only the split class loader and not the the base. The split
+     *    with {@code index} has its context at {@code splitClassLoaderEncodingCache[index - 1]}.
+     * @param splitDependencies the dependencies for all splits. Note that in this array index 0
+     *    is the base and splits start from index 1.
+     * @param classLoaderContexts the result accumulator. index 0 is the base and never set. Splits
+     *    start at index 1.
+     * @param splitDependencyOnBase the encoding of the implicit split dependency on base.
+     */
+    private static String getParentDependencies(int index, String[] splitClassLoaderEncodingCache,
+            SparseArray<int[]> splitDependencies, String[] classLoaderContexts,
+            String splitDependencyOnBase) {
+        // If we hit the base apk return its custom dependency list which is
+        // sharedLibraries + base.apk
+        if (index == 0) {
+            return splitDependencyOnBase;
+        }
+        // Return the result if we've computed the splitDependencies for this index already.
+        if (classLoaderContexts[index] != null) {
+            return classLoaderContexts[index];
+        }
+        // Get the splitDependencies for the parent of this index and append its path to it.
+        int parent = splitDependencies.get(index)[0];
+        String parentDependencies = getParentDependencies(parent, splitClassLoaderEncodingCache,
+                splitDependencies, classLoaderContexts, splitDependencyOnBase);
+
+        // The split context is: `parent context + parent dependencies context`.
+        String splitContext = (parent == 0) ?
+                parentDependencies :
+                encodeClassLoaderChain(splitClassLoaderEncodingCache[parent - 1], parentDependencies);
+        classLoaderContexts[index] = splitContext;
+        return splitContext;
+    }
+
+    /**
+     * Encodes the shared libraries classpathElements in a format accepted by dexopt.
+     * NOTE: Keep this in sync with the dexopt expectations! Right now that is
+     * a list separated by ':'.
+     */
+    private static String encodeClasspath(String[] classpathElements) {
+        if (classpathElements == null || classpathElements.length == 0) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (String element : classpathElements) {
+            if (sb.length() != 0) {
+                sb.append(":");
+            }
+            sb.append(element);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Adds an element to the encoding of an existing classpath.
+     * {@see PackageDexOptimizer.encodeClasspath(String[])}
+     */
+    private static String encodeClasspath(String classpath, String newElement) {
+        return classpath.isEmpty() ? newElement : (classpath + ":" + newElement);
+    }
+
+    /**
+     * Encodes a single class loader dependency starting from {@param path} and
+     * {@param classLoaderName}.
+     * NOTE: Keep this in sync with the dexopt expectations! Right now that is either "PCL[path]"
+     * for a PathClassLoader or "DLC[path]" for a DelegateLastClassLoader.
+     */
+    private static String encodeClassLoader(String classpath, String classLoaderName) {
+        String classLoaderDexoptEncoding = classLoaderName;
+        if (ClassLoaderFactory.isPathClassLoaderName(classLoaderName)) {
+            classLoaderDexoptEncoding = "PCL";
+        } else if (ClassLoaderFactory.isDelegateLastClassLoaderName(classLoaderName)) {
+            classLoaderDexoptEncoding = "DLC";
+        } else {
+            Slog.wtf(TAG, "Unsupported classLoaderName: " + classLoaderName);
+        }
+        return classLoaderDexoptEncoding + "[" + classpath + "]";
+    }
+
+    /**
+     * Links to dependencies together in a format accepted by dexopt.
+     * NOTE: Keep this in sync with the dexopt expectations! Right now that is a list of split
+     * dependencies {@see encodeClassLoader} separated by ';'.
+     */
+    private static String encodeClassLoaderChain(String cl1, String cl2) {
+        return cl1.isEmpty() ? cl2 : (cl1 + ";" + cl2);
+    }
+}
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
new file mode 100644
index 0000000..f81895e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptUtilsTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 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.pm.dex;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.pm.ApplicationInfo;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseArray;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.PathClassLoader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DexoptUtilsTest {
+    private static final String PATH_CLASS_LOADER_NAME = PathClassLoader.class.getName();
+    private static final String DELEGATE_LAST_CLASS_LOADER_NAME =
+            DelegateLastClassLoader.class.getName();
+
+    private ApplicationInfo 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;
+
+        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"});
+
+            ai.splitClassLoaderNames = new String[]{
+                    DELEGATE_LAST_CLASS_LOADER_NAME,
+                    DELEGATE_LAST_CLASS_LOADER_NAME,
+                    PATH_CLASS_LOADER_NAME,
+                    PATH_CLASS_LOADER_NAME,
+                    PATH_CLASS_LOADER_NAME,
+                    null};  // A null class loader name should default to PathClassLoader.
+            if (addSplitDependencies) {
+                ai.splitDependencies = new SparseArray<>(ai.splitClassLoaderNames.length + 1);
+                ai.splitDependencies.put(0, new int[] {-1}); // base has no dependency
+                ai.splitDependencies.put(1, new int[] {2}); // split 1 depends on 2
+                ai.splitDependencies.put(2, new int[] {4}); // split 2 depends on 4
+                ai.splitDependencies.put(3, new int[] {4}); // split 3 depends on 4
+                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
+            }
+        }
+        return ai;
+    }
+
+    @Test
+    public void testSplitChain() {
+        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, true);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(7, 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]);
+        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]);
+    }
+
+    @Test
+    public void testSplitChainNoSplitDependencies() {
+        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, true, false);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(7, contexts.length);
+        assertEquals("PCL[a.dex:b.dex]", contexts[0]);
+        assertEquals("DLC[];PCL[a.dex:b.dex:base.dex]", contexts[1]);
+        assertEquals("DLC[];PCL[a.dex:b.dex:base.dex]", contexts[2]);
+        assertEquals("PCL[];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[a.dex:b.dex:base.dex]", contexts[6]);
+    }
+
+    @Test
+    public void testSplitChainNoSharedLibraries() {
+        ApplicationInfo ai = createMockApplicationInfo(
+                DELEGATE_LAST_CLASS_LOADER_NAME, true, true);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+
+        assertEquals(7, 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]);
+        assertEquals("PCL[];PCL[base-4.dex];DLC[base.dex]", contexts[3]);
+        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]);
+    }
+
+    @Test
+    public void testSplitChainWithNullPrimaryClassLoader() {
+        // A null classLoaderName should mean PathClassLoader.
+        ApplicationInfo ai = createMockApplicationInfo(null, true, true);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(7, 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]);
+        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]);
+    }
+
+    @Test
+    public void tesNoSplits() {
+        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(1, contexts.length);
+        assertEquals("PCL[a.dex:b.dex]", contexts[0]);
+    }
+
+    @Test
+    public void tesNoSplitsNullClassLoaderName() {
+        ApplicationInfo ai = createMockApplicationInfo(null, false, false);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(1, contexts.length);
+        assertEquals("PCL[a.dex:b.dex]", contexts[0]);
+    }
+
+    @Test
+    public void tesNoSplitDelegateLast() {
+        ApplicationInfo ai = createMockApplicationInfo(
+                DELEGATE_LAST_CLASS_LOADER_NAME, false, false);
+        String[] sharedLibrary = new String[] {"a.dex", "b.dex"};
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, sharedLibrary);
+
+        assertEquals(1, contexts.length);
+        assertEquals("DLC[a.dex:b.dex]", contexts[0]);
+    }
+
+    @Test
+    public void tesNoSplitsNoSharedLibraries() {
+        ApplicationInfo ai = createMockApplicationInfo(PATH_CLASS_LOADER_NAME, false, false);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+
+        assertEquals(1, contexts.length);
+        assertEquals("PCL[]", contexts[0]);
+    }
+
+    @Test
+    public void tesNoSplitDelegateLastNoSharedLibraries() {
+        ApplicationInfo ai = createMockApplicationInfo(
+                DELEGATE_LAST_CLASS_LOADER_NAME, false, false);
+        String[] contexts = DexoptUtils.getClassLoaderContexts(ai, null);
+
+        assertEquals(1, contexts.length);
+        assertEquals("DLC[]", contexts[0]);
+    }
+}