Support installation of DexMetadata files (.dm)

Add support in the package installer to install dex metadata files
alongside the application apks (base or splits).

During installation or update the dex metadata files will need to have a
matching apk file. The matching is done by checking the file extension
(e.g. base.apk -> base.dm, split_a.apk -> split.dm).

On disk, the metadata files are placed next to the apks.

The .dm files will be used during install-time optimizations and passed
verbatim to dex2oat.

Test: adb shell am instrument -w \
1) adb shell am instrument -w \
  -e class android.content.pm.DexMetadataHelperTest

com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner

2) adb install-multiple CtsClassloaderSplitApp.apk
CtsClassloaderSplitApp.dm CtsClassloaderSplitAppFeatureA.apk
CtsClassloaderSplitAppFeatureA.dm CtsClassloaderSplitAppFeatureB.apk

3) gts-tradefed -m GtsAndroidRuntimeManagerHostTestCases

Bug: 30934496

Change-Id: I86f0a8307705ff3d6a5a85c2fcaae085dd62d4af
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 5f82c2a..6d2da00 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1296,6 +1296,15 @@
      */
     public static final int INSTALL_FAILED_INSTANT_APP_INVALID = -116;
 
+    /**
+     * Installation parse return code: this is passed in the
+     * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the dex metadata file is invalid or
+     * if there was no matching apk file for a dex metadata file.
+     *
+     * @hide
+     */
+    public static final int INSTALL_FAILED_BAD_DEX_METADATA = -117;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "DELETE_" }, value = {
             DELETE_KEEP_DATA,
@@ -5628,6 +5637,8 @@
             case INSTALL_FAILED_DUPLICATE_PERMISSION: return "INSTALL_FAILED_DUPLICATE_PERMISSION";
             case INSTALL_FAILED_NO_MATCHING_ABIS: return "INSTALL_FAILED_NO_MATCHING_ABIS";
             case INSTALL_FAILED_ABORTED: return "INSTALL_FAILED_ABORTED";
+            case INSTALL_FAILED_BAD_DEX_METADATA:
+                return "INSTALL_FAILED_BAD_DEX_METADATA";
             default: return Integer.toString(status);
         }
     }
@@ -5672,6 +5683,7 @@
             case INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID: return PackageInstaller.STATUS_FAILURE_INVALID;
             case INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: return PackageInstaller.STATUS_FAILURE_INVALID;
             case INSTALL_PARSE_FAILED_MANIFEST_EMPTY: return PackageInstaller.STATUS_FAILURE_INVALID;
+            case INSTALL_FAILED_BAD_DEX_METADATA: return PackageInstaller.STATUS_FAILURE_INVALID;
             case INSTALL_FAILED_INTERNAL_ERROR: return PackageInstaller.STATUS_FAILURE;
             case INSTALL_FAILED_USER_RESTRICTED: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
             case INSTALL_FAILED_DUPLICATE_PERMISSION: return PackageInstaller.STATUS_FAILURE_CONFLICT;
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 77eb57f2..71b6e43 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -247,6 +247,9 @@
     }
 
     /** @hide */
