Add support for Android 12 profile format.
* Add support for reading METADATA_002 format to capture `typeIdCount`.
* Add the ability to merge the new profile metadata correctly.
Test: Added end to end transcode tests for Android 12.
Bug: b/205741998
Change-Id: I505b1ff80c52d7dda5d05751ff14e87275b0f165
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 5b50458..79c5087 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -180,6 +180,7 @@
mProfile = ProfileTranscoder.readMeta(
is,
metaVersion,
+ mDesiredVersion,
profile
);
return this;
@@ -304,6 +305,9 @@
case Build.VERSION_CODES.R:
return ProfileVersion.V010_P;
+ case Build.VERSION_CODES.S:
+ return ProfileVersion.V015_S;
+
default:
return null;
}
@@ -330,6 +334,12 @@
case Build.VERSION_CODES.P:
case Build.VERSION_CODES.Q:
case Build.VERSION_CODES.R:
+ return false;
+
+ // The profiles for S require a typeIdCount. Therefore metadata is required.
+ case Build.VERSION_CODES.S:
+ return true;
+
default:
return false;
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DexProfileData.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DexProfileData.java
index 840dc13..91b18dd 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DexProfileData.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DexProfileData.java
@@ -26,6 +26,7 @@
@NonNull
final String dexName;
final long dexChecksum;
+ long mTypeIdCount;
int classSetSize;
final int hotMethodRegionSize;
final int numMethodIds;
@@ -37,6 +38,7 @@
@NonNull String apkName,
@NonNull String dexName,
long dexChecksum,
+ long typeIdCount,
int classSetSize,
int hotMethodRegionSize,
int numMethodIds,
@@ -46,6 +48,7 @@
this.apkName = apkName;
this.dexName = dexName;
this.dexChecksum = dexChecksum;
+ this.mTypeIdCount = typeIdCount;
this.classSetSize = classSetSize;
this.hotMethodRegionSize = hotMethodRegionSize;
this.numMethodIds = numMethodIds;
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
index bdb7263..91d47bb 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
@@ -168,18 +168,20 @@
static void writeCompressed(@NonNull OutputStream os, byte[] data) throws IOException {
writeUInt32(os, data.length); // uncompressed size
- Deflater deflater = new Deflater(Deflater.BEST_SPEED);
- try {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- try (DeflaterOutputStream dos = new DeflaterOutputStream(bos, deflater)) {
- dos.write(data);
- }
- byte[] outputData = bos.toByteArray();
- writeUInt32(os, outputData.length); // compressed size
- os.write(outputData); // compressed body
+ byte[] outputData = compress(data);
+ writeUInt32(os, outputData.length); // compressed size
+ os.write(outputData); // compressed body
+ }
+
+ static byte[] compress(@NonNull byte[] data) throws IOException {
+ Deflater compressor = new Deflater(Deflater.BEST_SPEED);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (DeflaterOutputStream deflater = new DeflaterOutputStream(out, compressor)) {
+ deflater.write(data);
} finally {
- deflater.end();
+ compressor.end();
}
+ return out.toByteArray();
}
static void writeAll(@NonNull InputStream is, @NonNull OutputStream os) throws IOException {
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/FileSectionType.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/FileSectionType.java
new file mode 100644
index 0000000..e1bb52f
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/FileSectionType.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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 androidx.profileinstaller;
+
+/**
+ * A list of profile file section types for Android S.
+ */
+enum FileSectionType {
+ /** Represents a dex file section. This is a required file section type. */
+ DEX_FILES(0L),
+
+ /**
+ * Optional file sections. The only ones we care about are CLASSES and METHODS.
+ * Listing EXTRA_DESCRIPTORS & AGGREGATION_COUNT for completeness.
+ */
+ EXTRA_DESCRIPTORS(1L),
+ CLASSES(2L),
+ METHODS(3L),
+ AGGREGATION_COUNT(4L);
+
+ private final long mValue;
+
+ FileSectionType(long value) {
+ this.mValue = value;
+ }
+
+ public long getValue() {
+ return mValue;
+ }
+
+ static FileSectionType fromValue(long value) {
+ FileSectionType[] values = FileSectionType.values();
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].getValue() == value) {
+ return values[i];
+ }
+ }
+ throw new IllegalArgumentException("Unsupported FileSection Type " + value);
+ }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileTranscoder.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileTranscoder.java
index 8dccf9b..5242052 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileTranscoder.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileTranscoder.java
@@ -19,7 +19,9 @@
import static androidx.profileinstaller.Encoding.SIZEOF_BYTE;
import static androidx.profileinstaller.Encoding.UINT_16_SIZE;
import static androidx.profileinstaller.Encoding.UINT_32_SIZE;
+import static androidx.profileinstaller.Encoding.UINT_8_SIZE;
import static androidx.profileinstaller.Encoding.bitsToBytes;
+import static androidx.profileinstaller.Encoding.compress;
import static androidx.profileinstaller.Encoding.error;
import static androidx.profileinstaller.Encoding.read;
import static androidx.profileinstaller.Encoding.readCompressed;
@@ -35,6 +37,7 @@
import static androidx.profileinstaller.Encoding.writeUInt8;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.io.ByteArrayInputStream;
@@ -42,13 +45,12 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
-import java.util.TreeSet;
@RequiresApi(19)
class ProfileTranscoder {
@@ -93,6 +95,11 @@
@NonNull byte[] desiredVersion,
@NonNull DexProfileData[] data
) throws IOException {
+ if (Arrays.equals(desiredVersion, ProfileVersion.V015_S)) {
+ writeProfileForS(os, data);
+ return true;
+ }
+
if (Arrays.equals(desiredVersion, ProfileVersion.V010_P)) {
writeProfileForP(os, data);
return true;
@@ -155,6 +162,276 @@
}
/**
+ * Writes the provided [lines] out into a binary profile suitable for S devices. This
+ * method expects that the MAGIC and Version of the profile header have already been written
+ * to the OutputStream.
+ *
+ * This format has the following encoding:
+ *
+ * The file starts with a header and section information:
+ * FileHeader
+ * FileSectionInfo[]
+ * The first FileSectionInfo must be for the DexFiles section.
+ *
+ * The rest of the file is allowed to contain different sections in any order,
+ * at arbitrary offsets, with any gaps between them and each section can be
+ * either plaintext or separately zipped. However, we're writing sections
+ * without any gaps with the following order and compression:
+ * DexFiles - mandatory, plaintext
+ * ExtraDescriptors - optional, zipped
+ * Classes - optional, zipped
+ * Methods - optional, zipped
+ * AggregationCounts - optional, zipped, server-side
+ *
+ * DexFiles:
+ * number_of_dex_files
+ * (checksum,num_type_ids,num_method_ids,profile_key)[number_of_dex_files]
+ * where `profile_key` is a length-prefixed string, the length is `uint16_t`.
+ *
+ * ExtraDescriptors:
+ * number_of_extra_descriptors
+ * (extra_descriptor)[number_of_extra_descriptors]
+ * where `extra_descriptor` is a length-prefixed string, the length is `uint16_t`.
+ *
+ * Classes section contains records for any number of dex files, each consisting of:
+ * profile_index // Index of the dex file in DexFiles section.
+ * number_of_classes
+ * type_index_diff[number_of_classes]
+ * where instead of storing plain sorted type indexes, we store their differences
+ * as smaller numbers are likely to compress better.
+ *
+ * Methods section contains records for any number of dex files, each consisting of:
+ * profile_index // Index of the dex file in DexFiles section.
+ * following_data_size // For easy skipping of remaining data when dex file is filtered out.
+ * method_flags
+ * bitmap_data
+ * method_encoding[] // Until the size indicated by `following_data_size`.
+ * where `method_flags` is a union of flags recorded for methods in the referenced dex file,
+ * `bitmap_data` contains `num_method_ids` bits for each bit set in `method_flags` other
+ * than "hot" (the size of `bitmap_data` is rounded up to whole bytes) and `method_encoding[]`
+ * contains data for hot methods. The `method_encoding` is:
+ * method_index_diff
+ * number_of_inline_caches
+ * inline_cache_encoding[number_of_inline_caches]
+ * where differences in method indexes are used for better compression,
+ * and the `inline_cache_encoding` is
+ * dex_pc
+ * (M|dex_map_size)
+ * type_index_diff[dex_map_size]
+ * where `M` stands for special encodings indicating missing types (kIsMissingTypesEncoding)
+ * or memamorphic call (kIsMegamorphicEncoding) which both imply `dex_map_size == 0`.
+ */
+ private static void writeProfileForS(
+ @NonNull OutputStream os,
+ @NonNull DexProfileData[] profileData
+ ) throws IOException {
+ writeProfileSections(os, profileData);
+ }
+
+ private static void writeProfileSections(
+ @NonNull OutputStream os,
+ @NonNull DexProfileData[] profileData
+ ) throws IOException {
+ // 3 Sections
+ // Dex, Classes and Methods
+ List<WritableFileSection> sections = new ArrayList<>(3);
+ List<byte[]> sectionContents = new ArrayList<>(3);
+ sections.add(writeDexFileSection(profileData));
+ sections.add(createCompressibleClassSection(profileData));
+ sections.add(createCompressibleMethodsSection(profileData));
+ // We already wrote the version + magic
+ // https://errorprone.info/bugpattern/IntLongMath
+ long offset = (long) ProfileVersion.V015_S.length + MAGIC_PROF.length;
+ // Number of sections
+ offset += UINT_32_SIZE;
+ // (section type, offset, size, inflate size) per section
+ offset += (4 * UINT_32_SIZE) * sections.size();
+ writeUInt32(os, sections.size());
+ for (int i = 0; i < sections.size(); i++) {
+ WritableFileSection section = sections.get(i);
+ // File Section Type
+ writeUInt32(os, section.mType.getValue());
+ // Compute contents, and keep track of next content offset
+ writeUInt32(os, offset);
+ // Compute Next Offset based on Contents
+ if (section.mNeedsCompression) {
+ long inflatedSize = section.mContents.length;
+ byte[] compressed = compress(section.mContents);
+ sectionContents.add(compressed);
+ // Size
+ writeUInt32(os, compressed.length);
+ // Inflated Size
+ writeUInt32(os, inflatedSize);
+ offset += compressed.length;
+ } else {
+ sectionContents.add(section.mContents);
+ // Size
+ writeUInt32(os, section.mContents.length);
+ // Inflated Size (0L represents uncompressed)
+ writeUInt32(os, 0L);
+ offset += section.mContents.length;
+ }
+ }
+ // Write contents
+ for (int i = 0; i < sectionContents.size(); i++) {
+ os.write(sectionContents.get(i));
+ }
+ }
+
+ private static WritableFileSection writeDexFileSection(
+ @NonNull DexProfileData[] profileData
+ ) throws IOException {
+ int expectedSize = 0;
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ // Number of Dex files
+ expectedSize += UINT_16_SIZE;
+ writeUInt16(out, profileData.length);
+ for (int i = 0; i < profileData.length; i++) {
+ DexProfileData profile = profileData[i];
+ // Checksum
+ expectedSize += UINT_32_SIZE;
+ writeUInt32(out, profile.dexChecksum);
+ // Number of type ids
+ expectedSize += UINT_32_SIZE;
+ // This is information we may not have.
+ // For this to be a valid profile, the data should have been merged with
+ // METADATA_0_0_2.
+ writeUInt32(out, profile.mTypeIdCount);
+ // Number of method ids
+ expectedSize += UINT_32_SIZE;
+ writeUInt32(out, profile.numMethodIds);
+ // Profile Key
+ String profileKey = generateDexKey(
+ profile.apkName,
+ profile.dexName,
+ ProfileVersion.V015_S
+ );
+ expectedSize += UINT_16_SIZE;
+ int keyLength = utf8Length(profileKey);
+ writeUInt16(out, keyLength);
+ expectedSize += keyLength * UINT_8_SIZE;
+ writeString(out, profileKey);
+ }
+ byte[] contents = out.toByteArray();
+ if (expectedSize != contents.length) {
+ throw error(
+ "Expected size " + expectedSize + ", does not match actual size "
+ + contents.length
+ );
+ }
+ return new WritableFileSection(
+ FileSectionType.DEX_FILES,
+ expectedSize,
+ contents,
+ false /* needsCompression */
+ );
+ }
+ }
+
+ private static WritableFileSection createCompressibleClassSection(
+ @NonNull DexProfileData[] profileData
+ ) throws IOException {
+ int expectedSize = 0;
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ for (int i = 0; i < profileData.length; i++) {
+ DexProfileData profile = profileData[i];
+ // Profile Index
+ expectedSize += UINT_16_SIZE;
+ writeUInt16(out, i);
+ // Number of classes
+ expectedSize += UINT_16_SIZE;
+ writeUInt16(out, profile.classSetSize);
+ // Class Indexes
+ expectedSize += UINT_16_SIZE * profile.classSetSize;
+ writeClasses(out, profile);
+ }
+ byte[] contents = out.toByteArray();
+ if (expectedSize != contents.length) {
+ throw error(
+ "Expected size " + expectedSize + ", does not match actual size "
+ + contents.length
+ );
+ }
+ return new WritableFileSection(
+ FileSectionType.CLASSES,
+ expectedSize,
+ contents,
+ true /* needsCompression */
+ );
+ }
+ }
+
+ private static WritableFileSection createCompressibleMethodsSection(
+ @NonNull DexProfileData[] profileData
+ ) throws IOException {
+ int expectedSize = 0;
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ for (int i = 0; i < profileData.length; i++) {
+ DexProfileData profile = profileData[i];
+ // Method Flags
+ int methodFlags = computeMethodFlags(profile);
+ // Bitmap Contents
+ byte[] bitmapContents = createMethodBitmapRegion(profile);
+ // Methods with Inline Caches
+ byte[] methodRegionContents = createMethodsWithInlineCaches(profile);
+ // Profile Index
+ expectedSize += UINT_16_SIZE;
+ writeUInt16(out, i);
+ // Following Data (flags + bitmap contents + method region)
+ int followingDataSize =
+ UINT_16_SIZE + bitmapContents.length + methodRegionContents.length;
+ expectedSize += UINT_32_SIZE;
+ writeUInt32(out, followingDataSize);
+ // Contents
+ writeUInt16(out, methodFlags);
+ out.write(bitmapContents);
+ out.write(methodRegionContents);
+ expectedSize += followingDataSize;
+ }
+ byte[] contents = out.toByteArray();
+ if (expectedSize != contents.length) {
+ throw error(
+ "Expected size " + expectedSize + ", does not match actual size "
+ + contents.length
+ );
+ }
+ return new WritableFileSection(
+ FileSectionType.METHODS,
+ expectedSize,
+ contents,
+ true /* needsCompression */
+ );
+ }
+ }
+
+ private static byte[] createMethodBitmapRegion(
+ @NonNull DexProfileData profile
+ ) throws IOException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ writeMethodBitmap(out, profile);
+ return out.toByteArray();
+ }
+ }
+
+ private static byte[] createMethodsWithInlineCaches(
+ @NonNull DexProfileData profile
+ ) throws IOException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ writeMethodsWithInlineCaches(out, profile);
+ return out.toByteArray();
+ }
+ }
+
+ private static int computeMethodFlags(@NonNull DexProfileData profileData) {
+ int methodFlags = 0;
+ for (Map.Entry<Integer, Integer> entry: profileData.methods.entrySet()) {
+ int flagValue = entry.getValue();
+ methodFlags |= flagValue;
+ }
+ return methodFlags;
+ }
+
+ /**
* Writes the provided [lines] out into a binary profile suitable for P,Q,R devices. This
* method expects that the MAGIC and Version of the profile header have already been written
* to the OutputStream.
@@ -532,9 +809,22 @@
}
}
+
+ static @NonNull DexProfileData[] readMeta(
+ @NonNull InputStream is,
+ @NonNull byte[] metadataVersion,
+ @NonNull byte[] desiredProfileVersion,
+ DexProfileData[] profile
+ ) throws IOException {
+ if (Arrays.equals(metadataVersion, ProfileVersion.METADATA_V001_N)) {
+ return readMetadata001(is, metadataVersion, profile);
+ } else if (Arrays.equals(metadataVersion, ProfileVersion.METADATA_V002)) {
+ return readMetadataV002(is, desiredProfileVersion, profile);
+ }
+ throw error("Unsupported meta version");
+ }
+
/**
- *
- *
* [profile_header, zipped[[dex_data_header1, dex_data_header2...],[dex_data1,
* dex_data2...], global_aggregation_count]]
* profile_header:
@@ -543,19 +833,13 @@
* dex_location,number_of_classes
* dex_data:
* class_id1,class_id2...
- *
- * @param is
- * @param version
- * @param profile
- * @return
- * @throws IOException
*/
- static @NonNull DexProfileData[] readMeta(
+ static @NonNull DexProfileData[] readMetadata001(
@NonNull InputStream is,
- @NonNull byte[] version,
+ @NonNull byte[] metadataVersion,
DexProfileData[] profile
) throws IOException {
- if (!Arrays.equals(version, ProfileVersion.METADATA_V001_N)) {
+ if (!Arrays.equals(metadataVersion, ProfileVersion.METADATA_V001_N)) {
throw error("Unsupported meta version");
}
int numberOfDexFiles = readUInt8(is);
@@ -577,6 +861,108 @@
}
/**
+ * 0.0.2 Metadata Serialization format (used by N, S)
+ * ==================================================
+ * profile_header:
+ * magic,version,number_of_dex_files,uncompressed_size_of_zipped_data,compressed_data_size
+ * profile_data:
+ * profile_index, profile_key_size, profile_key,
+ * type_id_size, class_index_size, class_index_deltas
+ */
+ @NonNull
+ static DexProfileData[] readMetadataV002(
+ @NonNull InputStream is,
+ @NonNull byte[] desiredProfileVersion,
+ DexProfileData[] profile
+ ) throws IOException {
+ // No of dex files
+ int dexFileCount = readUInt16(is);
+ // Uncompressed Size
+ long uncompressed = readUInt32(is);
+ // Compressed Size
+ long compressed = readUInt32(is);
+ // We are done with the header, so everything that follows is the compressed blob. We
+ // uncompress it all and load it into memory
+ byte[] contents = readCompressed(
+ is,
+ (int) compressed,
+ (int) uncompressed
+ );
+ if (is.read() > 0) throw error("Content found after the end of file");
+ try (InputStream dataStream = new ByteArrayInputStream(contents)) {
+ return readMetadataV002Body(
+ dataStream,
+ desiredProfileVersion,
+ dexFileCount,
+ profile
+ );
+ }
+ }
+
+ @NonNull
+ private static DexProfileData[] readMetadataV002Body(
+ @NonNull InputStream is,
+ @NonNull byte[] desiredProfileVersion,
+ int dexFileCount,
+ DexProfileData[] profile
+ ) throws IOException {
+ // If the uncompressed profile data stream is empty then we have nothing more to do.
+ if (is.available() == 0) {
+ return new DexProfileData[0];
+ }
+ if (dexFileCount != profile.length) {
+ throw error("Mismatched number of dex files found in metadata");
+ }
+ for (int i = 0; i < dexFileCount; i++) {
+ // Profile Index
+ readUInt16(is);
+ // Profile Key
+ int profileKeySize = readUInt16(is);
+ String profileKey = readString(is, profileKeySize);
+ // Total number of type ids
+ long typeIdCount = readUInt32(is);
+ // Class Index Size
+ int classIdSetSize = readUInt16(is);
+ DexProfileData data = findByName(profile, profileKey);
+ if (data == null) {
+ throw error("Missing profile key: " + profileKey);
+ }
+ // Purely additive information
+ data.mTypeIdCount = typeIdCount;
+ // Classes
+ // Read classes even though we may not actually use it given we need to advance
+ // the offsets of the input stream to be consistent.
+ int[] classes = readClasses(is, classIdSetSize);
+ // We only need classIds for Android N and N MR1.
+ // For other profile versions we need to use type ids instead.
+ if (Arrays.equals(desiredProfileVersion, ProfileVersion.V001_N)) {
+ data.classSetSize = classIdSetSize;
+ data.classes = classes;
+ }
+ }
+ return profile;
+ }
+
+ @Nullable
+ private static DexProfileData findByName(
+ @NonNull DexProfileData[] profile,
+ @NonNull String name) {
+
+ if (profile.length <= 0) return null;
+ for (int i = 0; i < profile.length; i++) {
+ String profileKey = generateDexKey(
+ profile[i].apkName,
+ profile[i].dexName,
+ ProfileVersion.V015_S
+ );
+ if (name.equals(profileKey)) {
+ return profile[i];
+ }
+ }
+ return null;
+ }
+
+ /**
* Parses the un-zipped blob of data in the P+ profile format. It is assumed that no data has
* been read from this blob, and that the InputStream that this method is passed was just
* decompressed from the original file.
@@ -611,9 +997,8 @@
throw error("Order of dexfiles in metadata did not match baseline");
}
data.classSetSize = sizes[i];
- data.classes = new int[data.classSetSize];
// Then the startup classes are stored
- readClasses(is, data);
+ data.classes = readClasses(is, data.classSetSize);
}
return profile;
@@ -625,8 +1010,9 @@
*
* This returns one of:
* 1. If dexName is "classes.dex" -> apkName
- * 2. If dexName ends with ".apk" -> dexName
- * 3. else -> $apkName$separator$deXName
+ * 2. If the apkName is empty -> return dexName
+ * 3. If dexName ends with ".apk" -> dexName
+ * 4. else -> $apkName$separator$deXName
*
* @param apkName name of APK to generate key for
* @param dexName name of dex file, or input string if original profile dex key matched ".*\
@@ -635,12 +1021,33 @@
* @return correctly formatted dex key for this API version
*/
@NonNull
- private static String generateDexKey(@NonNull String apkName, @NonNull String dexName,
+ private static String generateDexKey(
+ @NonNull String apkName,
+ @NonNull String dexName,
@NonNull byte[] version) {
+ String separator = ProfileVersion.dexKeySeparator(version);
+ if (apkName.length() <= 0) return enforceSeparator(dexName, separator);
if (dexName.equals("classes.dex")) return apkName;
+ if (dexName.contains("!") || dexName.contains(":")) {
+ return enforceSeparator(dexName, separator);
+ }
+ if (dexName.endsWith(".apk")) return dexName;
return apkName + ProfileVersion.dexKeySeparator(version) + dexName;
}
+ @NonNull
+ private static String enforceSeparator(
+ @NonNull String value,
+ @NonNull String separator) {
+ if ("!".equals(separator)) {
+ return value.replace(":", "!");
+ } else if (":".equals(separator)) {
+ return value.replace("!", ":");
+ } else {
+ return value;
+ }
+ }
+
/**
* Parses the un-zipped blob of data in the P+ profile format. It is assumed that no data has
* been read from this blob, and that the InputStream that this method is passed was just
@@ -670,6 +1077,7 @@
apkName,
readString(is, dexNameSize), /* req: only dex name no separater from profgen */
dexChecksum,
+ 0L, /* typeId count. */
classSetSize,
(int) hotMethodRegionSize,
(int) numMethodIds,
@@ -686,7 +1094,7 @@
readHotMethodRegion(is, data);
// Then the startup classes are stored
- readClasses(is, data);
+ data.classes = readClasses(is, data.classSetSize);
// In addition to [HOT], the methods can be labeled as [STARTUP] and [POST_STARTUP].
// To compress this information better, this information is stored as a bitmap, with
@@ -757,17 +1165,19 @@
}
}
- private static void readClasses(
+ private static int[] readClasses(
@NonNull InputStream is,
- @NonNull DexProfileData data
+ int classSetSize
) throws IOException {
+ int[] classes = new int[classSetSize];
int lastClassIndex = 0;
- for (int k = 0; k < data.classSetSize; k++) {
+ for (int k = 0; k < classSetSize; k++) {
int diffWithTheLastClassIndex = readUInt16(is);
int classDexIndex = lastClassIndex + diffWithTheLastClassIndex;
- data.classes[k] = classDexIndex;
+ classes[k] = classDexIndex;
lastClassIndex = classDexIndex;
}
+ return classes;
}
private static void readMethodBitmap(
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
index 799131b..e98c680 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
@@ -22,11 +22,13 @@
class ProfileVersion {
private ProfileVersion() {}
+ static final byte[] V015_S = new byte[]{'0', '1', '5', '\0'};
static final byte[] V010_P = new byte[]{'0', '1', '0', '\0'};
static final byte[] V009_O_MR1 = new byte[]{'0', '0', '9', '\0'};
static final byte[] V005_O = new byte[]{'0', '0', '5', '\0'};
static final byte[] V001_N = new byte[]{'0', '0', '1', '\0'};
static final byte[] METADATA_V001_N = new byte[]{'0', '0', '1', '\0'};
+ static final byte[] METADATA_V002 = new byte[]{'0', '0', '2', '\0'};
static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
static String dexKeySeparator(byte[] version) {
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/WritableFileSection.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/WritableFileSection.java
new file mode 100644
index 0000000..d975719
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/WritableFileSection.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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 androidx.profileinstaller;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A Writable Profile Section for ART profiles on Android 12.
+ */
+class WritableFileSection {
+ final FileSectionType mType;
+ final int mExpectedInflateSize;
+ final byte[] mContents;
+ final boolean mNeedsCompression;
+
+ WritableFileSection(
+ @NonNull FileSectionType type,
+ int expectedInflateSize,
+ @NonNull byte[] contents,
+ boolean needsCompression) {
+ this.mType = type;
+ this.mExpectedInflateSize = expectedInflateSize;
+ this.mContents = contents;
+ this.mNeedsCompression = needsCompression;
+ }
+}
diff --git a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
index 4132408..f04d352 100644
--- a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
+++ b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
@@ -38,6 +38,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -59,7 +60,9 @@
@After
public void rmTmpDir() {
try {
- Files.delete(mTmpDir);
+ Files.walk(mTmpDir)
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile).forEach(File::delete);
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileTranscoderTests.java b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileTranscoderTests.java
index 18e8b32..8a9bd79 100644
--- a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileTranscoderTests.java
+++ b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileTranscoderTests.java
@@ -98,6 +98,17 @@
}
@Test
+ public void testTranscodeForS() throws IOException {
+ assertGoldenTranscodeWithMeta(
+ testFile("jetcaster/baseline-multidex-p.prof"),
+ testFile("jetcaster/baseline-multidex-s.profm"),
+ testFile("jetcaster/baseline-multidex-s.prof"),
+ ProfileVersion.V015_S,
+ "" /* apkName */
+ );
+ }
+
+ @Test
public void testMultidexTranscodeForO() throws IOException {
assertGoldenTranscode(
testFile("baseline-multidex.prof"),
@@ -121,7 +132,31 @@
testFile("baseline-multidex.prof"),
testFile("baseline-multidex.profm"),
testFile("baseline-multidex-n.prof"),
- ProfileVersion.V001_N
+ ProfileVersion.V001_N,
+ APK_NAME
+ );
+ }
+
+ @Test
+ public void testMultiDexTranscodeForN_withMetadata002() throws IOException {
+ File input = testFile("jetcaster/baseline-multidex-p.prof");
+ File inputMeta001 = testFile("jetcaster/baseline-multidex-n.profm");
+ File inputMeta002 = testFile("jetcaster/baseline-multidex-s.profm");
+ String apkName = "";
+ byte[] desiredVersion = ProfileVersion.V001_N;
+ byte[] merged001 = readProfileAndMetadata(input, inputMeta001, desiredVersion, apkName);
+ byte[] merged002 = readProfileAndMetadata(input, inputMeta002, desiredVersion, apkName);
+ Truth.assertThat(Arrays.equals(merged001, merged002)).isTrue();
+ }
+
+ @Test
+ public void testMultidexTranscodeForN_withMetadata002_WithGolden() throws IOException {
+ assertGoldenTranscodeWithMeta(
+ testFile("jetcaster/baseline-multidex-p.prof"),
+ testFile("jetcaster/baseline-multidex-s.profm"), // metadata002
+ testFile("jetcaster/baseline-multidex-n.prof"),
+ ProfileVersion.V001_N,
+ ""
);
}
@@ -156,28 +191,36 @@
@NonNull File input,
@NonNull File inputMeta,
@NonNull File golden,
- @NonNull byte[] desiredVersion
+ @NonNull byte[] desiredVersion,
+ @NonNull String apkName
+ ) throws IOException {
+ byte[] actualBytes = readProfileAndMetadata(input, inputMeta, desiredVersion, apkName);
+ byte[] goldenBytes = Files.readAllBytes(golden.toPath());
+ Truth.assertThat(Arrays.equals(goldenBytes, actualBytes)).isTrue();
+ }
+
+ private static byte[] readProfileAndMetadata(
+ @NonNull File input,
+ @NonNull File inputMeta,
+ @NonNull byte[] desiredVersion,
+ @NonNull String apkName
) throws IOException {
try (
InputStream isProf = new FileInputStream(input);
InputStream isProfM = new FileInputStream(inputMeta);
- ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ByteArrayOutputStream os = new ByteArrayOutputStream()
) {
byte[] version = ProfileTranscoder.readHeader(isProf, MAGIC_PROF);
DexProfileData[] data = ProfileTranscoder.readProfile(
isProf,
version,
- APK_NAME
+ apkName
);
-
byte[] metaVersion = ProfileTranscoder.readHeader(isProfM, MAGIC_PROFM);
- data = ProfileTranscoder.readMeta(isProfM, metaVersion, data);
-
+ data = ProfileTranscoder.readMeta(isProfM, metaVersion, desiredVersion, data);
ProfileTranscoder.writeHeader(os, desiredVersion);
ProfileTranscoder.transcodeAndWriteBody(os, desiredVersion, data);
- byte[] goldenBytes = Files.readAllBytes(golden.toPath());
- byte[] actualBytes = os.toByteArray();
- Truth.assertThat(Arrays.equals(goldenBytes, actualBytes)).isTrue();
+ return os.toByteArray();
}
}
diff --git a/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.prof b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.prof
new file mode 100644
index 0000000..d59b634
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.prof
Binary files differ
diff --git a/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.profm b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.profm
new file mode 100644
index 0000000..0b4e88a
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-n.profm
Binary files differ
diff --git a/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-p.prof b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-p.prof
new file mode 100644
index 0000000..cc144a0
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-p.prof
Binary files differ
diff --git a/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.prof b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.prof
new file mode 100644
index 0000000..3a2fa86
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.prof
Binary files differ
diff --git a/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.profm b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.profm
new file mode 100644
index 0000000..273dbae
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/test/test-data/jetcaster/baseline-multidex-s.profm
Binary files differ