Integrate adb with fastdeploy
Test: lunch sdk-eng && make sdk -j44
Test: lunch aosp_walleye-eng && cd system/core/adb && mm
Test: adb install -r -f --force-agent --local-agent ~/example_apks/example.apk
Test: adb install -r -f --no-streaming --force-agent --local-agent ~/example_apks/example.apk
Change-Id: Ia1c2160f87ea584656f8fdd67e314a260d39d607
diff --git a/adb/fastdeploy/Android.bp b/adb/fastdeploy/Android.bp
new file mode 100644
index 0000000..30f4730
--- /dev/null
+++ b/adb/fastdeploy/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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.
+//
+
+java_library {
+ name: "deployagent",
+ sdk_version: "24",
+ srcs: ["deployagent/src/**/*.java", "deploylib/src/**/*.java", "proto/**/*.proto"],
+ static_libs: ["apkzlib_zip"],
+ proto: {
+ type: "lite",
+ }
+}
+
+cc_prebuilt_binary {
+ name: "deployagent.sh",
+
+ srcs: ["deployagent/deployagent.sh"],
+ required: ["deployagent"],
+ device_supported: true,
+}
+
+java_binary_host {
+ name: "deploypatchgenerator",
+ srcs: ["deploypatchgenerator/src/**/*.java", "deploylib/src/**/*.java", "proto/**/*.proto"],
+ static_libs: ["apkzlib"],
+ manifest: "deploypatchgenerator/manifest.txt",
+ proto: {
+ type: "full",
+ }
+}
diff --git a/adb/fastdeploy/deployagent/deployagent.sh b/adb/fastdeploy/deployagent/deployagent.sh
new file mode 100755
index 0000000..4f17eb7
--- /dev/null
+++ b/adb/fastdeploy/deployagent/deployagent.sh
@@ -0,0 +1,7 @@
+# Script to start "deployagent" on the device, which has a very rudimentary
+# shell.
+#
+base=/data/local/tmp
+export CLASSPATH=$base/deployagent.jar
+exec app_process $base com.android.fastdeploy.DeployAgent "$@"
+
diff --git a/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java
new file mode 100644
index 0000000..cd6f168
--- /dev/null
+++ b/adb/fastdeploy/deployagent/src/com/android/fastdeploy/DeployAgent.java
@@ -0,0 +1,295 @@
+/*
+ * 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 com.android.fastdeploy;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Set;
+
+import com.android.fastdeploy.APKMetaData;
+import com.android.fastdeploy.PatchUtils;
+
+public final class DeployAgent {
+ private static final int BUFFER_SIZE = 128 * 1024;
+ private static final int AGENT_VERSION = 0x00000001;
+
+ public static void main(String[] args) {
+ int exitCode = 0;
+ try {
+ if (args.length < 1) {
+ showUsage(0);
+ }
+
+ String commandString = args[0];
+
+ if (commandString.equals("extract")) {
+ if (args.length != 2) {
+ showUsage(1);
+ }
+
+ String packageName = args[1];
+ extractMetaData(packageName);
+ } else if (commandString.equals("apply")) {
+ if (args.length < 4) {
+ showUsage(1);
+ }
+
+ String packageName = args[1];
+ String patchPath = args[2];
+ String outputParam = args[3];
+
+ InputStream deltaInputStream = null;
+ if (patchPath.compareTo("-") == 0) {
+ deltaInputStream = System.in;
+ } else {
+ deltaInputStream = new FileInputStream(patchPath);
+ }
+
+ if (outputParam.equals("-o")) {
+ OutputStream outputStream = null;
+ if (args.length > 4) {
+ String outputPath = args[4];
+ if (!outputPath.equals("-")) {
+ outputStream = new FileOutputStream(outputPath);
+ }
+ }
+ if (outputStream == null) {
+ outputStream = System.out;
+ }
+ File deviceFile = getFileFromPackageName(packageName);
+ writePatchToStream(
+ new RandomAccessFile(deviceFile, "r"), deltaInputStream, outputStream);
+ } else if (outputParam.equals("-pm")) {
+ String[] sessionArgs = null;
+ if (args.length > 4) {
+ int numSessionArgs = args.length-4;
+ sessionArgs = new String[numSessionArgs];
+ for (int i=0 ; i<numSessionArgs ; i++) {
+ sessionArgs[i] = args[i+4];
+ }
+ }
+ exitCode = applyPatch(packageName, deltaInputStream, sessionArgs);
+ }
+ } else if (commandString.equals("version")) {
+ System.out.printf("0x%08X\n", AGENT_VERSION);
+ } else {
+ showUsage(1);
+ }
+ } catch (Exception e) {
+ System.err.println("Error: " + e);
+ e.printStackTrace();
+ System.exit(2);
+ }
+ System.exit(exitCode);
+ }
+
+ private static void showUsage(int exitCode) {
+ System.err.println(
+ "usage: deployagent <command> [<args>]\n\n" +
+ "commands:\n" +
+ "version get the version\n" +
+ "extract PKGNAME extract an installed package's metadata\n" +
+ "apply PKGNAME PATCHFILE [-o|-pm] apply a patch from PATCHFILE (- for stdin) to an installed package\n" +
+ " -o <FILE> directs output to FILE, default or - for stdout\n" +
+ " -pm <ARGS> directs output to package manager, passes <ARGS> to 'pm install-create'\n"
+ );
+
+ System.exit(exitCode);
+ }
+
+ private static Process executeCommand(String command) throws IOException {
+ try {
+ Process p;
+ p = Runtime.getRuntime().exec(command);
+ p.waitFor();
+ return p;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ private static File getFileFromPackageName(String packageName) throws IOException {
+ StringBuilder commandBuilder = new StringBuilder();
+ commandBuilder.append("pm list packages -f " + packageName);
+
+ Process p = executeCommand(commandBuilder.toString());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
+
+ String packagePrefix = "package:";
+ String line = "";
+ while ((line = reader.readLine()) != null) {
+ int packageIndex = line.indexOf(packagePrefix);
+ int equalsIndex = line.indexOf("=" + packageName);
+ return new File(line.substring(packageIndex + packagePrefix.length(), equalsIndex));
+ }
+
+ return null;
+ }
+
+ private static void extractMetaData(String packageName) throws IOException {
+ File apkFile = getFileFromPackageName(packageName);
+ APKMetaData apkMetaData = PatchUtils.getAPKMetaData(apkFile);
+ apkMetaData.writeDelimitedTo(System.out);
+ }
+
+ private static int createInstallSession(String[] args) throws IOException {
+ StringBuilder commandBuilder = new StringBuilder();
+ commandBuilder.append("pm install-create ");
+ for (int i=0 ; args != null && i<args.length ; i++) {
+ commandBuilder.append(args[i] + " ");
+ }
+
+ Process p = executeCommand(commandBuilder.toString());
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String line = "";
+ String successLineStart = "Success: created install session [";
+ String successLineEnd = "]";
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith(successLineStart) && line.endsWith(successLineEnd)) {
+ return Integer.parseInt(line.substring(successLineStart.length(), line.lastIndexOf(successLineEnd)));
+ }
+ }
+
+ return -1;
+ }
+
+ private static int commitInstallSession(int sessionId) throws IOException {
+ StringBuilder commandBuilder = new StringBuilder();
+ commandBuilder.append(String.format("pm install-commit %d -- - ", sessionId));
+ Process p = executeCommand(commandBuilder.toString());
+ return p.exitValue();
+ }
+
+ private static int applyPatch(String packageName, InputStream deltaStream, String[] sessionArgs)
+ throws IOException, PatchFormatException {
+ File deviceFile = getFileFromPackageName(packageName);
+ int sessionId = createInstallSession(sessionArgs);
+ if (sessionId < 0) {
+ System.err.println("PM Create Session Failed");
+ return -1;
+ }
+
+ int writeExitCode = writePatchedDataToSession(new RandomAccessFile(deviceFile, "r"), deltaStream, sessionId);
+
+ if (writeExitCode == 0) {
+ return commitInstallSession(sessionId);
+ } else {
+ return -1;
+ }
+ }
+
+ private static long writePatchToStream(RandomAccessFile oldData, InputStream patchData,
+ OutputStream outputStream) throws IOException, PatchFormatException {
+ long newSize = readPatchHeader(patchData);
+ long bytesWritten = writePatchedDataToStream(oldData, newSize, patchData, outputStream);
+ outputStream.flush();
+ if (bytesWritten != newSize) {
+ throw new PatchFormatException(String.format(
+ "output size mismatch (expected %ld but wrote %ld)", newSize, bytesWritten));
+ }
+ return bytesWritten;
+ }
+
+ private static long readPatchHeader(InputStream patchData)
+ throws IOException, PatchFormatException {
+ byte[] signatureBuffer = new byte[PatchUtils.SIGNATURE.length()];
+ try {
+ PatchUtils.readFully(patchData, signatureBuffer, 0, signatureBuffer.length);
+ } catch (IOException e) {
+ throw new PatchFormatException("truncated signature");
+ }
+
+ String signature = new String(signatureBuffer, 0, signatureBuffer.length, "US-ASCII");
+ if (!PatchUtils.SIGNATURE.equals(signature)) {
+ throw new PatchFormatException("bad signature");
+ }
+
+ long newSize = PatchUtils.readBsdiffLong(patchData);
+ if (newSize < 0 || newSize > Integer.MAX_VALUE) {
+ throw new PatchFormatException("bad newSize");
+ }
+
+ return newSize;
+ }
+
+ // Note that this function assumes patchData has been seek'ed to the start of the delta stream
+ // (i.e. the signature has already been read by readPatchHeader). For a stream that points to the
+ // start of a patch file call writePatchToStream
+ private static long writePatchedDataToStream(RandomAccessFile oldData, long newSize,
+ InputStream patchData, OutputStream outputStream) throws IOException {
+ long newDataBytesWritten = 0;
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ while (newDataBytesWritten < newSize) {
+ long copyLen = PatchUtils.readFormattedLong(patchData);
+ if (copyLen > 0) {
+ PatchUtils.pipe(patchData, outputStream, buffer, (int) copyLen);
+ }
+
+ long oldDataOffset = PatchUtils.readFormattedLong(patchData);
+ long oldDataLen = PatchUtils.readFormattedLong(patchData);
+ oldData.seek(oldDataOffset);
+ if (oldDataLen > 0) {
+ PatchUtils.pipe(oldData, outputStream, buffer, (int) oldDataLen);
+ }
+
+ newDataBytesWritten += copyLen + oldDataLen;
+ }
+
+ return newDataBytesWritten;
+ }
+
+ private static int writePatchedDataToSession(RandomAccessFile oldData, InputStream patchData, int sessionId)
+ throws IOException, PatchFormatException {
+ try {
+ Process p;
+ long newSize = readPatchHeader(patchData);
+ StringBuilder commandBuilder = new StringBuilder();
+ commandBuilder.append(String.format("pm install-write -S %d %d -- -", newSize, sessionId));
+
+ String command = commandBuilder.toString();
+ p = Runtime.getRuntime().exec(command);
+
+ OutputStream sessionOutputStream = p.getOutputStream();
+ long bytesWritten = writePatchedDataToStream(oldData, newSize, patchData, sessionOutputStream);
+ sessionOutputStream.flush();
+ p.waitFor();
+ if (bytesWritten != newSize) {
+ throw new PatchFormatException(
+ String.format("output size mismatch (expected %d but wrote %)", newSize, bytesWritten));
+ }
+ return p.exitValue();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return -1;
+ }
+}
diff --git a/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchFormatException.java b/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchFormatException.java
new file mode 100644
index 0000000..f0655f3
--- /dev/null
+++ b/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchFormatException.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.fastdeploy;
+
+class PatchFormatException extends Exception {
+ /**
+ * Constructs a new exception with the specified message.
+ * @param message the message
+ */
+ public PatchFormatException(String message) { super(message); }
+
+ /**
+ * Constructs a new exception with the specified message and cause.
+ * @param message the message
+ * @param cause the cause of the error
+ */
+ public PatchFormatException(String message, Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+}
diff --git a/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java b/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java
new file mode 100644
index 0000000..f0f00e1
--- /dev/null
+++ b/adb/fastdeploy/deploylib/src/com/android/fastdeploy/PatchUtils.java
@@ -0,0 +1,186 @@
+/*
+ * 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 com.android.fastdeploy;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+import com.android.tools.build.apkzlib.zip.ZFile;
+import com.android.tools.build.apkzlib.zip.ZFileOptions;
+import com.android.tools.build.apkzlib.zip.StoredEntry;
+import com.android.tools.build.apkzlib.zip.StoredEntryType;
+import com.android.tools.build.apkzlib.zip.CentralDirectoryHeaderCompressInfo;
+import com.android.tools.build.apkzlib.zip.CentralDirectoryHeader;
+
+import com.android.fastdeploy.APKMetaData;
+import com.android.fastdeploy.APKEntry;
+
+class PatchUtils {
+ private static final long NEGATIVE_MASK = 1L << 63;
+ private static final long NEGATIVE_LONG_SIGN_MASK = 1L << 63;
+ public static final String SIGNATURE = "HAMADI/IHD";
+
+ private static long getOffsetFromEntry(StoredEntry entry) {
+ return entry.getCentralDirectoryHeader().getOffset() + entry.getLocalHeaderSize();
+ }
+
+ public static APKMetaData getAPKMetaData(File apkFile) throws IOException {
+ APKMetaData.Builder apkEntriesBuilder = APKMetaData.newBuilder();
+ ZFileOptions options = new ZFileOptions();
+ ZFile zFile = new ZFile(apkFile, options);
+
+ ArrayList<StoredEntry> metaDataEntries = new ArrayList<StoredEntry>();
+
+ for (StoredEntry entry : zFile.entries()) {
+ if (entry.getType() != StoredEntryType.FILE) {
+ continue;
+ }
+ metaDataEntries.add(entry);
+ }
+
+ Collections.sort(metaDataEntries, new Comparator<StoredEntry>() {
+ private long getOffsetFromEntry(StoredEntry entry) {
+ return PatchUtils.getOffsetFromEntry(entry);
+ }
+
+ @Override
+ public int compare(StoredEntry lhs, StoredEntry rhs) {
+ // -1 - less than, 1 - greater than, 0 - equal, all inversed for descending
+ return Long.compare(getOffsetFromEntry(lhs), getOffsetFromEntry(rhs));
+ }
+ });
+
+ for (StoredEntry entry : metaDataEntries) {
+ CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();
+ CentralDirectoryHeaderCompressInfo cdhci = cdh.getCompressionInfoWithWait();
+
+ APKEntry.Builder entryBuilder = APKEntry.newBuilder();
+ entryBuilder.setCrc32(cdh.getCrc32());
+ entryBuilder.setFileName(cdh.getName());
+ entryBuilder.setCompressedSize(cdhci.getCompressedSize());
+ entryBuilder.setUncompressedSize(cdh.getUncompressedSize());
+ entryBuilder.setDataOffset(getOffsetFromEntry(entry));
+
+ apkEntriesBuilder.addEntries(entryBuilder);
+ apkEntriesBuilder.build();
+ }
+ return apkEntriesBuilder.build();
+ }
+
+ /**
+ * Writes a 64-bit signed integer to the specified {@link OutputStream}. The least significant
+ * byte is written first and the most significant byte is written last.
+ * @param value the value to write
+ * @param outputStream the stream to write to
+ */
+ static void writeFormattedLong(final long value, OutputStream outputStream) throws IOException {
+ long y = value;
+ if (y < 0) {
+ y = (-y) | NEGATIVE_MASK;
+ }
+
+ for (int i = 0; i < 8; ++i) {
+ outputStream.write((byte) (y & 0xff));
+ y >>>= 8;
+ }
+ }
+
+ /**
+ * Reads a 64-bit signed integer written by {@link #writeFormattedLong(long, OutputStream)} from
+ * the specified {@link InputStream}.
+ * @param inputStream the stream to read from
+ */
+ static long readFormattedLong(InputStream inputStream) throws IOException {
+ long result = 0;
+ for (int bitshift = 0; bitshift < 64; bitshift += 8) {
+ result |= ((long) inputStream.read()) << bitshift;
+ }
+
+ if ((result - NEGATIVE_MASK) > 0) {
+ result = (result & ~NEGATIVE_MASK) * -1;
+ }
+ return result;
+ }
+
+ static final long readBsdiffLong(InputStream in) throws PatchFormatException, IOException {
+ long result = 0;
+ for (int bitshift = 0; bitshift < 64; bitshift += 8) {
+ result |= ((long) in.read()) << bitshift;
+ }
+
+ if (result == NEGATIVE_LONG_SIGN_MASK) {
+ // "Negative zero", which is valid in signed-magnitude format.
+ // NB: No sane patch generator should ever produce such a value.
+ throw new PatchFormatException("read negative zero");
+ }
+
+ if ((result & NEGATIVE_LONG_SIGN_MASK) != 0) {
+ result = -(result & ~NEGATIVE_LONG_SIGN_MASK);
+ }
+
+ return result;
+ }
+
+ static void readFully(final InputStream in, final byte[] destination, final int startAt,
+ final int numBytes) throws IOException {
+ int numRead = 0;
+ while (numRead < numBytes) {
+ int readNow = in.read(destination, startAt + numRead, numBytes - numRead);
+ if (readNow == -1) {
+ throw new IOException("truncated input stream");
+ }
+ numRead += readNow;
+ }
+ }
+
+ static void pipe(final InputStream in, final OutputStream out, final byte[] buffer,
+ long copyLength) throws IOException {
+ while (copyLength > 0) {
+ int maxCopy = Math.min(buffer.length, (int) copyLength);
+ readFully(in, buffer, 0, maxCopy);
+ out.write(buffer, 0, maxCopy);
+ copyLength -= maxCopy;
+ }
+ }
+
+ static void pipe(final RandomAccessFile in, final OutputStream out, final byte[] buffer,
+ long copyLength) throws IOException {
+ while (copyLength > 0) {
+ int maxCopy = Math.min(buffer.length, (int) copyLength);
+ in.readFully(buffer, 0, maxCopy);
+ out.write(buffer, 0, maxCopy);
+ copyLength -= maxCopy;
+ }
+ }
+
+ static void fill(byte value, final OutputStream out, final byte[] buffer, long fillLength)
+ throws IOException {
+ while (fillLength > 0) {
+ int maxCopy = Math.min(buffer.length, (int) fillLength);
+ Arrays.fill(buffer, 0, maxCopy, value);
+ out.write(buffer, 0, maxCopy);
+ fillLength -= maxCopy;
+ }
+ }
+}
diff --git a/adb/fastdeploy/deploypatchgenerator/manifest.txt b/adb/fastdeploy/deploypatchgenerator/manifest.txt
new file mode 100644
index 0000000..5c00505
--- /dev/null
+++ b/adb/fastdeploy/deploypatchgenerator/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.fastdeploy.DeployPatchGenerator
diff --git a/adb/fastdeploy/deploypatchgenerator/src/com/android/fastdeploy/DeployPatchGenerator.java b/adb/fastdeploy/deploypatchgenerator/src/com/android/fastdeploy/DeployPatchGenerator.java
new file mode 100644
index 0000000..5577364
--- /dev/null
+++ b/adb/fastdeploy/deploypatchgenerator/src/com/android/fastdeploy/DeployPatchGenerator.java
@@ -0,0 +1,207 @@
+/*
+ * 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 com.android.fastdeploy;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.StringBuilder;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ArrayList;
+
+import java.nio.charset.StandardCharsets;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.AbstractMap.SimpleEntry;
+
+import com.android.fastdeploy.APKMetaData;
+import com.android.fastdeploy.APKEntry;
+
+public final class DeployPatchGenerator {
+ private static final int BUFFER_SIZE = 128 * 1024;
+
+ public static void main(String[] args) {
+ try {
+ if (args.length < 2) {
+ showUsage(0);
+ }
+
+ boolean verbose = false;
+ if (args.length > 2) {
+ String verboseFlag = args[2];
+ if (verboseFlag.compareTo("--verbose") == 0) {
+ verbose = true;
+ }
+ }
+
+ StringBuilder sb = null;
+ String apkPath = args[0];
+ String deviceMetadataPath = args[1];
+ File hostFile = new File(apkPath);
+
+ List<APKEntry> deviceZipEntries = getMetadataFromFile(deviceMetadataPath);
+ if (verbose) {
+ sb = new StringBuilder();
+ for (APKEntry entry : deviceZipEntries) {
+ APKEntryToString(entry, sb);
+ }
+ System.err.println("Device Entries (" + deviceZipEntries.size() + ")");
+ System.err.println(sb.toString());
+ }
+
+ List<APKEntry> hostFileEntries = PatchUtils.getAPKMetaData(hostFile).getEntriesList();
+ if (verbose) {
+ sb = new StringBuilder();
+ for (APKEntry entry : hostFileEntries) {
+ APKEntryToString(entry, sb);
+ }
+ System.err.println("Host Entries (" + hostFileEntries.size() + ")");
+ System.err.println(sb.toString());
+ }
+
+ List<SimpleEntry<APKEntry, APKEntry>> identicalContentsEntrySet =
+ getIdenticalContents(deviceZipEntries, hostFileEntries);
+ reportIdenticalContents(identicalContentsEntrySet, hostFile);
+
+ if (verbose) {
+ sb = new StringBuilder();
+ for (SimpleEntry<APKEntry, APKEntry> identicalEntry : identicalContentsEntrySet) {
+ APKEntry entry = identicalEntry.getValue();
+ APKEntryToString(entry, sb);
+ }
+ System.err.println("Identical Entries (" + identicalContentsEntrySet.size() + ")");
+ System.err.println(sb.toString());
+ }
+
+ createPatch(identicalContentsEntrySet, hostFile, System.out);
+ } catch (Exception e) {
+ System.err.println("Error: " + e);
+ e.printStackTrace();
+ System.exit(2);
+ }
+ System.exit(0);
+ }
+
+ private static void showUsage(int exitCode) {
+ System.err.println("usage: deploypatchgenerator <apkpath> <deviceapkmetadata> [--verbose]");
+ System.err.println("");
+ System.exit(exitCode);
+ }
+
+ private static void APKEntryToString(APKEntry entry, StringBuilder outputString) {
+ outputString.append(String.format("Filename: %s\n", entry.getFileName()));
+ outputString.append(String.format("CRC32: 0x%08X\n", entry.getCrc32()));
+ outputString.append(String.format("Data Offset: %d\n", entry.getDataOffset()));
+ outputString.append(String.format("Compressed Size: %d\n", entry.getCompressedSize()));
+ outputString.append(String.format("Uncompressed Size: %d\n", entry.getUncompressedSize()));
+ }
+
+ private static List<APKEntry> getMetadataFromFile(String deviceMetadataPath) throws IOException {
+ InputStream is = new FileInputStream(new File(deviceMetadataPath));
+ APKMetaData apkMetaData = APKMetaData.parseDelimitedFrom(is);
+ return apkMetaData.getEntriesList();
+ }
+
+ private static List<SimpleEntry<APKEntry, APKEntry>> getIdenticalContents(
+ List<APKEntry> deviceZipEntries, List<APKEntry> hostZipEntries) throws IOException {
+ List<SimpleEntry<APKEntry, APKEntry>> identicalContents =
+ new ArrayList<SimpleEntry<APKEntry, APKEntry>>();
+
+ for (APKEntry deviceZipEntry : deviceZipEntries) {
+ for (APKEntry hostZipEntry : hostZipEntries) {
+ if (deviceZipEntry.getCrc32() == hostZipEntry.getCrc32()) {
+ identicalContents.add(new SimpleEntry(deviceZipEntry, hostZipEntry));
+ }
+ }
+ }
+
+ Collections.sort(identicalContents, new Comparator<SimpleEntry<APKEntry, APKEntry>>() {
+ @Override
+ public int compare(
+ SimpleEntry<APKEntry, APKEntry> p1, SimpleEntry<APKEntry, APKEntry> p2) {
+ return Long.compare(p1.getValue().getDataOffset(), p2.getValue().getDataOffset());
+ }
+ });
+
+ return identicalContents;
+ }
+
+ private static void reportIdenticalContents(
+ List<SimpleEntry<APKEntry, APKEntry>> identicalContentsEntrySet, File hostFile)
+ throws IOException {
+ long totalEqualBytes = 0;
+ int totalEqualFiles = 0;
+ for (SimpleEntry<APKEntry, APKEntry> entries : identicalContentsEntrySet) {
+ APKEntry hostAPKEntry = entries.getValue();
+ totalEqualBytes += hostAPKEntry.getCompressedSize();
+ totalEqualFiles++;
+ }
+
+ float savingPercent = (float) (totalEqualBytes * 100) / hostFile.length();
+
+ System.err.println("Detected " + totalEqualFiles + " equal APK entries");
+ System.err.println(totalEqualBytes + " bytes are equal out of " + hostFile.length() + " ("
+ + savingPercent + "%)");
+ }
+
+ static void createPatch(List<SimpleEntry<APKEntry, APKEntry>> zipEntrySimpleEntrys,
+ File hostFile, OutputStream patchStream) throws IOException, PatchFormatException {
+ FileInputStream hostFileInputStream = new FileInputStream(hostFile);
+
+ patchStream.write(PatchUtils.SIGNATURE.getBytes(StandardCharsets.US_ASCII));
+ PatchUtils.writeFormattedLong(hostFile.length(), patchStream);
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ long totalBytesWritten = 0;
+ Iterator<SimpleEntry<APKEntry, APKEntry>> entrySimpleEntryIterator =
+ zipEntrySimpleEntrys.iterator();
+ while (entrySimpleEntryIterator.hasNext()) {
+ SimpleEntry<APKEntry, APKEntry> entrySimpleEntry = entrySimpleEntryIterator.next();
+ APKEntry deviceAPKEntry = entrySimpleEntry.getKey();
+ APKEntry hostAPKEntry = entrySimpleEntry.getValue();
+
+ long newDataLen = hostAPKEntry.getDataOffset() - totalBytesWritten;
+ long oldDataOffset = deviceAPKEntry.getDataOffset();
+ long oldDataLen = deviceAPKEntry.getCompressedSize();
+
+ PatchUtils.writeFormattedLong(newDataLen, patchStream);
+ PatchUtils.pipe(hostFileInputStream, patchStream, buffer, newDataLen);
+ PatchUtils.writeFormattedLong(oldDataOffset, patchStream);
+ PatchUtils.writeFormattedLong(oldDataLen, patchStream);
+
+ long skip = hostFileInputStream.skip(oldDataLen);
+ if (skip != oldDataLen) {
+ throw new PatchFormatException("skip error: attempted to skip " + oldDataLen
+ + " bytes but return code was " + skip);
+ }
+ totalBytesWritten += oldDataLen + newDataLen;
+ }
+ long remainderLen = hostFile.length() - totalBytesWritten;
+ PatchUtils.writeFormattedLong(remainderLen, patchStream);
+ PatchUtils.pipe(hostFileInputStream, patchStream, buffer, remainderLen);
+ PatchUtils.writeFormattedLong(0, patchStream);
+ PatchUtils.writeFormattedLong(0, patchStream);
+ patchStream.flush();
+ }
+}
diff --git a/adb/fastdeploy/proto/ApkEntry.proto b/adb/fastdeploy/proto/ApkEntry.proto
new file mode 100644
index 0000000..9460d15
--- /dev/null
+++ b/adb/fastdeploy/proto/ApkEntry.proto
@@ -0,0 +1,18 @@
+syntax = "proto2";
+
+package com.android.fastdeploy;
+
+option java_package = "com.android.fastdeploy";
+option java_multiple_files = true;
+
+message APKEntry {
+ required int64 crc32 = 1;
+ required string fileName = 2;
+ required int64 dataOffset = 3;
+ required int64 compressedSize = 4;
+ required int64 uncompressedSize = 5;
+}
+
+message APKMetaData {
+ repeated APKEntry entries = 1;
+}