+    public static final String APK_FILE_EXTENSION = ".apk";
+
+    /** @hide */
     public static class NewPermissionInfo {
         public final String name;
         public final int sdkVersion;
@@ -621,7 +624,7 @@
     }
 
     public static boolean isApkPath(String path) {
-        return path.endsWith(".apk");
+        return path.endsWith(APK_FILE_EXTENSION);
     }
 
     /**
diff --git a/core/java/android/content/pm/dex/DexMetadataHelper.java b/core/java/android/content/pm/dex/DexMetadataHelper.java
new file mode 100644
index 0000000..c5f1c85
--- /dev/null
+++ b/core/java/android/content/pm/dex/DexMetadataHelper.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright 2018 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 android.content.pm.dex;
+
+import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA;
+import static android.content.pm.PackageParser.APK_FILE_EXTENSION;
+
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.PackageParser.PackageParserException;
+import android.util.ArrayMap;
+import android.util.jar.StrictJarFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class used to compute and validate the location of dex metadata files.
+ *
+ * @hide
+ */
+public class DexMetadataHelper {
+    private static final String DEX_METADATA_FILE_EXTENSION = ".dm";
+
+    private DexMetadataHelper() {}
+
+    /** Return true if the given file is a dex metadata file. */
+    public static boolean isDexMetadataFile(File file) {
+        return isDexMetadataPath(file.getName());
+    }
+
+    /** Return true if the given path is a dex metadata path. */
+    private static boolean isDexMetadataPath(String path) {
+        return path.endsWith(DEX_METADATA_FILE_EXTENSION);
+    }
+
+    /**
+     * Return the size (in bytes) of all dex metadata files associated with the given package.
+     */
+    public static long getPackageDexMetadataSize(PackageLite pkg) {
+        long sizeBytes = 0;
+        Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values();
+        for (String dexMetadata : dexMetadataList) {
+            sizeBytes += new File(dexMetadata).length();
+        }
+        return sizeBytes;
+    }
+
+    /**
+     * Search for the dex metadata file associated with the given target file.
+     * If it exists, the method returns the dex metadata file; otherwise it returns null.
+     *
+     * Note that this performs a loose matching suitable to be used in the InstallerSession logic.
+     * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+     * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+     */
+    public static File findDexMetadataForFile(File targetFile) {
+        String dexMetadataPath = buildDexMetadataPathForFile(targetFile);
+        File dexMetadataFile = new File(dexMetadataPath);
+        return dexMetadataFile.exists() ? dexMetadataFile : null;
+    }
+
+    /**
+     * Return the dex metadata files for the given package as a map
+     * [code path -> dex metadata path].
+     *
+     * NOTE: involves I/O checks.
+     */
+    public static Map<String, String> getPackageDexMetadata(PackageParser.Package pkg) {
+        return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+    }
+
+    /**
+     * Return the dex metadata files for the given package as a map
+     * [code path -> dex metadata path].
+     *
+     * NOTE: involves I/O checks.
+     */
+    private static Map<String, String> getPackageDexMetadata(PackageLite pkg) {
+        return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+    }
+
+    /**
+     * Look up the dex metadata files for the given code paths building the map
+     * [code path -> dex metadata].
+     *
+     * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists.
+     * If it does it adds the pair to the returned map.
+     *
+     * Note that this method will do a strict
+     * matching based on the extension ('foo.dm' will only match 'foo.apk').
+     *
+     * This should only be used for code paths extracted from a package structure after the naming
+     * was enforced in the installer.
+     */
+    private static Map<String, String> buildPackageApkToDexMetadataMap(
+            List<String> codePaths) {
+        ArrayMap<String, String> result = new ArrayMap<>();
+        for (int i = codePaths.size() - 1; i >= 0; i--) {
+            String codePath = codePaths.get(i);
+            String dexMetadataPath = buildDexMetadataPathForApk(codePath);
+
+            if (Files.exists(Paths.get(dexMetadataPath))) {
+                result.put(codePath, dexMetadataPath);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Return the dex metadata path associated with the given code path.
+     * (replaces '.apk' extension with '.dm')
+     *
+     * @throws IllegalArgumentException if the code path is not an .apk.
+     */
+    public static String buildDexMetadataPathForApk(String codePath) {
+        if (!PackageParser.isApkPath(codePath)) {
+            throw new IllegalStateException(
+                    "Corrupted package. Code path is not an apk " + codePath);
+        }
+        return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length())
+                + DEX_METADATA_FILE_EXTENSION;
+    }
+
+    /**
+     * Return the dex metadata path corresponding to the given {@code targetFile} using a loose
+     * matching.
+     * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+     * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+     */
+    private static String buildDexMetadataPathForFile(File targetFile) {
+        return PackageParser.isApkFile(targetFile)
+                ? buildDexMetadataPathForApk(targetFile.getPath())
+                : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION;
+    }
+
+    /**
+     * Validate the dex metadata files installed for the given package.
+     *
+     * @throws PackageParserException in case of errors.
+     */
+    public static void validatePackageDexMetadata(PackageParser.Package pkg)
+            throws PackageParserException {
+        Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values();
+        for (String dexMetadata : apkToDexMetadataList) {
+            validateDexMetadataFile(dexMetadata);
+        }
+    }
+
+    /**
+     * Validate that the given file is a dex metadata archive.
+     * This is just a sanity validation that the file is a zip archive.
+     *
+     * @throws PackageParserException if the file is not a .dm file.
+     */
+    private static void validateDexMetadataFile(String dmaPath) throws PackageParserException {
+        StrictJarFile jarFile = null;
+        try {
+            jarFile = new StrictJarFile(dmaPath, false, false);
+        } catch (IOException e) {
+            throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA,
+                    "Error opening " + dmaPath, e);
+        } finally {
+            if (jarFile != null) {
+                try {
+                    jarFile.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Validates that all dex metadata paths in the given list have a matching apk.
+     * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file).
+     * If that's not the case it throws {@code IllegalStateException}.
+     *
+     * This is used to perform a basic sanity check during adb install commands.
+     * (The installer does not support stand alone .dm files)
+     */
+    public static void validateDexPaths(String[] paths) {
+        ArrayList<String> apks = new ArrayList<>();
+        for (int i = 0; i < paths.length; i++) {
+            if (PackageParser.isApkPath(paths[i])) {
+                apks.add(paths[i]);
+            }
+        }
+        ArrayList<String> unmatchedDmFiles = new ArrayList<>();
+        for (int i = 0; i < paths.length; i++) {
+            String dmPath = paths[i];
+            if (isDexMetadataPath(dmPath)) {
+                boolean valid = false;
+                for (int j = apks.size() - 1; j >= 0; j--) {
+                    if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) {
+                        valid = true;
+                        break;
+                    }
+                }
+                if (!valid) {
+                    unmatchedDmFiles.add(dmPath);
+                }
+            }
+        }
+        if (!unmatchedDmFiles.isEmpty()) {
+            throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles);
+        }
+    }
+
+}
diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java
index e765ab1..8a456d1 100644
--- a/core/java/com/android/internal/content/PackageHelper.java
+++ b/core/java/com/android/internal/content/PackageHelper.java
@@ -25,6 +25,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.dex.DexMetadataHelper;
 import android.os.Environment;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -415,6 +416,9 @@
             sizeBytes += codeFile.length();
         }
 
+        // Include raw dex metadata files
+        sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg);
+
         // Include all relevant native code
         sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
 
