*/
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,
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);
}
}
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;
}
/** @hide */
+ public static final String APK_FILE_EXTENSION = ".apk";
+
+ /** @hide */
public static class NewPermissionInfo {
public final String name;
public final int sdkVersion;
}
public static boolean isApkPath(String path) {
- return path.endsWith(".apk");
+ return path.endsWith(APK_FILE_EXTENSION);
}
/**
--- /dev/null
+/**
+ * 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);
+ }
+ }
+
+}
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;
sizeBytes += codeFile.length();
}
+ // Include raw dex metadata files
+ sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg);
+
// Include all relevant native code
sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
--- /dev/null
+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
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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 {
+}
--- /dev/null
+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
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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 {
+}
--- /dev/null
+/**
+ * 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()));
+ }
+}
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;
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;
// 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;
}
};
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.
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) {
// 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,
}
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) {
}
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) {
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
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);
+ }
}
}
}
}
/**
- * 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.
*/
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;
final PackageParser.Package pkg;
try {
pkg = pp.parsePackage(tmpPackageFile, parseFlags);
+ DexMetadataHelper.validatePackageDexMetadata(pkg);
} catch (PackageParserException e) {
res.setError("Failed parse during installPackageLI", e);
return;
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;
import libcore.io.IoUtils;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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());