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;
+}