diff --git a/core/tests/coretests/apks/install-split-base/Android.mk b/core/tests/coretests/apks/install-split-base/Android.mk
new file mode 100644
index 0000000..5b60e31
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-base/Android.mk
@@ -0,0 +1,10 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := install_split_base
+
+include $(FrameworkCoreTests_BUILD_PACKAGE)
\ No newline at end of file
diff --git a/core/tests/coretests/apks/install-split-base/AndroidManifest.xml b/core/tests/coretests/apks/install-split-base/AndroidManifest.xml
new file mode 100644
index 0000000..c2bfedd
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-base/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.coretests.install_split"
+        android:isolatedSplits="true">
+
+    <application android:label="ClassloaderSplitApp">
+        <activity android:name=".BaseActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/core/tests/coretests/apks/install-split-base/src/com/google/android/dexapis/splitapp/BaseActivity.java b/core/tests/coretests/apks/install-split-base/src/com/google/android/dexapis/splitapp/BaseActivity.java
new file mode 100644
index 0000000..cb5760ce
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-base/src/com/google/android/dexapis/splitapp/BaseActivity.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2018 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.google.android.dexapis.splitapp;
+
+import android.app.Activity;
+
+/** Main activity */
+public class BaseActivity extends Activity {
+}
diff --git a/core/tests/coretests/apks/install-split-feature-a/Android.mk b/core/tests/coretests/apks/install-split-feature-a/Android.mk
new file mode 100644
index 0000000..0f37d16
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-feature-a/Android.mk
@@ -0,0 +1,14 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := install_split_feature_a
+
+LOCAL_USE_AAPT2 := true
+LOCAL_AAPT_FLAGS += --custom-package com.google.android.dexapis.splitapp.feature_a
+LOCAL_AAPT_FLAGS += --package-id 0x80
+
+include $(FrameworkCoreTests_BUILD_PACKAGE)
\ No newline at end of file
diff --git a/core/tests/coretests/apks/install-split-feature-a/AndroidManifest.xml b/core/tests/coretests/apks/install-split-feature-a/AndroidManifest.xml
new file mode 100644
index 0000000..3221c75
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-feature-a/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.coretests.install_split"
+        featureSplit="feature_a">
+
+    <application>
+        <activity android:name=".feature_a.FeatureAActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/core/tests/coretests/apks/install-split-feature-a/src/com/google/android/dexapis/splitapp/feature_a/FeatureAActivity.java b/core/tests/coretests/apks/install-split-feature-a/src/com/google/android/dexapis/splitapp/feature_a/FeatureAActivity.java
new file mode 100644
index 0000000..0af5f89
--- /dev/null
+++ b/core/tests/coretests/apks/install-split-feature-a/src/com/google/android/dexapis/splitapp/feature_a/FeatureAActivity.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2018 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.google.android.dexapis.splitapp.feature_a;
+
+import android.app.Activity;
+
+/** Main activity */
+public class FeatureAActivity extends Activity {
+}
diff --git a/core/tests/coretests/src/android/content/pm/dex/DexMetadataHelperTest.java b/core/tests/coretests/src/android/content/pm/dex/DexMetadataHelperTest.java
new file mode 100644
index 0000000..4b84429
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/dex/DexMetadataHelperTest.java
@@ -0,0 +1,208 @@
+/**
+ * Copyright (C) 2018 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 android.content.pm.dex;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.Package;
+import android.content.pm.PackageParser.PackageParserException;
+import android.os.FileUtils;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.frameworks.coretests.R;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import libcore.io.IoUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DexMetadataHelperTest {
+    private static final String APK_FILE_EXTENSION = ".apk";
+    private static final String DEX_METADATA_FILE_EXTENSION = ".dm";
+
+    private File mTmpDir = null;
+
+    @Before
+    public void setUp() {
+        mTmpDir = IoUtils.createTemporaryDirectory("DexMetadataHelperTest");
+    }
+
+    @After
+    public void tearDown() {
+        if (mTmpDir != null) {
+            File[] files = mTmpDir.listFiles();
+            for (File f : files) {
+                f.delete();
+            }
+        }
+    }
+
+    private File createDexMetadataFile(String apkFileName) throws IOException {
+        File dmFile = new File(mTmpDir, apkFileName.replace(APK_FILE_EXTENSION,
+                DEX_METADATA_FILE_EXTENSION));
+        try (FileOutputStream fos = new FileOutputStream(dmFile)) {
+            try (ZipOutputStream zipOs = new ZipOutputStream(fos)) {
+                zipOs.putNextEntry(new ZipEntry("primary.prof"));
+                zipOs.closeEntry();
+            }
+        }
+        return dmFile;
+    }
+
+    private File copyApkToToTmpDir(String apkFileName, int apkResourceId) throws IOException {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        File outFile = new File(mTmpDir, apkFileName);
+        try (InputStream is = context.getResources().openRawResource(apkResourceId)) {
+            FileUtils.copyToFileOrThrow(is, outFile);
+        }
+        return outFile;
+    }
+
+    @Test
+    public void testParsePackageWithDmFileValid() throws IOException, PackageParserException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        createDexMetadataFile("install_split_base.apk");
+        Package pkg = new PackageParser().parsePackage(mTmpDir, 0 /* flags */);
+
+        Map<String, String> packageDexMetadata = DexMetadataHelper.getPackageDexMetadata(pkg);
+        assertEquals(1, packageDexMetadata.size());
+        String baseDexMetadata = packageDexMetadata.get(pkg.baseCodePath);
+        assertNotNull(baseDexMetadata);
+        assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.baseCodePath));
+    }
+
+    @Test
+    public void testParsePackageSplitsWithDmFileValid()
+            throws IOException, PackageParserException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
+        createDexMetadataFile("install_split_base.apk");
+        createDexMetadataFile("install_split_feature_a.apk");
+        Package pkg = new PackageParser().parsePackage(mTmpDir, 0 /* flags */);
+
+        Map<String, String> packageDexMetadata = DexMetadataHelper.getPackageDexMetadata(pkg);
+        assertEquals(2, packageDexMetadata.size());
+        String baseDexMetadata = packageDexMetadata.get(pkg.baseCodePath);
+        assertNotNull(baseDexMetadata);
+        assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.baseCodePath));
+
+        String splitDexMetadata = packageDexMetadata.get(pkg.splitCodePaths[0]);
+        assertNotNull(splitDexMetadata);
+        assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.splitCodePaths[0]));
+    }
+
+    @Test
+    public void testParsePackageSplitsNoBaseWithDmFileValid()
+            throws IOException, PackageParserException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
+        createDexMetadataFile("install_split_feature_a.apk");
+        Package pkg = new PackageParser().parsePackage(mTmpDir, 0 /* flags */);
+
+        Map<String, String> packageDexMetadata = DexMetadataHelper.getPackageDexMetadata(pkg);
+        assertEquals(1, packageDexMetadata.size());
+
+        String splitDexMetadata = packageDexMetadata.get(pkg.splitCodePaths[0]);
+        assertNotNull(splitDexMetadata);
+        assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.splitCodePaths[0]));
+    }
+
+    @Test
+    public void testParsePackageWithDmFileInvalid() throws IOException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        File invalidDmFile = new File(mTmpDir, "install_split_base.dm");
+        Files.createFile(invalidDmFile.toPath());
+        try {
+            PackageParser.Package pkg = new PackageParser().parsePackage(mTmpDir, 0 /* flags */);
+            DexMetadataHelper.validatePackageDexMetadata(pkg);
+        } catch (PackageParserException e) {
+            assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA);
+        }
+    }
+
+    @Test
+    public void testParsePackageSplitsWithDmFileInvalid()
+            throws IOException, PackageParserException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
+        createDexMetadataFile("install_split_base.apk");
+        File invalidDmFile = new File(mTmpDir, "install_split_feature_a.dm");
+        Files.createFile(invalidDmFile.toPath());
+
+        try {
+            PackageParser.Package pkg = new PackageParser().parsePackage(mTmpDir, 0 /* flags */);
+            DexMetadataHelper.validatePackageDexMetadata(pkg);
+        } catch (PackageParserException e) {
+            assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA);
+        }
+    }
+
+    @Test
+    public void testPackageWithDmFileNoMatch() throws IOException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        createDexMetadataFile("non_existent.apk");
+
+        try {
+            DexMetadataHelper.validateDexPaths(mTmpDir.list());
+            fail("Should fail validation");
+        } catch (IllegalStateException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testPackageSplitsWithDmFileNoMatch()
+            throws IOException, PackageParserException {
+        copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
+        copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
+        createDexMetadataFile("install_split_base.apk");
+        createDexMetadataFile("install_split_feature_a.mistake.apk");
+
+        try {
+            DexMetadataHelper.validateDexPaths(mTmpDir.list());
+            fail("Should fail validation");
+        } catch (IllegalStateException e) {
+            // expected.
+        }
+    }
+
+    private static boolean isDexMetadataForApk(String dmaPath, String apkPath) {
+        return apkPath.substring(0, apkPath.length() - APK_FILE_EXTENSION.length()).equals(
+                dmaPath.substring(0, dmaPath.length() - DEX_METADATA_FILE_EXTENSION.length()));
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 5577de8..087b6a3 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -17,10 +17,12 @@
 package com.android.server.pm;
 
 import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
+import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA;
 import static android.content.pm.PackageManager.INSTALL_FAILED_CONTAINER_ERROR;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
+import static android.content.pm.PackageParser.APK_FILE_EXTENSION;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_WRONLY;
@@ -96,6 +98,7 @@
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapter;
 
+import android.content.pm.dex.DexMetadataHelper;
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -261,6 +264,7 @@
             // entries like "lost+found".
             if (file.isDirectory()) return false;
             if (file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false;
+            if (DexMetadataHelper.isDexMetadataFile(file)) return false;
             return true;
         }
     };
@@ -941,6 +945,15 @@
                 mInstallerPackageName, mInstallerUid, user, mCertificates);
     }
 
+    private static void maybeRenameFile(File from, File to) throws PackageManagerException {
+        if (!from.equals(to)) {
+            if (!from.renameTo(to)) {
+                throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
+                        "Could not rename file " + from + " to " + to);
+            }
+        }
+    }
+
     /**
      * Validate install by confirming that all application packages are have
      * consistent package name, version code, and signing certificates.
@@ -985,6 +998,7 @@
         if (ArrayUtils.isEmpty(addedFiles) && removeSplitList.size() == 0) {
             throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged");
         }
+
         // Verify that all staged packages are internally consistent
         final ArraySet<String> stagedSplits = new ArraySet<>();
         for (File addedFile : addedFiles) {
@@ -1019,9 +1033,9 @@
             // Take this opportunity to enforce uniform naming
             final String targetName;
             if (apk.splitName == null) {
-                targetName = "base.apk";
+                targetName = "base" + APK_FILE_EXTENSION;
             } else {
-                targetName = "split_" + apk.splitName + ".apk";
+                targetName = "split_" + apk.splitName + APK_FILE_EXTENSION;
             }
             if (!FileUtils.isValidExtFilename(targetName)) {
                 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
@@ -1029,9 +1043,7 @@
             }
 
             final File targetFile = new File(mResolvedStageDir, targetName);
-            if (!addedFile.equals(targetFile)) {
-                addedFile.renameTo(targetFile);
-            }
+            maybeRenameFile(addedFile, targetFile);
 
             // Base is coming from session
             if (apk.splitName == null) {
@@ -1039,6 +1051,18 @@
             }
 
             mResolvedStagedFiles.add(targetFile);
+
+            final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(addedFile);
+            if (dexMetadataFile != null) {
+                if (!FileUtils.isValidExtFilename(dexMetadataFile.getName())) {
+                    throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+                            "Invalid filename: " + dexMetadataFile);
+                }
+                final File targetDexMetadataFile = new File(mResolvedStageDir,
+                        DexMetadataHelper.buildDexMetadataPathForApk(targetName));
+                mResolvedStagedFiles.add(targetDexMetadataFile);
+                maybeRenameFile(dexMetadataFile, targetDexMetadataFile);
+            }
         }
 
         if (removeSplitList.size() > 0) {
@@ -1096,6 +1120,12 @@
             if (mResolvedBaseFile == null) {
                 mResolvedBaseFile = new File(appInfo.getBaseCodePath());
                 mResolvedInheritedFiles.add(mResolvedBaseFile);
+                // Inherit the dex metadata if present.
+                final File baseDexMetadataFile =
+                        DexMetadataHelper.findDexMetadataForFile(mResolvedBaseFile);
+                if (baseDexMetadataFile != null) {
+                    mResolvedInheritedFiles.add(baseDexMetadataFile);
+                }
             }
 
             // Inherit splits if not overridden
@@ -1106,6 +1136,12 @@
                     final boolean splitRemoved = removeSplitList.contains(splitName);
                     if (!stagedSplits.contains(splitName) && !splitRemoved) {
                         mResolvedInheritedFiles.add(splitFile);
+                        // Inherit the dex metadata if present.
+                        final File splitDexMetadataFile =
+                                DexMetadataHelper.findDexMetadataForFile(splitFile);
+                        if (splitDexMetadataFile != null) {
+                            mResolvedInheritedFiles.add(splitDexMetadataFile);
+                        }
                     }
                 }
             }
@@ -1162,43 +1198,6 @@
     }
 
     /**
-     * Calculate the final install footprint size, combining both staged and
-     * existing APKs together and including unpacked native code from both.
-     */
-    private long calculateInstalledSize() throws PackageManagerException {
-        Preconditions.checkNotNull(mResolvedBaseFile);
-
-        final ApkLite baseApk;
-        try {
-            baseApk = PackageParser.parseApkLite(mResolvedBaseFile, 0);
-        } catch (PackageParserException e) {
-            throw PackageManagerException.from(e);
-        }
-
-        final List<String> splitPaths = new ArrayList<>();
-        for (File file : mResolvedStagedFiles) {
-            if (mResolvedBaseFile.equals(file)) continue;
-            splitPaths.add(file.getAbsolutePath());
-        }
-        for (File file : mResolvedInheritedFiles) {
-            if (mResolvedBaseFile.equals(file)) continue;
-            splitPaths.add(file.getAbsolutePath());
-        }
-
-        // This is kind of hacky; we're creating a half-parsed package that is
-        // straddled between the inherited and staged APKs.
-        final PackageLite pkg = new PackageLite(null, baseApk, null, null, null, null,
-                splitPaths.toArray(new String[splitPaths.size()]), null);
-
-        try {
-            return PackageHelper.calculateInstalledSize(pkg, params.abiOverride);
-        } catch (IOException e) {
-            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
-                    "Failed to calculate install size", e);
-        }
-    }
-
-    /**
      * Determine if creating hard links between source and destination is
      * possible. That is, do they all live on the same underlying device.
      */
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 44aad44..29069f1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -190,6 +190,7 @@
 import android.content.pm.VerifierDeviceIdentity;
 import android.content.pm.VerifierInfo;
 import android.content.pm.VersionedPackage;
+import android.content.pm.dex.DexMetadataHelper;
 import android.content.pm.dex.IArtManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -16395,6 +16396,7 @@
         final PackageParser.Package pkg;
         try {
             pkg = pp.parsePackage(tmpPackageFile, parseFlags);
+            DexMetadataHelper.validatePackageDexMetadata(pkg);
         } catch (PackageParserException e) {
             res.setError("Failed parse during installPackageLI", e);
             return;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 2d82c46..28d78a4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -46,6 +46,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.content.pm.VersionedPackage;
+import android.content.pm.dex.DexMetadataHelper;
 import android.content.res.AssetManager;
 import android.content.res.Resources;
 import android.net.Uri;
@@ -78,7 +79,6 @@
 
 import libcore.io.IoUtils;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -2221,6 +2221,14 @@
             session = new PackageInstaller.Session(
                     mInterface.getPackageInstaller().openSession(sessionId));
 
+            // Sanity check that all .dm files match an apk.
+            // (The installer does not support standalone .dm files and will not process them.)
+            try {
+                DexMetadataHelper.validateDexPaths(session.getNames());
+            } catch (IllegalStateException | IOException e) {
+                pw.println("Warning [Could not validate the dex paths: " + e.getMessage() + "]");
+            }
+
             final LocalIntentReceiver receiver = new LocalIntentReceiver();
             session.commit(receiver.getIntentSender());