Project import
diff --git a/recovery/Android.mk b/recovery/Android.mk
new file mode 100644
index 0000000..b1d84cd
--- /dev/null
+++ b/recovery/Android.mk
@@ -0,0 +1,160 @@
+# Copyright (C) 2007 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# libfusesideload (static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := fuse_sideload.cpp
+LOCAL_CLANG := true
+LOCAL_CFLAGS := -O2 -g -DADB_HOST=0 -Wall -Wno-unused-parameter -Werror
+LOCAL_CFLAGS += -D_XOPEN_SOURCE -D_GNU_SOURCE
+LOCAL_MODULE := libfusesideload
+LOCAL_STATIC_LIBRARIES := libcutils libc libcrypto
+include $(BUILD_STATIC_LIBRARY)
+
+# libmounts (static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := mounts.cpp
+LOCAL_CLANG := true
+LOCAL_CFLAGS := -Wall -Wno-unused-parameter -Werror
+LOCAL_MODULE := libmounts
+include $(BUILD_STATIC_LIBRARY)
+
+# recovery (static executable)
+# ===============================
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+    adb_install.cpp \
+    asn1_decoder.cpp \
+    bootloader.cpp \
+    device.cpp \
+    fuse_sdcard_provider.cpp \
+    install.cpp \
+    recovery.cpp \
+    roots.cpp \
+    screen_ui.cpp \
+    ui.cpp \
+    verifier.cpp \
+    wear_ui.cpp \
+
+LOCAL_MODULE := recovery
+
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+
+ifeq ($(TARGET_USERIMAGES_USE_F2FS),true)
+ifeq ($(HOST_OS),linux)
+LOCAL_REQUIRED_MODULES := mkfs.f2fs
+endif
+endif
+
+RECOVERY_API_VERSION := 3
+RECOVERY_FSTAB_VERSION := 2
+LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
+LOCAL_CFLAGS += -Wno-unused-parameter
+LOCAL_CLANG := true
+
+LOCAL_C_INCLUDES += \
+    system/vold \
+    system/extras/ext4_utils \
+    system/core/adb \
+
+LOCAL_STATIC_LIBRARIES := \
+    libbatterymonitor \
+    libext4_utils_static \
+    libsparse_static \
+    libminzip \
+    libmounts \
+    libz \
+    libminadbd \
+    libfusesideload \
+    libminui \
+    libpng \
+    libfs_mgr \
+    libcrypto_utils \
+    libcrypto \
+    libbase \
+    libcutils \
+    libutils \
+    liblog \
+    libselinux \
+    libm \
+    libc
+
+LOCAL_HAL_STATIC_LIBRARIES := libhealthd
+
+LOCAL_C_INCLUDES += system/extras/ext4_utils
+LOCAL_STATIC_LIBRARIES += libext4_utils_static libz
+
+LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/sbin
+
+ifeq ($(TARGET_RECOVERY_UI_LIB),)
+  LOCAL_SRC_FILES += default_device.cpp
+else
+  LOCAL_STATIC_LIBRARIES += $(TARGET_RECOVERY_UI_LIB)
+endif
+
+ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),)
+LOCAL_REQUIRED_MODULES := recovery-persist recovery-refresh
+endif
+
+include $(BUILD_EXECUTABLE)
+
+# recovery-persist (system partition dynamic executable run after /data mounts)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := recovery-persist.cpp
+LOCAL_MODULE := recovery-persist
+LOCAL_SHARED_LIBRARIES := liblog libbase
+LOCAL_CFLAGS := -Werror
+LOCAL_INIT_RC := recovery-persist.rc
+include $(BUILD_EXECUTABLE)
+
+# recovery-refresh (system partition dynamic executable run at init)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := recovery-refresh.cpp
+LOCAL_MODULE := recovery-refresh
+LOCAL_SHARED_LIBRARIES := liblog
+LOCAL_CFLAGS := -Werror
+LOCAL_INIT_RC := recovery-refresh.rc
+include $(BUILD_EXECUTABLE)
+
+# libverifier (static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_MODULE := libverifier
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := \
+    asn1_decoder.cpp \
+    verifier.cpp \
+    ui.cpp
+LOCAL_STATIC_LIBRARIES := libcrypto_utils libcrypto
+include $(BUILD_STATIC_LIBRARY)
+
+include $(LOCAL_PATH)/minui/Android.mk \
+    $(LOCAL_PATH)/minzip/Android.mk \
+    $(LOCAL_PATH)/minadbd/Android.mk \
+    $(LOCAL_PATH)/tests/Android.mk \
+    $(LOCAL_PATH)/tools/Android.mk \
+    $(LOCAL_PATH)/edify/Android.mk \
+    $(LOCAL_PATH)/uncrypt/Android.mk \
+    $(LOCAL_PATH)/otafault/Android.mk \
+    $(LOCAL_PATH)/updater/Android.mk \
+    $(LOCAL_PATH)/update_verifier/Android.mk \
+    $(LOCAL_PATH)/applypatch/Android.mk
diff --git a/recovery/CleanSpec.mk b/recovery/CleanSpec.mk
new file mode 100644
index 0000000..e2d97d4
--- /dev/null
+++ b/recovery/CleanSpec.mk
@@ -0,0 +1,51 @@
+# Copyright (C) 2007 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.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list.  These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+#     $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+#     $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list.  E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/EXECUTABLES/recovery_intermediates)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libminui_intermediates/import_includes)
diff --git a/recovery/NOTICE b/recovery/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/recovery/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/recovery/README.md b/recovery/README.md
new file mode 100644
index 0000000..01fab94
--- /dev/null
+++ b/recovery/README.md
@@ -0,0 +1,29 @@
+The Recovery Image
+==================
+
+Quick turn-around testing
+-------------------------
+
+    mm -j && m ramdisk-nodeps && m recoveryimage-nodeps
+
+    # To boot into the new recovery image
+    # without flashing the recovery partition:
+    adb reboot bootloader
+    fastboot boot $ANDROID_PRODUCT_OUT/recovery.img
+
+Running the tests
+-----------------
+    # After setting up environment and lunch.
+    mmma -j bootable/recovery
+
+    # Running the tests on device.
+    adb root
+    adb sync data
+
+    # 32-bit device
+    adb shell /data/nativetest/recovery_unit_test/recovery_unit_test
+    adb shell /data/nativetest/recovery_component_test/recovery_component_test
+
+    # Or 64-bit device
+    adb shell /data/nativetest64/recovery_unit_test/recovery_unit_test
+    adb shell /data/nativetest64/recovery_component_test/recovery_component_test
diff --git a/recovery/adb_install.cpp b/recovery/adb_install.cpp
new file mode 100644
index 0000000..60616ca
--- /dev/null
+++ b/recovery/adb_install.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <fcntl.h>
+
+#include "ui.h"
+#include "cutils/properties.h"
+#include "install.h"
+#include "common.h"
+#include "adb_install.h"
+#include "minadbd/fuse_adb_provider.h"
+#include "fuse_sideload.h"
+
+static void set_usb_driver(RecoveryUI* ui, bool enabled) {
+    int fd = open("/sys/class/android_usb/android0/enable", O_WRONLY);
+    if (fd < 0) {
+        ui->Print("failed to open driver control: %s\n", strerror(errno));
+        return;
+    }
+    if (TEMP_FAILURE_RETRY(write(fd, enabled ? "1" : "0", 1)) == -1) {
+        ui->Print("failed to set driver control: %s\n", strerror(errno));
+    }
+    if (close(fd) < 0) {
+        ui->Print("failed to close driver control: %s\n", strerror(errno));
+    }
+}
+
+static void stop_adbd(RecoveryUI* ui) {
+    ui->Print("Stopping adbd...\n");
+    property_set("ctl.stop", "adbd");
+    set_usb_driver(ui, false);
+}
+
+static void maybe_restart_adbd(RecoveryUI* ui) {
+    if (is_ro_debuggable()) {
+        ui->Print("Restarting adbd...\n");
+        set_usb_driver(ui, true);
+        property_set("ctl.start", "adbd");
+    }
+}
+
+// How long (in seconds) we wait for the host to start sending us a
+// package, before timing out.
+#define ADB_INSTALL_TIMEOUT 300
+
+int apply_from_adb(RecoveryUI* ui, bool* wipe_cache, const char* install_file) {
+    modified_flash = true;
+
+    stop_adbd(ui);
+    set_usb_driver(ui, true);
+
+    ui->Print("\n\nNow send the package you want to apply\n"
+              "to the device with \"adb sideload <filename>\"...\n");
+
+    pid_t child;
+    if ((child = fork()) == 0) {
+        execl("/sbin/recovery", "recovery", "--adbd", NULL);
+        _exit(-1);
+    }
+
+    // FUSE_SIDELOAD_HOST_PATHNAME will start to exist once the host
+    // connects and starts serving a package.  Poll for its
+    // appearance.  (Note that inotify doesn't work with FUSE.)
+    int result = INSTALL_ERROR;
+    int status;
+    bool waited = false;
+    struct stat st;
+    for (int i = 0; i < ADB_INSTALL_TIMEOUT; ++i) {
+        if (waitpid(child, &status, WNOHANG) != 0) {
+            result = INSTALL_ERROR;
+            waited = true;
+            break;
+        }
+
+        if (stat(FUSE_SIDELOAD_HOST_PATHNAME, &st) != 0) {
+            if (errno == ENOENT && i < ADB_INSTALL_TIMEOUT-1) {
+                sleep(1);
+                continue;
+            } else {
+                ui->Print("\nTimed out waiting for package.\n\n");
+                result = INSTALL_ERROR;
+                kill(child, SIGKILL);
+                break;
+            }
+        }
+        result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, install_file, false);
+        break;
+    }
+
+    if (!waited) {
+        // Calling stat() on this magic filename signals the minadbd
+        // subprocess to shut down.
+        stat(FUSE_SIDELOAD_HOST_EXIT_PATHNAME, &st);
+
+        // TODO(dougz): there should be a way to cancel waiting for a
+        // package (by pushing some button combo on the device).  For now
+        // you just have to 'adb sideload' a file that's not a valid
+        // package, like "/dev/null".
+        waitpid(child, &status, 0);
+    }
+
+    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+        if (WEXITSTATUS(status) == 3) {
+            ui->Print("\nYou need adb 1.0.32 or newer to sideload\nto this device.\n\n");
+        } else if (!WIFSIGNALED(status)) {
+            ui->Print("\n(adbd status %d)\n", WEXITSTATUS(status));
+        }
+    }
+
+    set_usb_driver(ui, false);
+    maybe_restart_adbd(ui);
+
+    return result;
+}
diff --git a/recovery/adb_install.h b/recovery/adb_install.h
new file mode 100644
index 0000000..efad436
--- /dev/null
+++ b/recovery/adb_install.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#ifndef _ADB_INSTALL_H
+#define _ADB_INSTALL_H
+
+class RecoveryUI;
+
+int apply_from_adb(RecoveryUI* h, bool* wipe_cache, const char* install_file);
+
+#endif
diff --git a/recovery/applypatch/Android.mk b/recovery/applypatch/Android.mk
new file mode 100644
index 0000000..604787e
--- /dev/null
+++ b/recovery/applypatch/Android.mk
@@ -0,0 +1,99 @@
+# Copyright (C) 2008 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# libapplypatch (static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := \
+    applypatch.cpp \
+    bspatch.cpp \
+    freecache.cpp \
+    imgpatch.cpp \
+    utils.cpp
+LOCAL_MODULE := libapplypatch
+LOCAL_MODULE_TAGS := eng
+LOCAL_C_INCLUDES += \
+    $(LOCAL_PATH)/include \
+    bootable/recovery
+LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
+LOCAL_STATIC_LIBRARIES += \
+    libotafault \
+    libbase \
+    libcrypto \
+    libbz \
+    libz
+include $(BUILD_STATIC_LIBRARY)
+
+# libimgpatch (static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := bspatch.cpp imgpatch.cpp utils.cpp
+LOCAL_MODULE := libimgpatch
+LOCAL_C_INCLUDES += \
+    $(LOCAL_PATH)/include \
+    bootable/recovery
+LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
+LOCAL_STATIC_LIBRARIES += libcrypto libbz libz
+include $(BUILD_STATIC_LIBRARY)
+
+# libimgpatch (host static library)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := bspatch.cpp imgpatch.cpp utils.cpp
+LOCAL_MODULE := libimgpatch
+LOCAL_MODULE_HOST_OS := linux
+LOCAL_C_INCLUDES += \
+    $(LOCAL_PATH)/include \
+    bootable/recovery
+LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
+LOCAL_STATIC_LIBRARIES += libcrypto libbz libz
+include $(BUILD_HOST_STATIC_LIBRARY)
+
+# applypatch (executable)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := main.cpp
+LOCAL_MODULE := applypatch
+LOCAL_C_INCLUDES += bootable/recovery
+LOCAL_STATIC_LIBRARIES += \
+    libapplypatch \
+    libbase \
+    libedify \
+    libotafault \
+    libminzip \
+    libcrypto \
+    libbz
+LOCAL_SHARED_LIBRARIES += libz libcutils libc
+include $(BUILD_EXECUTABLE)
+
+# imgdiff (host static executable)
+# ===============================
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := imgdiff.cpp utils.cpp
+LOCAL_MODULE := imgdiff
+LOCAL_STATIC_LIBRARIES += \
+    libbsdiff \
+    libbz \
+    libdivsufsort64 \
+    libdivsufsort \
+    libz
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+include $(BUILD_HOST_EXECUTABLE)
diff --git a/recovery/applypatch/Makefile b/recovery/applypatch/Makefile
new file mode 100644
index 0000000..fb49843
--- /dev/null
+++ b/recovery/applypatch/Makefile
@@ -0,0 +1,33 @@
+# Copyright (C) 2016 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.
+
+# This file is for building imgdiff in Chrome OS.
+
+CPPFLAGS += -iquote.. -Iinclude
+CXXFLAGS += -std=c++11 -O3 -Wall -Werror
+LDLIBS += -lbz2 -lz
+
+.PHONY: all clean
+
+all: imgdiff libimgpatch.a
+
+clean:
+	rm -f *.o imgdiff libimgpatch.a
+
+imgdiff: imgdiff.o bsdiff.o utils.o
+	$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LDLIBS) -o $@ $^
+
+libimgpatch.a utils.o: CXXFLAGS += -fPIC
+libimgpatch.a: imgpatch.o bspatch.o utils.o
+	${AR} rcs $@ $^
diff --git a/recovery/applypatch/NOTICE b/recovery/applypatch/NOTICE
new file mode 100644
index 0000000..6156a0c
--- /dev/null
+++ b/recovery/applypatch/NOTICE
@@ -0,0 +1,41 @@
+Copyright (C) 2009 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.
+
+
+bsdiff.c
+bspatch.c
+
+Copyright 2003-2005 Colin Percival
+All rights reserved
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted providing that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/recovery/applypatch/applypatch.cpp b/recovery/applypatch/applypatch.cpp
new file mode 100644
index 0000000..02a3c6e
--- /dev/null
+++ b/recovery/applypatch/applypatch.cpp
@@ -0,0 +1,888 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <memory>
+#include <string>
+
+#include <android-base/strings.h>
+
+#include "openssl/sha.h"
+#include "applypatch/applypatch.h"
+#include "edify/expr.h"
+#include "ota_io.h"
+#include "print_sha1.h"
+
+static int LoadPartitionContents(const char* filename, FileContents* file);
+static ssize_t FileSink(const unsigned char* data, ssize_t len, void* token);
+static int GenerateTarget(FileContents* source_file,
+                          const Value* source_patch_value,
+                          FileContents* copy_file,
+                          const Value* copy_patch_value,
+                          const char* source_filename,
+                          const char* target_filename,
+                          const uint8_t target_sha1[SHA_DIGEST_LENGTH],
+                          size_t target_size,
+                          const Value* bonus_data);
+
+// Read a file into memory; store the file contents and associated
+// metadata in *file.
+//
+// Return 0 on success.
+int LoadFileContents(const char* filename, FileContents* file) {
+    // A special 'filename' beginning with "EMMC:" means to
+    // load the contents of a partition.
+    if (strncmp(filename, "EMMC:", 5) == 0) {
+        return LoadPartitionContents(filename, file);
+    }
+
+    if (stat(filename, &file->st) != 0) {
+        printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    std::vector<unsigned char> data(file->st.st_size);
+    FILE* f = ota_fopen(filename, "rb");
+    if (f == NULL) {
+        printf("failed to open \"%s\": %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    size_t bytes_read = ota_fread(data.data(), 1, data.size(), f);
+    if (bytes_read != data.size()) {
+        printf("short read of \"%s\" (%zu bytes of %zu)\n", filename, bytes_read, data.size());
+        ota_fclose(f);
+        return -1;
+    }
+    ota_fclose(f);
+    file->data = std::move(data);
+    SHA1(file->data.data(), file->data.size(), file->sha1);
+    return 0;
+}
+
+// Load the contents of an EMMC partition into the provided
+// FileContents.  filename should be a string of the form
+// "EMMC:<partition_device>:...".  The smallest size_n bytes for
+// which that prefix of the partition contents has the corresponding
+// sha1 hash will be loaded.  It is acceptable for a size value to be
+// repeated with different sha1s.  Will return 0 on success.
+//
+// This complexity is needed because if an OTA installation is
+// interrupted, the partition might contain either the source or the
+// target data, which might be of different lengths.  We need to know
+// the length in order to read from a partition (there is no
+// "end-of-file" marker), so the caller must specify the possible
+// lengths and the hash of the data, and we'll do the load expecting
+// to find one of those hashes.
+static int LoadPartitionContents(const char* filename, FileContents* file) {
+    std::string copy(filename);
+    std::vector<std::string> pieces = android::base::Split(copy, ":");
+    if (pieces.size() < 4 || pieces.size() % 2 != 0) {
+        printf("LoadPartitionContents called with bad filename (%s)\n", filename);
+        return -1;
+    }
+
+    if (pieces[0] != "EMMC") {
+        printf("LoadPartitionContents called with bad filename (%s)\n", filename);
+        return -1;
+    }
+    const char* partition = pieces[1].c_str();
+
+    size_t pairs = (pieces.size() - 2) / 2;    // # of (size, sha1) pairs in filename
+    std::vector<size_t> index(pairs);
+    std::vector<size_t> size(pairs);
+    std::vector<std::string> sha1sum(pairs);
+
+    for (size_t i = 0; i < pairs; ++i) {
+        size[i] = strtol(pieces[i*2+2].c_str(), NULL, 10);
+        if (size[i] == 0) {
+            printf("LoadPartitionContents called with bad size (%s)\n", filename);
+            return -1;
+        }
+        sha1sum[i] = pieces[i*2+3].c_str();
+        index[i] = i;
+    }
+
+    // Sort the index[] array so it indexes the pairs in order of increasing size.
+    sort(index.begin(), index.end(),
+        [&](const size_t& i, const size_t& j) {
+            return (size[i] < size[j]);
+        }
+    );
+
+    FILE* dev = ota_fopen(partition, "rb");
+    if (dev == NULL) {
+        printf("failed to open emmc partition \"%s\": %s\n", partition, strerror(errno));
+        return -1;
+    }
+
+    SHA_CTX sha_ctx;
+    SHA1_Init(&sha_ctx);
+    uint8_t parsed_sha[SHA_DIGEST_LENGTH];
+
+    // Allocate enough memory to hold the largest size.
+    std::vector<unsigned char> data(size[index[pairs-1]]);
+    char* p = reinterpret_cast<char*>(data.data());
+    size_t data_size = 0;                // # bytes read so far
+    bool found = false;
+
+    for (size_t i = 0; i < pairs; ++i) {
+        // Read enough additional bytes to get us up to the next size. (Again,
+        // we're trying the possibilities in order of increasing size).
+        size_t next = size[index[i]] - data_size;
+        if (next > 0) {
+            size_t read = ota_fread(p, 1, next, dev);
+            if (next != read) {
+                printf("short read (%zu bytes of %zu) for partition \"%s\"\n",
+                       read, next, partition);
+                return -1;
+            }
+            SHA1_Update(&sha_ctx, p, read);
+            data_size += read;
+            p += read;
+        }
+
+        // Duplicate the SHA context and finalize the duplicate so we can
+        // check it against this pair's expected hash.
+        SHA_CTX temp_ctx;
+        memcpy(&temp_ctx, &sha_ctx, sizeof(SHA_CTX));
+        uint8_t sha_so_far[SHA_DIGEST_LENGTH];
+        SHA1_Final(sha_so_far, &temp_ctx);
+
+        if (ParseSha1(sha1sum[index[i]].c_str(), parsed_sha) != 0) {
+            printf("failed to parse sha1 %s in %s\n", sha1sum[index[i]].c_str(), filename);
+            return -1;
+        }
+
+        if (memcmp(sha_so_far, parsed_sha, SHA_DIGEST_LENGTH) == 0) {
+            // we have a match.  stop reading the partition; we'll return
+            // the data we've read so far.
+            printf("partition read matched size %zu sha %s\n",
+                   size[index[i]], sha1sum[index[i]].c_str());
+            found = true;
+            break;
+        }
+    }
+
+    ota_fclose(dev);
+
+    if (!found) {
+        // Ran off the end of the list of (size,sha1) pairs without finding a match.
+        printf("contents of partition \"%s\" didn't match %s\n", partition, filename);
+        return -1;
+    }
+
+    SHA1_Final(file->sha1, &sha_ctx);
+
+    data.resize(data_size);
+    file->data = std::move(data);
+    // Fake some stat() info.
+    file->st.st_mode = 0644;
+    file->st.st_uid = 0;
+    file->st.st_gid = 0;
+
+    return 0;
+}
+
+
+// Save the contents of the given FileContents object under the given
+// filename.  Return 0 on success.
+int SaveFileContents(const char* filename, const FileContents* file) {
+    int fd = ota_open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR);
+    if (fd < 0) {
+        printf("failed to open \"%s\" for write: %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    ssize_t bytes_written = FileSink(file->data.data(), file->data.size(), &fd);
+    if (bytes_written != static_cast<ssize_t>(file->data.size())) {
+        printf("short write of \"%s\" (%zd bytes of %zu) (%s)\n",
+               filename, bytes_written, file->data.size(), strerror(errno));
+        ota_close(fd);
+        return -1;
+    }
+    if (ota_fsync(fd) != 0) {
+        printf("fsync of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
+    if (ota_close(fd) != 0) {
+        printf("close of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    if (chmod(filename, file->st.st_mode) != 0) {
+        printf("chmod of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
+    if (chown(filename, file->st.st_uid, file->st.st_gid) != 0) {
+        printf("chown of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    return 0;
+}
+
+// Write a memory buffer to 'target' partition, a string of the form
+// "EMMC:<partition_device>[:...]". The target name
+// might contain multiple colons, but WriteToPartition() only uses the first
+// two and ignores the rest. Return 0 on success.
+int WriteToPartition(const unsigned char* data, size_t len, const char* target) {
+    std::string copy(target);
+    std::vector<std::string> pieces = android::base::Split(copy, ":");
+
+    if (pieces.size() < 2) {
+        printf("WriteToPartition called with bad target (%s)\n", target);
+        return -1;
+    }
+
+    if (pieces[0] != "EMMC") {
+        printf("WriteToPartition called with bad target (%s)\n", target);
+        return -1;
+    }
+    const char* partition = pieces[1].c_str();
+
+    size_t start = 0;
+    bool success = false;
+    int fd = ota_open(partition, O_RDWR | O_SYNC);
+    if (fd < 0) {
+        printf("failed to open %s: %s\n", partition, strerror(errno));
+        return -1;
+    }
+
+    for (size_t attempt = 0; attempt < 2; ++attempt) {
+        if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) {
+            printf("failed seek on %s: %s\n", partition, strerror(errno));
+            return -1;
+        }
+        while (start < len) {
+            size_t to_write = len - start;
+            if (to_write > 1<<20) to_write = 1<<20;
+
+            ssize_t written = TEMP_FAILURE_RETRY(ota_write(fd, data+start, to_write));
+            if (written == -1) {
+                printf("failed write writing to %s: %s\n", partition, strerror(errno));
+                return -1;
+            }
+            start += written;
+        }
+        if (ota_fsync(fd) != 0) {
+            printf("failed to sync to %s (%s)\n", partition, strerror(errno));
+            return -1;
+        }
+        if (ota_close(fd) != 0) {
+            printf("failed to close %s (%s)\n", partition, strerror(errno));
+            return -1;
+        }
+        fd = ota_open(partition, O_RDONLY);
+        if (fd < 0) {
+            printf("failed to reopen %s for verify (%s)\n", partition, strerror(errno));
+            return -1;
+        }
+
+        // Drop caches so our subsequent verification read
+        // won't just be reading the cache.
+        sync();
+        int dc = ota_open("/proc/sys/vm/drop_caches", O_WRONLY);
+        if (TEMP_FAILURE_RETRY(ota_write(dc, "3\n", 2)) == -1) {
+            printf("write to /proc/sys/vm/drop_caches failed: %s\n", strerror(errno));
+        } else {
+            printf("  caches dropped\n");
+        }
+        ota_close(dc);
+        sleep(1);
+
+        // verify
+        if (TEMP_FAILURE_RETRY(lseek(fd, 0, SEEK_SET)) == -1) {
+            printf("failed to seek back to beginning of %s: %s\n",
+                   partition, strerror(errno));
+            return -1;
+        }
+        unsigned char buffer[4096];
+        start = len;
+        for (size_t p = 0; p < len; p += sizeof(buffer)) {
+            size_t to_read = len - p;
+            if (to_read > sizeof(buffer)) {
+                to_read = sizeof(buffer);
+            }
+
+            size_t so_far = 0;
+            while (so_far < to_read) {
+                ssize_t read_count =
+                    TEMP_FAILURE_RETRY(ota_read(fd, buffer+so_far, to_read-so_far));
+                if (read_count == -1) {
+                    printf("verify read error %s at %zu: %s\n",
+                           partition, p, strerror(errno));
+                    return -1;
+                }
+                if (static_cast<size_t>(read_count) < to_read) {
+                    printf("short verify read %s at %zu: %zd %zu %s\n",
+                           partition, p, read_count, to_read, strerror(errno));
+                }
+                so_far += read_count;
+            }
+
+            if (memcmp(buffer, data+p, to_read) != 0) {
+                printf("verification failed starting at %zu\n", p);
+                start = p;
+                break;
+            }
+        }
+
+        if (start == len) {
+            printf("verification read succeeded (attempt %zu)\n", attempt+1);
+            success = true;
+            break;
+        }
+    }
+
+    if (!success) {
+        printf("failed to verify after all attempts\n");
+        return -1;
+    }
+
+    if (ota_close(fd) != 0) {
+        printf("error closing %s (%s)\n", partition, strerror(errno));
+        return -1;
+    }
+    sync();
+
+    return 0;
+}
+
+
+// Take a string 'str' of 40 hex digits and parse it into the 20
+// byte array 'digest'.  'str' may contain only the digest or be of
+// the form "<digest>:<anything>".  Return 0 on success, -1 on any
+// error.
+int ParseSha1(const char* str, uint8_t* digest) {
+    const char* ps = str;
+    uint8_t* pd = digest;
+    for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) {
+        int digit;
+        if (*ps >= '0' && *ps <= '9') {
+            digit = *ps - '0';
+        } else if (*ps >= 'a' && *ps <= 'f') {
+            digit = *ps - 'a' + 10;
+        } else if (*ps >= 'A' && *ps <= 'F') {
+            digit = *ps - 'A' + 10;
+        } else {
+            return -1;
+        }
+        if (i % 2 == 0) {
+            *pd = digit << 4;
+        } else {
+            *pd |= digit;
+            ++pd;
+        }
+    }
+    if (*ps != '\0') return -1;
+    return 0;
+}
+
+// Search an array of sha1 strings for one matching the given sha1.
+// Return the index of the match on success, or -1 if no match is
+// found.
+int FindMatchingPatch(uint8_t* sha1, char* const * const patch_sha1_str,
+                      int num_patches) {
+    uint8_t patch_sha1[SHA_DIGEST_LENGTH];
+    for (int i = 0; i < num_patches; ++i) {
+        if (ParseSha1(patch_sha1_str[i], patch_sha1) == 0 &&
+            memcmp(patch_sha1, sha1, SHA_DIGEST_LENGTH) == 0) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+// Returns 0 if the contents of the file (argv[2]) or the cached file
+// match any of the sha1's on the command line (argv[3:]).  Returns
+// nonzero otherwise.
+int applypatch_check(const char* filename, int num_patches,
+                     char** const patch_sha1_str) {
+    FileContents file;
+
+    // It's okay to specify no sha1s; the check will pass if the
+    // LoadFileContents is successful.  (Useful for reading
+    // partitions, where the filename encodes the sha1s; no need to
+    // check them twice.)
+    if (LoadFileContents(filename, &file) != 0 ||
+        (num_patches > 0 &&
+         FindMatchingPatch(file.sha1, patch_sha1_str, num_patches) < 0)) {
+        printf("file \"%s\" doesn't have any of expected "
+               "sha1 sums; checking cache\n", filename);
+
+        // If the source file is missing or corrupted, it might be because
+        // we were killed in the middle of patching it.  A copy of it
+        // should have been made in CACHE_TEMP_SOURCE.  If that file
+        // exists and matches the sha1 we're looking for, the check still
+        // passes.
+
+        if (LoadFileContents(CACHE_TEMP_SOURCE, &file) != 0) {
+            printf("failed to load cache file\n");
+            return 1;
+        }
+
+        if (FindMatchingPatch(file.sha1, patch_sha1_str, num_patches) < 0) {
+            printf("cache bits don't match any sha1 for \"%s\"\n", filename);
+            return 1;
+        }
+    }
+    return 0;
+}
+
+int ShowLicenses() {
+    ShowBSDiffLicense();
+    return 0;
+}
+
+ssize_t FileSink(const unsigned char* data, ssize_t len, void* token) {
+    int fd = *static_cast<int*>(token);
+    ssize_t done = 0;
+    ssize_t wrote;
+    while (done < len) {
+        wrote = TEMP_FAILURE_RETRY(ota_write(fd, data+done, len-done));
+        if (wrote == -1) {
+            printf("error writing %zd bytes: %s\n", (len-done), strerror(errno));
+            return done;
+        }
+        done += wrote;
+    }
+    return done;
+}
+
+ssize_t MemorySink(const unsigned char* data, ssize_t len, void* token) {
+    std::string* s = static_cast<std::string*>(token);
+    s->append(reinterpret_cast<const char*>(data), len);
+    return len;
+}
+
+// Return the amount of free space (in bytes) on the filesystem
+// containing filename.  filename must exist.  Return -1 on error.
+size_t FreeSpaceForFile(const char* filename) {
+    struct statfs sf;
+    if (statfs(filename, &sf) != 0) {
+        printf("failed to statfs %s: %s\n", filename, strerror(errno));
+        return -1;
+    }
+    return sf.f_bsize * sf.f_bavail;
+}
+
+int CacheSizeCheck(size_t bytes) {
+    if (MakeFreeSpaceOnCache(bytes) < 0) {
+        printf("unable to make %zu bytes available on /cache\n", bytes);
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+// This function applies binary patches to files in a way that is safe
+// (the original file is not touched until we have the desired
+// replacement for it) and idempotent (it's okay to run this program
+// multiple times).
+//
+// - if the sha1 hash of <target_filename> is <target_sha1_string>,
+//   does nothing and exits successfully.
+//
+// - otherwise, if the sha1 hash of <source_filename> is one of the
+//   entries in <patch_sha1_str>, the corresponding patch from
+//   <patch_data> (which must be a VAL_BLOB) is applied to produce a
+//   new file (the type of patch is automatically detected from the
+//   blob data).  If that new file has sha1 hash <target_sha1_str>,
+//   moves it to replace <target_filename>, and exits successfully.
+//   Note that if <source_filename> and <target_filename> are not the
+//   same, <source_filename> is NOT deleted on success.
+//   <target_filename> may be the string "-" to mean "the same as
+//   source_filename".
+//
+// - otherwise, or if any error is encountered, exits with non-zero
+//   status.
+//
+// <source_filename> may refer to a partition to read the source data.
+// See the comments for the LoadPartitionContents() function above
+// for the format of such a filename.
+
+int applypatch(const char* source_filename,
+               const char* target_filename,
+               const char* target_sha1_str,
+               size_t target_size,
+               int num_patches,
+               char** const patch_sha1_str,
+               Value** patch_data,
+               Value* bonus_data) {
+    printf("patch %s: ", source_filename);
+
+    if (target_filename[0] == '-' && target_filename[1] == '\0') {
+        target_filename = source_filename;
+    }
+
+    uint8_t target_sha1[SHA_DIGEST_LENGTH];
+    if (ParseSha1(target_sha1_str, target_sha1) != 0) {
+        printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
+        return 1;
+    }
+
+    FileContents copy_file;
+    FileContents source_file;
+    const Value* source_patch_value = NULL;
+    const Value* copy_patch_value = NULL;
+
+    // We try to load the target file into the source_file object.
+    if (LoadFileContents(target_filename, &source_file) == 0) {
+        if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
+            // The early-exit case:  the patch was already applied, this file
+            // has the desired hash, nothing for us to do.
+            printf("already %s\n", short_sha1(target_sha1).c_str());
+            return 0;
+        }
+    }
+
+    if (source_file.data.empty() ||
+        (target_filename != source_filename &&
+         strcmp(target_filename, source_filename) != 0)) {
+        // Need to load the source file:  either we failed to load the
+        // target file, or we did but it's different from the source file.
+        source_file.data.clear();
+        LoadFileContents(source_filename, &source_file);
+    }
+
+    if (!source_file.data.empty()) {
+        int to_use = FindMatchingPatch(source_file.sha1, patch_sha1_str, num_patches);
+        if (to_use >= 0) {
+            source_patch_value = patch_data[to_use];
+        }
+    }
+
+    if (source_patch_value == NULL) {
+        source_file.data.clear();
+        printf("source file is bad; trying copy\n");
+
+        if (LoadFileContents(CACHE_TEMP_SOURCE, &copy_file) < 0) {
+            // fail.
+            printf("failed to read copy file\n");
+            return 1;
+        }
+
+        int to_use = FindMatchingPatch(copy_file.sha1, patch_sha1_str, num_patches);
+        if (to_use >= 0) {
+            copy_patch_value = patch_data[to_use];
+        }
+
+        if (copy_patch_value == NULL) {
+            // fail.
+            printf("copy file doesn't match source SHA-1s either\n");
+            return 1;
+        }
+    }
+
+    return GenerateTarget(&source_file, source_patch_value,
+                          &copy_file, copy_patch_value,
+                          source_filename, target_filename,
+                          target_sha1, target_size, bonus_data);
+}
+
+/*
+ * This function flashes a given image to the target partition. It verifies
+ * the target cheksum first, and will return if target has the desired hash.
+ * It checks the checksum of the given source image before flashing, and
+ * verifies the target partition afterwards. The function is idempotent.
+ * Returns zero on success.
+ */
+int applypatch_flash(const char* source_filename, const char* target_filename,
+                     const char* target_sha1_str, size_t target_size) {
+    printf("flash %s: ", target_filename);
+
+    uint8_t target_sha1[SHA_DIGEST_LENGTH];
+    if (ParseSha1(target_sha1_str, target_sha1) != 0) {
+        printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
+        return 1;
+    }
+
+    FileContents source_file;
+    std::string target_str(target_filename);
+
+    std::vector<std::string> pieces = android::base::Split(target_str, ":");
+    if (pieces.size() != 2 || pieces[0] != "EMMC") {
+        printf("invalid target name \"%s\"", target_filename);
+        return 1;
+    }
+
+    // Load the target into the source_file object to see if already applied.
+    pieces.push_back(std::to_string(target_size));
+    pieces.push_back(target_sha1_str);
+    std::string fullname = android::base::Join(pieces, ':');
+    if (LoadPartitionContents(fullname.c_str(), &source_file) == 0 &&
+        memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
+        // The early-exit case: the image was already applied, this partition
+        // has the desired hash, nothing for us to do.
+        printf("already %s\n", short_sha1(target_sha1).c_str());
+        return 0;
+    }
+
+    if (LoadFileContents(source_filename, &source_file) == 0) {
+        if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
+            // The source doesn't have desired checksum.
+            printf("source \"%s\" doesn't have expected sha1 sum\n", source_filename);
+            printf("expected: %s, found: %s\n", short_sha1(target_sha1).c_str(),
+                    short_sha1(source_file.sha1).c_str());
+            return 1;
+        }
+    }
+
+    if (WriteToPartition(source_file.data.data(), target_size, target_filename) != 0) {
+        printf("write of copied data to %s failed\n", target_filename);
+        return 1;
+    }
+    return 0;
+}
+
+static int GenerateTarget(FileContents* source_file,
+                          const Value* source_patch_value,
+                          FileContents* copy_file,
+                          const Value* copy_patch_value,
+                          const char* source_filename,
+                          const char* target_filename,
+                          const uint8_t target_sha1[SHA_DIGEST_LENGTH],
+                          size_t target_size,
+                          const Value* bonus_data) {
+    int retry = 1;
+    SHA_CTX ctx;
+    std::string memory_sink_str;
+    FileContents* source_to_use;
+    int made_copy = 0;
+
+    bool target_is_partition = (strncmp(target_filename, "EMMC:", 5) == 0);
+    const std::string tmp_target_filename = std::string(target_filename) + ".patch";
+
+    // assume that target_filename (eg "/system/app/Foo.apk") is located
+    // on the same filesystem as its top-level directory ("/system").
+    // We need something that exists for calling statfs().
+    std::string target_fs = target_filename;
+    auto slash_pos = target_fs.find('/', 1);
+    if (slash_pos != std::string::npos) {
+        target_fs.resize(slash_pos);
+    }
+
+    const Value* patch;
+    if (source_patch_value != NULL) {
+        source_to_use = source_file;
+        patch = source_patch_value;
+    } else {
+        source_to_use = copy_file;
+        patch = copy_patch_value;
+    }
+    if (patch->type != VAL_BLOB) {
+        printf("patch is not a blob\n");
+        return 1;
+    }
+    char* header = patch->data;
+    ssize_t header_bytes_read = patch->size;
+    bool use_bsdiff = false;
+    if (header_bytes_read >= 8 && memcmp(header, "BSDIFF40", 8) == 0) {
+        use_bsdiff = true;
+    } else if (header_bytes_read >= 8 && memcmp(header, "IMGDIFF2", 8) == 0) {
+        use_bsdiff = false;
+    } else {
+        printf("Unknown patch file format\n");
+        return 1;
+    }
+
+    do {
+        // Is there enough room in the target filesystem to hold the patched
+        // file?
+
+        if (target_is_partition) {
+            // If the target is a partition, we're actually going to
+            // write the output to /tmp and then copy it to the
+            // partition.  statfs() always returns 0 blocks free for
+            // /tmp, so instead we'll just assume that /tmp has enough
+            // space to hold the file.
+
+            // We still write the original source to cache, in case
+            // the partition write is interrupted.
+            if (MakeFreeSpaceOnCache(source_file->data.size()) < 0) {
+                printf("not enough free space on /cache\n");
+                return 1;
+            }
+            if (SaveFileContents(CACHE_TEMP_SOURCE, source_file) < 0) {
+                printf("failed to back up source file\n");
+                return 1;
+            }
+            made_copy = 1;
+            retry = 0;
+        } else {
+            int enough_space = 0;
+            if (retry > 0) {
+                size_t free_space = FreeSpaceForFile(target_fs.c_str());
+                enough_space =
+                    (free_space > (256 << 10)) &&          // 256k (two-block) minimum
+                    (free_space > (target_size * 3 / 2));  // 50% margin of error
+                if (!enough_space) {
+                    printf("target %zu bytes; free space %zu bytes; retry %d; enough %d\n",
+                           target_size, free_space, retry, enough_space);
+                }
+            }
+
+            if (!enough_space) {
+                retry = 0;
+            }
+
+            if (!enough_space && source_patch_value != NULL) {
+                // Using the original source, but not enough free space.  First
+                // copy the source file to cache, then delete it from the original
+                // location.
+
+                if (strncmp(source_filename, "EMMC:", 5) == 0) {
+                    // It's impossible to free space on the target filesystem by
+                    // deleting the source if the source is a partition.  If
+                    // we're ever in a state where we need to do this, fail.
+                    printf("not enough free space for target but source is partition\n");
+                    return 1;
+                }
+
+                if (MakeFreeSpaceOnCache(source_file->data.size()) < 0) {
+                    printf("not enough free space on /cache\n");
+                    return 1;
+                }
+
+                if (SaveFileContents(CACHE_TEMP_SOURCE, source_file) < 0) {
+                    printf("failed to back up source file\n");
+                    return 1;
+                }
+                made_copy = 1;
+                unlink(source_filename);
+
+                size_t free_space = FreeSpaceForFile(target_fs.c_str());
+                printf("(now %zu bytes free for target) ", free_space);
+            }
+        }
+
+
+        SinkFn sink = NULL;
+        void* token = NULL;
+        int output_fd = -1;
+        if (target_is_partition) {
+            // We store the decoded output in memory.
+            sink = MemorySink;
+            token = &memory_sink_str;
+        } else {
+            // We write the decoded output to "<tgt-file>.patch".
+            output_fd = ota_open(tmp_target_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_SYNC,
+                                 S_IRUSR | S_IWUSR);
+            if (output_fd < 0) {
+                printf("failed to open output file %s: %s\n", tmp_target_filename.c_str(),
+                       strerror(errno));
+                return 1;
+            }
+            sink = FileSink;
+            token = &output_fd;
+        }
+
+
+        SHA1_Init(&ctx);
+
+        int result;
+        if (use_bsdiff) {
+            result = ApplyBSDiffPatch(source_to_use->data.data(), source_to_use->data.size(),
+                                      patch, 0, sink, token, &ctx);
+        } else {
+            result = ApplyImagePatch(source_to_use->data.data(), source_to_use->data.size(),
+                                     patch, sink, token, &ctx, bonus_data);
+        }
+
+        if (!target_is_partition) {
+            if (ota_fsync(output_fd) != 0) {
+                printf("failed to fsync file \"%s\" (%s)\n", tmp_target_filename.c_str(),
+                       strerror(errno));
+                result = 1;
+            }
+            if (ota_close(output_fd) != 0) {
+                printf("failed to close file \"%s\" (%s)\n", tmp_target_filename.c_str(),
+                       strerror(errno));
+                result = 1;
+            }
+        }
+
+        if (result != 0) {
+            if (retry == 0) {
+                printf("applying patch failed\n");
+                return result != 0;
+            } else {
+                printf("applying patch failed; retrying\n");
+            }
+            if (!target_is_partition) {
+                unlink(tmp_target_filename.c_str());
+            }
+        } else {
+            // succeeded; no need to retry
+            break;
+        }
+    } while (retry-- > 0);
+
+    uint8_t current_target_sha1[SHA_DIGEST_LENGTH];
+    SHA1_Final(current_target_sha1, &ctx);
+    if (memcmp(current_target_sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
+        printf("patch did not produce expected sha1\n");
+        return 1;
+    } else {
+        printf("now %s\n", short_sha1(target_sha1).c_str());
+    }
+
+    if (target_is_partition) {
+        // Copy the temp file to the partition.
+        if (WriteToPartition(reinterpret_cast<const unsigned char*>(memory_sink_str.c_str()),
+                             memory_sink_str.size(), target_filename) != 0) {
+            printf("write of patched data to %s failed\n", target_filename);
+            return 1;
+        }
+    } else {
+        // Give the .patch file the same owner, group, and mode of the
+        // original source file.
+        if (chmod(tmp_target_filename.c_str(), source_to_use->st.st_mode) != 0) {
+            printf("chmod of \"%s\" failed: %s\n", tmp_target_filename.c_str(), strerror(errno));
+            return 1;
+        }
+        if (chown(tmp_target_filename.c_str(), source_to_use->st.st_uid, source_to_use->st.st_gid) != 0) {
+            printf("chown of \"%s\" failed: %s\n", tmp_target_filename.c_str(), strerror(errno));
+            return 1;
+        }
+
+        // Finally, rename the .patch file to replace the target file.
+        if (rename(tmp_target_filename.c_str(), target_filename) != 0) {
+            printf("rename of .patch to \"%s\" failed: %s\n", target_filename, strerror(errno));
+            return 1;
+        }
+    }
+
+    // If this run of applypatch created the copy, and we're here, we
+    // can delete it.
+    if (made_copy) {
+        unlink(CACHE_TEMP_SOURCE);
+    }
+
+    // Success!
+    return 0;
+}
diff --git a/recovery/applypatch/applypatch.sh b/recovery/applypatch/applypatch.sh
new file mode 100755
index 0000000..8ea68a1
--- /dev/null
+++ b/recovery/applypatch/applypatch.sh
@@ -0,0 +1,350 @@
+#!/bin/bash
+#
+# A test suite for applypatch.  Run in a client where you have done
+# envsetup, choosecombo, etc.
+#
+# DO NOT RUN THIS ON A DEVICE YOU CARE ABOUT.  It will mess up your
+# system partition.
+#
+#
+# TODO: find some way to get this run regularly along with the rest of
+# the tests.
+
+EMULATOR_PORT=5580
+DATA_DIR=$ANDROID_BUILD_TOP/bootable/recovery/applypatch/testdata
+
+# This must be the filename that applypatch uses for its copies.
+CACHE_TEMP_SOURCE=/cache/saved.file
+
+# Put all binaries and files here.  We use /cache because it's a
+# temporary filesystem in the emulator; it's created fresh each time
+# the emulator starts.
+WORK_DIR=/system
+
+# partition that WORK_DIR is located on, without the leading slash
+WORK_FS=system
+
+# set to 0 to use a device instead
+USE_EMULATOR=1
+
+# ------------------------
+
+tmpdir=$(mktemp -d)
+
+if [ "$USE_EMULATOR" == 1 ]; then
+  emulator -wipe-data -noaudio -no-window -port $EMULATOR_PORT &
+  pid_emulator=$!
+  ADB="adb -s emulator-$EMULATOR_PORT "
+else
+  ADB="adb -d "
+fi
+
+echo "waiting to connect to device"
+$ADB wait-for-device
+echo "device is available"
+$ADB remount
+# free up enough space on the system partition for the test to run.
+$ADB shell rm -r /system/media
+
+# run a command on the device; exit with the exit status of the device
+# command.
+run_command() {
+  $ADB shell "$@" \; echo \$? | awk '{if (b) {print a}; a=$0; b=1} END {exit a}'
+}
+
+testname() {
+  echo
+  echo "$1"...
+  testname="$1"
+}
+
+fail() {
+  echo
+  echo FAIL: $testname
+  echo
+  [ "$open_pid" == "" ] || kill $open_pid
+  [ "$pid_emulator" == "" ] || kill $pid_emulator
+  exit 1
+}
+
+sha1() {
+  sha1sum $1 | awk '{print $1}'
+}
+
+free_space() {
+  run_command df | awk "/$1/ {print gensub(/K/, \"\", \"g\", \$6)}"
+}
+
+cleanup() {
+  # not necessary if we're about to kill the emulator, but nice for
+  # running on real devices or already-running emulators.
+  testname "removing test files"
+  run_command rm $WORK_DIR/bloat.dat
+  run_command rm $WORK_DIR/old.file
+  run_command rm $WORK_DIR/foo
+  run_command rm $WORK_DIR/patch.bsdiff
+  run_command rm $WORK_DIR/applypatch
+  run_command rm $CACHE_TEMP_SOURCE
+  run_command rm /cache/bloat*.dat
+
+  [ "$pid_emulator" == "" ] || kill $pid_emulator
+
+  if [ $# == 0 ]; then
+    rm -rf $tmpdir
+  fi
+}
+
+cleanup leave_tmp
+
+$ADB push $ANDROID_PRODUCT_OUT/system/bin/applypatch $WORK_DIR/applypatch
+
+BAD1_SHA1=$(printf "%040x" $RANDOM)
+BAD2_SHA1=$(printf "%040x" $RANDOM)
+OLD_SHA1=$(sha1 $DATA_DIR/old.file)
+NEW_SHA1=$(sha1 $DATA_DIR/new.file)
+NEW_SIZE=$(stat -c %s $DATA_DIR/new.file)
+
+# --------------- basic execution ----------------------
+
+testname "usage message"
+run_command $WORK_DIR/applypatch && fail
+
+testname "display license"
+run_command $WORK_DIR/applypatch -l | grep -q -i copyright || fail
+
+
+# --------------- check mode ----------------------
+
+$ADB push $DATA_DIR/old.file $WORK_DIR
+
+testname "check mode single"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $OLD_SHA1 || fail
+
+testname "check mode multiple"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD1_SHA1 $OLD_SHA1 $BAD2_SHA1|| fail
+
+testname "check mode failure"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD2_SHA1 $BAD1_SHA1 && fail
+
+$ADB push $DATA_DIR/old.file $CACHE_TEMP_SOURCE
+# put some junk in the old file
+run_command dd if=/dev/urandom of=$WORK_DIR/old.file count=100 bs=1024 || fail
+
+testname "check mode cache (corrupted) single"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $OLD_SHA1 || fail
+
+testname "check mode cache (corrupted) multiple"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD1_SHA1 $OLD_SHA1 $BAD2_SHA1|| fail
+
+testname "check mode cache (corrupted) failure"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD2_SHA1 $BAD1_SHA1 && fail
+
+# remove the old file entirely
+run_command rm $WORK_DIR/old.file
+
+testname "check mode cache (missing) single"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $OLD_SHA1 || fail
+
+testname "check mode cache (missing) multiple"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD1_SHA1 $OLD_SHA1 $BAD2_SHA1|| fail
+
+testname "check mode cache (missing) failure"
+run_command $WORK_DIR/applypatch -c $WORK_DIR/old.file $BAD2_SHA1 $BAD1_SHA1 && fail
+
+
+# --------------- apply patch ----------------------
+
+$ADB push $DATA_DIR/old.file $WORK_DIR
+$ADB push $DATA_DIR/patch.bsdiff $WORK_DIR
+echo hello > $tmpdir/foo
+$ADB push $tmpdir/foo $WORK_DIR
+
+# Check that the partition has enough space to apply the patch without
+# copying.  If it doesn't, we'll be testing the low-space condition
+# when we intend to test the not-low-space condition.
+testname "apply patches (with enough space)"
+free_kb=$(free_space $WORK_FS)
+echo "${free_kb}kb free on /$WORK_FS."
+if (( free_kb * 1024 < NEW_SIZE * 3 / 2 )); then
+  echo "Not enough space on /$WORK_FS to patch test file."
+  echo
+  echo "This doesn't mean that applypatch is necessarily broken;"
+  echo "just that /$WORK_FS doesn't have enough free space to"
+  echo "properly run this test."
+  exit 1
+fi
+
+testname "apply bsdiff patch"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+testname "reapply bsdiff patch"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+
+# --------------- apply patch in new location ----------------------
+
+$ADB push $DATA_DIR/old.file $WORK_DIR
+$ADB push $DATA_DIR/patch.bsdiff $WORK_DIR
+
+# Check that the partition has enough space to apply the patch without
+# copying.  If it doesn't, we'll be testing the low-space condition
+# when we intend to test the not-low-space condition.
+testname "apply patch to new location (with enough space)"
+free_kb=$(free_space $WORK_FS)
+echo "${free_kb}kb free on /$WORK_FS."
+if (( free_kb * 1024 < NEW_SIZE * 3 / 2 )); then
+  echo "Not enough space on /$WORK_FS to patch test file."
+  echo
+  echo "This doesn't mean that applypatch is necessarily broken;"
+  echo "just that /$WORK_FS doesn't have enough free space to"
+  echo "properly run this test."
+  exit 1
+fi
+
+run_command rm $WORK_DIR/new.file
+run_command rm $CACHE_TEMP_SOURCE
+
+testname "apply bsdiff patch to new location"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file $WORK_DIR/new.file $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/new.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+testname "reapply bsdiff patch to new location"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file $WORK_DIR/new.file $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/new.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+$ADB push $DATA_DIR/old.file $CACHE_TEMP_SOURCE
+# put some junk in the old file
+run_command dd if=/dev/urandom of=$WORK_DIR/old.file count=100 bs=1024 || fail
+
+testname "apply bsdiff patch to new location with corrupted source"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file $WORK_DIR/new.file $NEW_SHA1 $NEW_SIZE $OLD_SHA1:$WORK_DIR/patch.bsdiff $BAD1_SHA1:$WORK_DIR/foo || fail
+$ADB pull $WORK_DIR/new.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+# put some junk in the cache copy, too
+run_command dd if=/dev/urandom of=$CACHE_TEMP_SOURCE count=100 bs=1024 || fail
+
+run_command rm $WORK_DIR/new.file
+testname "apply bsdiff patch to new location with corrupted source and copy (no new file)"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file $WORK_DIR/new.file $NEW_SHA1 $NEW_SIZE $OLD_SHA1:$WORK_DIR/patch.bsdiff $BAD1_SHA1:$WORK_DIR/foo && fail
+
+# put some junk in the new file
+run_command dd if=/dev/urandom of=$WORK_DIR/new.file count=100 bs=1024 || fail
+
+testname "apply bsdiff patch to new location with corrupted source and copy (bad new file)"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file $WORK_DIR/new.file $NEW_SHA1 $NEW_SIZE $OLD_SHA1:$WORK_DIR/patch.bsdiff $BAD1_SHA1:$WORK_DIR/foo && fail
+
+# --------------- apply patch with low space on /system ----------------------
+
+$ADB push $DATA_DIR/old.file $WORK_DIR
+$ADB push $DATA_DIR/patch.bsdiff $WORK_DIR
+
+free_kb=$(free_space $WORK_FS)
+echo "${free_kb}kb free on /$WORK_FS; we'll soon fix that."
+echo run_command dd if=/dev/zero of=$WORK_DIR/bloat.dat count=$((free_kb-512)) bs=1024 || fail
+run_command dd if=/dev/zero of=$WORK_DIR/bloat.dat count=$((free_kb-512)) bs=1024 || fail
+free_kb=$(free_space $WORK_FS)
+echo "${free_kb}kb free on /$WORK_FS now."
+
+testname "apply bsdiff patch with low space"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+testname "reapply bsdiff patch with low space"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+# --------------- apply patch with low space on /system and /cache ----------------------
+
+$ADB push $DATA_DIR/old.file $WORK_DIR
+$ADB push $DATA_DIR/patch.bsdiff $WORK_DIR
+
+free_kb=$(free_space $WORK_FS)
+echo "${free_kb}kb free on /$WORK_FS"
+
+run_command mkdir /cache/subdir
+run_command 'echo > /cache/subdir/a.file'
+run_command 'echo > /cache/a.file'
+run_command mkdir /cache/recovery /cache/recovery/otatest
+run_command 'echo > /cache/recovery/otatest/b.file'
+run_command "echo > $CACHE_TEMP_SOURCE"
+free_kb=$(free_space cache)
+echo "${free_kb}kb free on /cache; we'll soon fix that."
+run_command dd if=/dev/zero of=/cache/bloat_small.dat count=128 bs=1024 || fail
+run_command dd if=/dev/zero of=/cache/bloat_large.dat count=$((free_kb-640)) bs=1024 || fail
+free_kb=$(free_space cache)
+echo "${free_kb}kb free on /cache now."
+
+testname "apply bsdiff patch with low space, full cache, can't delete enough"
+$ADB shell 'cat >> /cache/bloat_large.dat' & open_pid=$!
+echo "open_pid is $open_pid"
+
+# size check should fail even though it deletes some stuff
+run_command $WORK_DIR/applypatch -s $NEW_SIZE && fail
+run_command ls /cache/bloat_small.dat && fail          # was deleted
+run_command ls /cache/a.file && fail                   # was deleted
+run_command ls /cache/recovery/otatest/b.file && fail  # was deleted
+run_command ls /cache/bloat_large.dat || fail          # wasn't deleted because it was open
+run_command ls /cache/subdir/a.file || fail            # wasn't deleted because it's in a subdir
+run_command ls $CACHE_TEMP_SOURCE || fail              # wasn't deleted because it's the source file copy
+
+# should fail; not enough files can be deleted
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff && fail
+run_command ls /cache/bloat_large.dat || fail   # wasn't deleted because it was open
+run_command ls /cache/subdir/a.file || fail     # wasn't deleted because it's in a subdir
+run_command ls $CACHE_TEMP_SOURCE || fail       # wasn't deleted because it's the source file copy
+
+kill $open_pid   # /cache/bloat_large.dat is no longer open
+
+testname "apply bsdiff patch with low space, full cache, can delete enough"
+
+# should succeed after deleting /cache/bloat_large.dat
+run_command $WORK_DIR/applypatch -s $NEW_SIZE || fail
+run_command ls /cache/bloat_large.dat && fail   # was deleted
+run_command ls /cache/subdir/a.file || fail     # still wasn't deleted because it's in a subdir
+run_command ls $CACHE_TEMP_SOURCE || fail       # wasn't deleted because it's the source file copy
+
+# should succeed
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+run_command ls /cache/subdir/a.file || fail     # still wasn't deleted because it's in a subdir
+run_command ls $CACHE_TEMP_SOURCE && fail       # was deleted because patching overwrote it, then deleted it
+
+# --------------- apply patch from cache ----------------------
+
+$ADB push $DATA_DIR/old.file $CACHE_TEMP_SOURCE
+# put some junk in the old file
+run_command dd if=/dev/urandom of=$WORK_DIR/old.file count=100 bs=1024 || fail
+
+testname "apply bsdiff patch from cache (corrupted source) with low space"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+$ADB push $DATA_DIR/old.file $CACHE_TEMP_SOURCE
+# remove the old file entirely
+run_command rm $WORK_DIR/old.file
+
+testname "apply bsdiff patch from cache (missing source) with low space"
+run_command $WORK_DIR/applypatch $WORK_DIR/old.file - $NEW_SHA1 $NEW_SIZE $BAD1_SHA1:$WORK_DIR/foo $OLD_SHA1:$WORK_DIR/patch.bsdiff || fail
+$ADB pull $WORK_DIR/old.file $tmpdir/patched
+diff -q $DATA_DIR/new.file $tmpdir/patched || fail
+
+
+# --------------- cleanup ----------------------
+
+cleanup
+
+echo
+echo PASS
+echo
+
diff --git a/recovery/applypatch/bspatch.cpp b/recovery/applypatch/bspatch.cpp
new file mode 100644
index 0000000..a4945da
--- /dev/null
+++ b/recovery/applypatch/bspatch.cpp
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+// This file is a nearly line-for-line copy of bspatch.c from the
+// bsdiff-4.3 distribution; the primary differences being how the
+// input and output data are read and the error handling.  Running
+// applypatch with the -l option will display the bsdiff license
+// notice.
+
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <bzlib.h>
+
+#include "openssl/sha.h"
+#include "applypatch/applypatch.h"
+
+void ShowBSDiffLicense() {
+    puts("The bsdiff library used herein is:\n"
+         "\n"
+         "Copyright 2003-2005 Colin Percival\n"
+         "All rights reserved\n"
+         "\n"
+         "Redistribution and use in source and binary forms, with or without\n"
+         "modification, are permitted providing that the following conditions\n"
+         "are met:\n"
+         "1. Redistributions of source code must retain the above copyright\n"
+         "   notice, this list of conditions and the following disclaimer.\n"
+         "2. Redistributions in binary form must reproduce the above copyright\n"
+         "   notice, this list of conditions and the following disclaimer in the\n"
+         "   documentation and/or other materials provided with the distribution.\n"
+         "\n"
+         "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
+         "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n"
+         "WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n"
+         "ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\n"
+         "DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n"
+         "DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n"
+         "OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n"
+         "HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n"
+         "STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\n"
+         "IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n"
+         "POSSIBILITY OF SUCH DAMAGE.\n"
+         "\n------------------\n\n"
+         "This program uses Julian R Seward's \"libbzip2\" library, available\n"
+         "from http://www.bzip.org/.\n"
+        );
+}
+
+static off_t offtin(u_char *buf)
+{
+    off_t y;
+
+    y=buf[7]&0x7F;
+    y=y*256;y+=buf[6];
+    y=y*256;y+=buf[5];
+    y=y*256;y+=buf[4];
+    y=y*256;y+=buf[3];
+    y=y*256;y+=buf[2];
+    y=y*256;y+=buf[1];
+    y=y*256;y+=buf[0];
+
+    if(buf[7]&0x80) y=-y;
+
+    return y;
+}
+
+int FillBuffer(unsigned char* buffer, int size, bz_stream* stream) {
+    stream->next_out = (char*)buffer;
+    stream->avail_out = size;
+    while (stream->avail_out > 0) {
+        int bzerr = BZ2_bzDecompress(stream);
+        if (bzerr != BZ_OK && bzerr != BZ_STREAM_END) {
+            printf("bz error %d decompressing\n", bzerr);
+            return -1;
+        }
+        if (stream->avail_out > 0) {
+            printf("need %d more bytes\n", stream->avail_out);
+        }
+    }
+    return 0;
+}
+
+int ApplyBSDiffPatch(const unsigned char* old_data, ssize_t old_size,
+                     const Value* patch, ssize_t patch_offset,
+                     SinkFn sink, void* token, SHA_CTX* ctx) {
+
+    std::vector<unsigned char> new_data;
+    if (ApplyBSDiffPatchMem(old_data, old_size, patch, patch_offset, &new_data) != 0) {
+        return -1;
+    }
+
+    if (sink(new_data.data(), new_data.size(), token) < static_cast<ssize_t>(new_data.size())) {
+        printf("short write of output: %d (%s)\n", errno, strerror(errno));
+        return 1;
+    }
+    if (ctx) SHA1_Update(ctx, new_data.data(), new_data.size());
+    return 0;
+}
+
+int ApplyBSDiffPatchMem(const unsigned char* old_data, ssize_t old_size,
+                        const Value* patch, ssize_t patch_offset,
+                        std::vector<unsigned char>* new_data) {
+    // Patch data format:
+    //   0       8       "BSDIFF40"
+    //   8       8       X
+    //   16      8       Y
+    //   24      8       sizeof(newfile)
+    //   32      X       bzip2(control block)
+    //   32+X    Y       bzip2(diff block)
+    //   32+X+Y  ???     bzip2(extra block)
+    // with control block a set of triples (x,y,z) meaning "add x bytes
+    // from oldfile to x bytes from the diff block; copy y bytes from the
+    // extra block; seek forwards in oldfile by z bytes".
+
+    unsigned char* header = (unsigned char*) patch->data + patch_offset;
+    if (memcmp(header, "BSDIFF40", 8) != 0) {
+        printf("corrupt bsdiff patch file header (magic number)\n");
+        return 1;
+    }
+
+    ssize_t ctrl_len, data_len, new_size;
+    ctrl_len = offtin(header+8);
+    data_len = offtin(header+16);
+    new_size = offtin(header+24);
+
+    if (ctrl_len < 0 || data_len < 0 || new_size < 0) {
+        printf("corrupt patch file header (data lengths)\n");
+        return 1;
+    }
+
+    int bzerr;
+
+    bz_stream cstream;
+    cstream.next_in = patch->data + patch_offset + 32;
+    cstream.avail_in = ctrl_len;
+    cstream.bzalloc = NULL;
+    cstream.bzfree = NULL;
+    cstream.opaque = NULL;
+    if ((bzerr = BZ2_bzDecompressInit(&cstream, 0, 0)) != BZ_OK) {
+        printf("failed to bzinit control stream (%d)\n", bzerr);
+    }
+
+    bz_stream dstream;
+    dstream.next_in = patch->data + patch_offset + 32 + ctrl_len;
+    dstream.avail_in = data_len;
+    dstream.bzalloc = NULL;
+    dstream.bzfree = NULL;
+    dstream.opaque = NULL;
+    if ((bzerr = BZ2_bzDecompressInit(&dstream, 0, 0)) != BZ_OK) {
+        printf("failed to bzinit diff stream (%d)\n", bzerr);
+    }
+
+    bz_stream estream;
+    estream.next_in = patch->data + patch_offset + 32 + ctrl_len + data_len;
+    estream.avail_in = patch->size - (patch_offset + 32 + ctrl_len + data_len);
+    estream.bzalloc = NULL;
+    estream.bzfree = NULL;
+    estream.opaque = NULL;
+    if ((bzerr = BZ2_bzDecompressInit(&estream, 0, 0)) != BZ_OK) {
+        printf("failed to bzinit extra stream (%d)\n", bzerr);
+    }
+
+    new_data->resize(new_size);
+
+    off_t oldpos = 0, newpos = 0;
+    off_t ctrl[3];
+    int i;
+    unsigned char buf[24];
+    while (newpos < new_size) {
+        // Read control data
+        if (FillBuffer(buf, 24, &cstream) != 0) {
+            printf("error while reading control stream\n");
+            return 1;
+        }
+        ctrl[0] = offtin(buf);
+        ctrl[1] = offtin(buf+8);
+        ctrl[2] = offtin(buf+16);
+
+        if (ctrl[0] < 0 || ctrl[1] < 0) {
+            printf("corrupt patch (negative byte counts)\n");
+            return 1;
+        }
+
+        // Sanity check
+        if (newpos + ctrl[0] > new_size) {
+            printf("corrupt patch (new file overrun)\n");
+            return 1;
+        }
+
+        // Read diff string
+        if (FillBuffer(new_data->data() + newpos, ctrl[0], &dstream) != 0) {
+            printf("error while reading diff stream\n");
+            return 1;
+        }
+
+        // Add old data to diff string
+        for (i = 0; i < ctrl[0]; ++i) {
+            if ((oldpos+i >= 0) && (oldpos+i < old_size)) {
+                (*new_data)[newpos+i] += old_data[oldpos+i];
+            }
+        }
+
+        // Adjust pointers
+        newpos += ctrl[0];
+        oldpos += ctrl[0];
+
+        // Sanity check
+        if (newpos + ctrl[1] > new_size) {
+            printf("corrupt patch (new file overrun)\n");
+            return 1;
+        }
+
+        // Read extra string
+        if (FillBuffer(new_data->data() + newpos, ctrl[1], &estream) != 0) {
+            printf("error while reading extra stream\n");
+            return 1;
+        }
+
+        // Adjust pointers
+        newpos += ctrl[1];
+        oldpos += ctrl[2];
+    }
+
+    BZ2_bzDecompressEnd(&cstream);
+    BZ2_bzDecompressEnd(&dstream);
+    BZ2_bzDecompressEnd(&estream);
+    return 0;
+}
diff --git a/recovery/applypatch/freecache.cpp b/recovery/applypatch/freecache.cpp
new file mode 100644
index 0000000..331cae2
--- /dev/null
+++ b/recovery/applypatch/freecache.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#include <errno.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <ctype.h>
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+
+#include "applypatch/applypatch.h"
+
+static int EliminateOpenFiles(std::set<std::string>* files) {
+  std::unique_ptr<DIR, decltype(&closedir)> d(opendir("/proc"), closedir);
+  if (!d) {
+    printf("error opening /proc: %s\n", strerror(errno));
+    return -1;
+  }
+  struct dirent* de;
+  while ((de = readdir(d.get())) != 0) {
+    unsigned int pid;
+    if (!android::base::ParseUint(de->d_name, &pid)) {
+        continue;
+    }
+    std::string path = android::base::StringPrintf("/proc/%s/fd/", de->d_name);
+
+    struct dirent* fdde;
+    std::unique_ptr<DIR, decltype(&closedir)> fdd(opendir(path.c_str()), closedir);
+    if (!fdd) {
+      printf("error opening %s: %s\n", path.c_str(), strerror(errno));
+      continue;
+    }
+    while ((fdde = readdir(fdd.get())) != 0) {
+      std::string fd_path = path + fdde->d_name;
+      char link[FILENAME_MAX];
+
+      int count = readlink(fd_path.c_str(), link, sizeof(link)-1);
+      if (count >= 0) {
+        link[count] = '\0';
+        if (strncmp(link, "/cache/", 7) == 0) {
+          if (files->erase(link) > 0) {
+            printf("%s is open by %s\n", link, de->d_name);
+          }
+        }
+      }
+    }
+  }
+  return 0;
+}
+
+static std::set<std::string> FindExpendableFiles() {
+  std::set<std::string> files;
+  // We're allowed to delete unopened regular files in any of these
+  // directories.
+  const char* dirs[2] = {"/cache", "/cache/recovery/otatest"};
+
+  for (size_t i = 0; i < sizeof(dirs)/sizeof(dirs[0]); ++i) {
+    std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirs[i]), closedir);
+    if (!d) {
+      printf("error opening %s: %s\n", dirs[i], strerror(errno));
+      continue;
+    }
+
+    // Look for regular files in the directory (not in any subdirectories).
+    struct dirent* de;
+    while ((de = readdir(d.get())) != 0) {
+      std::string path = std::string(dirs[i]) + "/" + de->d_name;
+
+      // We can't delete CACHE_TEMP_SOURCE; if it's there we might have
+      // restarted during installation and could be depending on it to
+      // be there.
+      if (path == CACHE_TEMP_SOURCE) {
+        continue;
+      }
+
+      struct stat st;
+      if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+        files.insert(path);
+      }
+    }
+  }
+
+  printf("%zu regular files in deletable directories\n", files.size());
+  if (EliminateOpenFiles(&files) < 0) {
+    return std::set<std::string>();
+  }
+  return files;
+}
+
+int MakeFreeSpaceOnCache(size_t bytes_needed) {
+  size_t free_now = FreeSpaceForFile("/cache");
+  printf("%zu bytes free on /cache (%zu needed)\n", free_now, bytes_needed);
+
+  if (free_now >= bytes_needed) {
+    return 0;
+  }
+  std::set<std::string> files = FindExpendableFiles();
+  if (files.empty()) {
+    // nothing we can delete to free up space!
+    printf("no files can be deleted to free space on /cache\n");
+    return -1;
+  }
+
+  // We could try to be smarter about which files to delete:  the
+  // biggest ones?  the smallest ones that will free up enough space?
+  // the oldest?  the newest?
+  //
+  // Instead, we'll be dumb.
+
+  for (const auto& file : files) {
+    unlink(file.c_str());
+    free_now = FreeSpaceForFile("/cache");
+    printf("deleted %s; now %zu bytes free\n", file.c_str(), free_now);
+    if (free_now < bytes_needed) {
+        break;
+    }
+  }
+  return (free_now >= bytes_needed) ? 0 : -1;
+}
diff --git a/recovery/applypatch/imgdiff.cpp b/recovery/applypatch/imgdiff.cpp
new file mode 100644
index 0000000..7c5bb86
--- /dev/null
+++ b/recovery/applypatch/imgdiff.cpp
@@ -0,0 +1,1068 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+/*
+ * This program constructs binary patches for images -- such as boot.img
+ * and recovery.img -- that consist primarily of large chunks of gzipped
+ * data interspersed with uncompressed data.  Doing a naive bsdiff of
+ * these files is not useful because small changes in the data lead to
+ * large changes in the compressed bitstream; bsdiff patches of gzipped
+ * data are typically as large as the data itself.
+ *
+ * To patch these usefully, we break the source and target images up into
+ * chunks of two types: "normal" and "gzip".  Normal chunks are simply
+ * patched using a plain bsdiff.  Gzip chunks are first expanded, then a
+ * bsdiff is applied to the uncompressed data, then the patched data is
+ * gzipped using the same encoder parameters.  Patched chunks are
+ * concatenated together to create the output file; the output image
+ * should be *exactly* the same series of bytes as the target image used
+ * originally to generate the patch.
+ *
+ * To work well with this tool, the gzipped sections of the target
+ * image must have been generated using the same deflate encoder that
+ * is available in applypatch, namely, the one in the zlib library.
+ * In practice this means that images should be compressed using the
+ * "minigzip" tool included in the zlib distribution, not the GNU gzip
+ * program.
+ *
+ * An "imgdiff" patch consists of a header describing the chunk structure
+ * of the file and any encoding parameters needed for the gzipped
+ * chunks, followed by N bsdiff patches, one per chunk.
+ *
+ * For a diff to be generated, the source and target images must have the
+ * same "chunk" structure: that is, the same number of gzipped and normal
+ * chunks in the same order.  Android boot and recovery images currently
+ * consist of five chunks:  a small normal header, a gzipped kernel, a
+ * small normal section, a gzipped ramdisk, and finally a small normal
+ * footer.
+ *
+ * Caveats:  we locate gzipped sections within the source and target
+ * images by searching for the byte sequence 1f8b0800:  1f8b is the gzip
+ * magic number; 08 specifies the "deflate" encoding [the only encoding
+ * supported by the gzip standard]; and 00 is the flags byte.  We do not
+ * currently support any extra header fields (which would be indicated by
+ * a nonzero flags byte).  We also don't handle the case when that byte
+ * sequence appears spuriously in the file.  (Note that it would have to
+ * occur spuriously within a normal chunk to be a problem.)
+ *
+ *
+ * The imgdiff patch header looks like this:
+ *
+ *    "IMGDIFF1"                  (8)   [magic number and version]
+ *    chunk count                 (4)
+ *    for each chunk:
+ *        chunk type              (4)   [CHUNK_{NORMAL, GZIP, DEFLATE, RAW}]
+ *        if chunk type == CHUNK_NORMAL:
+ *           source start         (8)
+ *           source len           (8)
+ *           bsdiff patch offset  (8)   [from start of patch file]
+ *        if chunk type == CHUNK_GZIP:      (version 1 only)
+ *           source start         (8)
+ *           source len           (8)
+ *           bsdiff patch offset  (8)   [from start of patch file]
+ *           source expanded len  (8)   [size of uncompressed source]
+ *           target expected len  (8)   [size of uncompressed target]
+ *           gzip level           (4)
+ *                method          (4)
+ *                windowBits      (4)
+ *                memLevel        (4)
+ *                strategy        (4)
+ *           gzip header len      (4)
+ *           gzip header          (gzip header len)
+ *           gzip footer          (8)
+ *        if chunk type == CHUNK_DEFLATE:   (version 2 only)
+ *           source start         (8)
+ *           source len           (8)
+ *           bsdiff patch offset  (8)   [from start of patch file]
+ *           source expanded len  (8)   [size of uncompressed source]
+ *           target expected len  (8)   [size of uncompressed target]
+ *           gzip level           (4)
+ *                method          (4)
+ *                windowBits      (4)
+ *                memLevel        (4)
+ *                strategy        (4)
+ *        if chunk type == RAW:             (version 2 only)
+ *           target len           (4)
+ *           data                 (target len)
+ *
+ * All integers are little-endian.  "source start" and "source len"
+ * specify the section of the input image that comprises this chunk,
+ * including the gzip header and footer for gzip chunks.  "source
+ * expanded len" is the size of the uncompressed source data.  "target
+ * expected len" is the size of the uncompressed data after applying
+ * the bsdiff patch.  The next five parameters specify the zlib
+ * parameters to be used when compressing the patched data, and the
+ * next three specify the header and footer to be wrapped around the
+ * compressed data to create the output chunk (so that header contents
+ * like the timestamp are recreated exactly).
+ *
+ * After the header there are 'chunk count' bsdiff patches; the offset
+ * of each from the beginning of the file is specified in the header.
+ *
+ * This tool can take an optional file of "bonus data".  This is an
+ * extra file of data that is appended to chunk #1 after it is
+ * compressed (it must be a CHUNK_DEFLATE chunk).  The same file must
+ * be available (and passed to applypatch with -b) when applying the
+ * patch.  This is used to reduce the size of recovery-from-boot
+ * patches by combining the boot image with recovery ramdisk
+ * information that is stored on the system partition.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <sys/types.h>
+
+#include <bsdiff.h>
+
+#include "zlib.h"
+#include "imgdiff.h"
+#include "utils.h"
+
+typedef struct {
+  int type;             // CHUNK_NORMAL, CHUNK_DEFLATE
+  size_t start;         // offset of chunk in original image file
+
+  size_t len;
+  unsigned char* data;  // data to be patched (uncompressed, for deflate chunks)
+
+  size_t source_start;
+  size_t source_len;
+
+  // --- for CHUNK_DEFLATE chunks only: ---
+
+  // original (compressed) deflate data
+  size_t deflate_len;
+  unsigned char* deflate_data;
+
+  char* filename;       // used for zip entries
+
+  // deflate encoder parameters
+  int level, method, windowBits, memLevel, strategy;
+
+  size_t source_uncompressed_len;
+} ImageChunk;
+
+typedef struct {
+  int data_offset;
+  int deflate_len;
+  int uncomp_len;
+  char* filename;
+} ZipFileEntry;
+
+static int fileentry_compare(const void* a, const void* b) {
+  int ao = ((ZipFileEntry*)a)->data_offset;
+  int bo = ((ZipFileEntry*)b)->data_offset;
+  if (ao < bo) {
+    return -1;
+  } else if (ao > bo) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+unsigned char* ReadZip(const char* filename,
+                       int* num_chunks, ImageChunk** chunks,
+                       int include_pseudo_chunk) {
+  struct stat st;
+  if (stat(filename, &st) != 0) {
+    printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
+    return NULL;
+  }
+
+  size_t sz = static_cast<size_t>(st.st_size);
+  unsigned char* img = reinterpret_cast<unsigned char*>(malloc(sz));
+  FILE* f = fopen(filename, "rb");
+  if (fread(img, 1, sz, f) != sz) {
+    printf("failed to read \"%s\" %s\n", filename, strerror(errno));
+    fclose(f);
+    return NULL;
+  }
+  fclose(f);
+
+  // look for the end-of-central-directory record.
+
+  int i;
+  for (i = st.st_size-20; i >= 0 && i > st.st_size - 65600; --i) {
+    if (img[i] == 0x50 && img[i+1] == 0x4b &&
+        img[i+2] == 0x05 && img[i+3] == 0x06) {
+      break;
+    }
+  }
+  // double-check: this archive consists of a single "disk"
+  if (!(img[i+4] == 0 && img[i+5] == 0 && img[i+6] == 0 && img[i+7] == 0)) {
+    printf("can't process multi-disk archive\n");
+    return NULL;
+  }
+
+  int cdcount = Read2(img+i+8);
+  int cdoffset = Read4(img+i+16);
+
+  ZipFileEntry* temp_entries = reinterpret_cast<ZipFileEntry*>(malloc(
+      cdcount * sizeof(ZipFileEntry)));
+  int entrycount = 0;
+
+  unsigned char* cd = img+cdoffset;
+  for (i = 0; i < cdcount; ++i) {
+    if (!(cd[0] == 0x50 && cd[1] == 0x4b && cd[2] == 0x01 && cd[3] == 0x02)) {
+      printf("bad central directory entry %d\n", i);
+      return NULL;
+    }
+
+    int clen = Read4(cd+20);   // compressed len
+    int ulen = Read4(cd+24);   // uncompressed len
+    int nlen = Read2(cd+28);   // filename len
+    int xlen = Read2(cd+30);   // extra field len
+    int mlen = Read2(cd+32);   // file comment len
+    int hoffset = Read4(cd+42);   // local header offset
+
+    char* filename = reinterpret_cast<char*>(malloc(nlen+1));
+    memcpy(filename, cd+46, nlen);
+    filename[nlen] = '\0';
+
+    int method = Read2(cd+10);
+
+    cd += 46 + nlen + xlen + mlen;
+
+    if (method != 8) {  // 8 == deflate
+      free(filename);
+      continue;
+    }
+
+    unsigned char* lh = img + hoffset;
+
+    if (!(lh[0] == 0x50 && lh[1] == 0x4b && lh[2] == 0x03 && lh[3] == 0x04)) {
+      printf("bad local file header entry %d\n", i);
+      return NULL;
+    }
+
+    if (Read2(lh+26) != nlen || memcmp(lh+30, filename, nlen) != 0) {
+      printf("central dir filename doesn't match local header\n");
+      return NULL;
+    }
+
+    xlen = Read2(lh+28);   // extra field len; might be different from CD entry?
+
+    temp_entries[entrycount].data_offset = hoffset+30+nlen+xlen;
+    temp_entries[entrycount].deflate_len = clen;
+    temp_entries[entrycount].uncomp_len = ulen;
+    temp_entries[entrycount].filename = filename;
+    ++entrycount;
+  }
+
+  qsort(temp_entries, entrycount, sizeof(ZipFileEntry), fileentry_compare);
+
+#if 0
+  printf("found %d deflated entries\n", entrycount);
+  for (i = 0; i < entrycount; ++i) {
+    printf("off %10d  len %10d unlen %10d   %p %s\n",
+           temp_entries[i].data_offset,
+           temp_entries[i].deflate_len,
+           temp_entries[i].uncomp_len,
+           temp_entries[i].filename,
+           temp_entries[i].filename);
+  }
+#endif
+
+  *num_chunks = 0;
+  *chunks = reinterpret_cast<ImageChunk*>(malloc((entrycount*2+2) * sizeof(ImageChunk)));
+  ImageChunk* curr = *chunks;
+
+  if (include_pseudo_chunk) {
+    curr->type = CHUNK_NORMAL;
+    curr->start = 0;
+    curr->len = st.st_size;
+    curr->data = img;
+    curr->filename = NULL;
+    ++curr;
+    ++*num_chunks;
+  }
+
+  int pos = 0;
+  int nextentry = 0;
+
+  while (pos < st.st_size) {
+    if (nextentry < entrycount && pos == temp_entries[nextentry].data_offset) {
+      curr->type = CHUNK_DEFLATE;
+      curr->start = pos;
+      curr->deflate_len = temp_entries[nextentry].deflate_len;
+      curr->deflate_data = img + pos;
+      curr->filename = temp_entries[nextentry].filename;
+
+      curr->len = temp_entries[nextentry].uncomp_len;
+      curr->data = reinterpret_cast<unsigned char*>(malloc(curr->len));
+
+      z_stream strm;
+      strm.zalloc = Z_NULL;
+      strm.zfree = Z_NULL;
+      strm.opaque = Z_NULL;
+      strm.avail_in = curr->deflate_len;
+      strm.next_in = curr->deflate_data;
+
+      // -15 means we are decoding a 'raw' deflate stream; zlib will
+      // not expect zlib headers.
+      int ret = inflateInit2(&strm, -15);
+
+      strm.avail_out = curr->len;
+      strm.next_out = curr->data;
+      ret = inflate(&strm, Z_NO_FLUSH);
+      if (ret != Z_STREAM_END) {
+        printf("failed to inflate \"%s\"; %d\n", curr->filename, ret);
+        return NULL;
+      }
+
+      inflateEnd(&strm);
+
+      pos += curr->deflate_len;
+      ++nextentry;
+      ++*num_chunks;
+      ++curr;
+      continue;
+    }
+
+    // use a normal chunk to take all the data up to the start of the
+    // next deflate section.
+
+    curr->type = CHUNK_NORMAL;
+    curr->start = pos;
+    if (nextentry < entrycount) {
+      curr->len = temp_entries[nextentry].data_offset - pos;
+    } else {
+      curr->len = st.st_size - pos;
+    }
+    curr->data = img + pos;
+    curr->filename = NULL;
+    pos += curr->len;
+
+    ++*num_chunks;
+    ++curr;
+  }
+
+  free(temp_entries);
+  return img;
+}
+
+/*
+ * Read the given file and break it up into chunks, putting the number
+ * of chunks and their info in *num_chunks and **chunks,
+ * respectively.  Returns a malloc'd block of memory containing the
+ * contents of the file; various pointers in the output chunk array
+ * will point into this block of memory.  The caller should free the
+ * return value when done with all the chunks.  Returns NULL on
+ * failure.
+ */
+unsigned char* ReadImage(const char* filename,
+                         int* num_chunks, ImageChunk** chunks) {
+  struct stat st;
+  if (stat(filename, &st) != 0) {
+    printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
+    return NULL;
+  }
+
+  size_t sz = static_cast<size_t>(st.st_size);
+  unsigned char* img = reinterpret_cast<unsigned char*>(malloc(sz + 4));
+  FILE* f = fopen(filename, "rb");
+  if (fread(img, 1, sz, f) != sz) {
+    printf("failed to read \"%s\" %s\n", filename, strerror(errno));
+    fclose(f);
+    return NULL;
+  }
+  fclose(f);
+
+  // append 4 zero bytes to the data so we can always search for the
+  // four-byte string 1f8b0800 starting at any point in the actual
+  // file data, without special-casing the end of the data.
+  memset(img+sz, 0, 4);
+
+  size_t pos = 0;
+
+  *num_chunks = 0;
+  *chunks = NULL;
+
+  while (pos < sz) {
+    unsigned char* p = img+pos;
+
+    if (sz - pos >= 4 &&
+        p[0] == 0x1f && p[1] == 0x8b &&
+        p[2] == 0x08 &&    // deflate compression
+        p[3] == 0x00) {    // no header flags
+      // 'pos' is the offset of the start of a gzip chunk.
+      size_t chunk_offset = pos;
+
+      *num_chunks += 3;
+      *chunks = reinterpret_cast<ImageChunk*>(realloc(*chunks,
+          *num_chunks * sizeof(ImageChunk)));
+      ImageChunk* curr = *chunks + (*num_chunks-3);
+
+      // create a normal chunk for the header.
+      curr->start = pos;
+      curr->type = CHUNK_NORMAL;
+      curr->len = GZIP_HEADER_LEN;
+      curr->data = p;
+
+      pos += curr->len;
+      p += curr->len;
+      ++curr;
+
+      curr->type = CHUNK_DEFLATE;
+      curr->filename = NULL;
+
+      // We must decompress this chunk in order to discover where it
+      // ends, and so we can put the uncompressed data and its length
+      // into curr->data and curr->len.
+
+      size_t allocated = 32768;
+      curr->len = 0;
+      curr->data = reinterpret_cast<unsigned char*>(malloc(allocated));
+      curr->start = pos;
+      curr->deflate_data = p;
+
+      z_stream strm;
+      strm.zalloc = Z_NULL;
+      strm.zfree = Z_NULL;
+      strm.opaque = Z_NULL;
+      strm.avail_in = sz - pos;
+      strm.next_in = p;
+
+      // -15 means we are decoding a 'raw' deflate stream; zlib will
+      // not expect zlib headers.
+      int ret = inflateInit2(&strm, -15);
+
+      do {
+        strm.avail_out = allocated - curr->len;
+        strm.next_out = curr->data + curr->len;
+        ret = inflate(&strm, Z_NO_FLUSH);
+        if (ret < 0) {
+          printf("Warning: inflate failed [%s] at offset [%zu],"
+                 " treating as a normal chunk\n",
+                 strm.msg, chunk_offset);
+          break;
+        }
+        curr->len = allocated - strm.avail_out;
+        if (strm.avail_out == 0) {
+          allocated *= 2;
+          curr->data = reinterpret_cast<unsigned char*>(realloc(curr->data, allocated));
+        }
+      } while (ret != Z_STREAM_END);
+
+      curr->deflate_len = sz - strm.avail_in - pos;
+      inflateEnd(&strm);
+
+      if (ret < 0) {
+        free(curr->data);
+        *num_chunks -= 2;
+        continue;
+      }
+
+      pos += curr->deflate_len;
+      p += curr->deflate_len;
+      ++curr;
+
+      // create a normal chunk for the footer
+
+      curr->type = CHUNK_NORMAL;
+      curr->start = pos;
+      curr->len = GZIP_FOOTER_LEN;
+      curr->data = img+pos;
+
+      pos += curr->len;
+      p += curr->len;
+      ++curr;
+
+      // The footer (that we just skipped over) contains the size of
+      // the uncompressed data.  Double-check to make sure that it
+      // matches the size of the data we got when we actually did
+      // the decompression.
+      size_t footer_size = Read4(p-4);
+      if (footer_size != curr[-2].len) {
+        printf("Error: footer size %zu != decompressed size %zu\n",
+            footer_size, curr[-2].len);
+        free(img);
+        return NULL;
+      }
+    } else {
+      // Reallocate the list for every chunk; we expect the number of
+      // chunks to be small (5 for typical boot and recovery images).
+      ++*num_chunks;
+      *chunks = reinterpret_cast<ImageChunk*>(realloc(*chunks, *num_chunks * sizeof(ImageChunk)));
+      ImageChunk* curr = *chunks + (*num_chunks-1);
+      curr->start = pos;
+
+      // 'pos' is not the offset of the start of a gzip chunk, so scan
+      // forward until we find a gzip header.
+      curr->type = CHUNK_NORMAL;
+      curr->data = p;
+
+      for (curr->len = 0; curr->len < (sz - pos); ++curr->len) {
+        if (p[curr->len] == 0x1f &&
+            p[curr->len+1] == 0x8b &&
+            p[curr->len+2] == 0x08 &&
+            p[curr->len+3] == 0x00) {
+          break;
+        }
+      }
+      pos += curr->len;
+    }
+  }
+
+  return img;
+}
+
+#define BUFFER_SIZE 32768
+
+/*
+ * Takes the uncompressed data stored in the chunk, compresses it
+ * using the zlib parameters stored in the chunk, and checks that it
+ * matches exactly the compressed data we started with (also stored in
+ * the chunk).  Return 0 on success.
+ */
+int TryReconstruction(ImageChunk* chunk, unsigned char* out) {
+  size_t p = 0;
+
+#if 0
+  printf("trying %d %d %d %d %d\n",
+          chunk->level, chunk->method, chunk->windowBits,
+          chunk->memLevel, chunk->strategy);
+#endif
+
+  z_stream strm;
+  strm.zalloc = Z_NULL;
+  strm.zfree = Z_NULL;
+  strm.opaque = Z_NULL;
+  strm.avail_in = chunk->len;
+  strm.next_in = chunk->data;
+  int ret;
+  ret = deflateInit2(&strm, chunk->level, chunk->method, chunk->windowBits,
+                     chunk->memLevel, chunk->strategy);
+  do {
+    strm.avail_out = BUFFER_SIZE;
+    strm.next_out = out;
+    ret = deflate(&strm, Z_FINISH);
+    size_t have = BUFFER_SIZE - strm.avail_out;
+
+    if (memcmp(out, chunk->deflate_data+p, have) != 0) {
+      // mismatch; data isn't the same.
+      deflateEnd(&strm);
+      return -1;
+    }
+    p += have;
+  } while (ret != Z_STREAM_END);
+  deflateEnd(&strm);
+  if (p != chunk->deflate_len) {
+    // mismatch; ran out of data before we should have.
+    return -1;
+  }
+  return 0;
+}
+
+/*
+ * Verify that we can reproduce exactly the same compressed data that
+ * we started with.  Sets the level, method, windowBits, memLevel, and
+ * strategy fields in the chunk to the encoding parameters needed to
+ * produce the right output.  Returns 0 on success.
+ */
+int ReconstructDeflateChunk(ImageChunk* chunk) {
+  if (chunk->type != CHUNK_DEFLATE) {
+    printf("attempt to reconstruct non-deflate chunk\n");
+    return -1;
+  }
+
+  unsigned char* out = reinterpret_cast<unsigned char*>(malloc(BUFFER_SIZE));
+
+  // We only check two combinations of encoder parameters:  level 6
+  // (the default) and level 9 (the maximum).
+  for (chunk->level = 6; chunk->level <= 9; chunk->level += 3) {
+    chunk->windowBits = -15;  // 32kb window; negative to indicate a raw stream.
+    chunk->memLevel = 8;      // the default value.
+    chunk->method = Z_DEFLATED;
+    chunk->strategy = Z_DEFAULT_STRATEGY;
+
+    if (TryReconstruction(chunk, out) == 0) {
+      free(out);
+      return 0;
+    }
+  }
+
+  free(out);
+  return -1;
+}
+
+/*
+ * Given source and target chunks, compute a bsdiff patch between them
+ * by running bsdiff in a subprocess.  Return the patch data, placing
+ * its length in *size.  Return NULL on failure.  We expect the bsdiff
+ * program to be in the path.
+ */
+unsigned char* MakePatch(ImageChunk* src, ImageChunk* tgt, size_t* size) {
+  if (tgt->type == CHUNK_NORMAL) {
+    if (tgt->len <= 160) {
+      tgt->type = CHUNK_RAW;
+      *size = tgt->len;
+      return tgt->data;
+    }
+  }
+
+  char ptemp[] = "/tmp/imgdiff-patch-XXXXXX";
+  int fd = mkstemp(ptemp);
+
+  if (fd == -1) {
+    printf("MakePatch failed to create a temporary file: %s\n",
+           strerror(errno));
+    return NULL;
+  }
+  close(fd); // temporary file is created and we don't need its file
+             // descriptor
+
+  int r = bsdiff::bsdiff(src->data, src->len, tgt->data, tgt->len, ptemp);
+  if (r != 0) {
+    printf("bsdiff() failed: %d\n", r);
+    return NULL;
+  }
+
+  struct stat st;
+  if (stat(ptemp, &st) != 0) {
+    printf("failed to stat patch file %s: %s\n",
+            ptemp, strerror(errno));
+    return NULL;
+  }
+
+  size_t sz = static_cast<size_t>(st.st_size);
+  // TODO: Memory leak on error return.
+  unsigned char* data = reinterpret_cast<unsigned char*>(malloc(sz));
+
+  if (tgt->type == CHUNK_NORMAL && tgt->len <= sz) {
+    unlink(ptemp);
+
+    tgt->type = CHUNK_RAW;
+    *size = tgt->len;
+    return tgt->data;
+  }
+
+  *size = sz;
+
+  FILE* f = fopen(ptemp, "rb");
+  if (f == NULL) {
+    printf("failed to open patch %s: %s\n", ptemp, strerror(errno));
+    return NULL;
+  }
+  if (fread(data, 1, sz, f) != sz) {
+    printf("failed to read patch %s: %s\n", ptemp, strerror(errno));
+    return NULL;
+  }
+  fclose(f);
+
+  unlink(ptemp);
+
+  tgt->source_start = src->start;
+  switch (tgt->type) {
+    case CHUNK_NORMAL:
+      tgt->source_len = src->len;
+      break;
+    case CHUNK_DEFLATE:
+      tgt->source_len = src->deflate_len;
+      tgt->source_uncompressed_len = src->len;
+      break;
+  }
+
+  return data;
+}
+
+/*
+ * Cause a gzip chunk to be treated as a normal chunk (ie, as a blob
+ * of uninterpreted data).  The resulting patch will likely be about
+ * as big as the target file, but it lets us handle the case of images
+ * where some gzip chunks are reconstructible but others aren't (by
+ * treating the ones that aren't as normal chunks).
+ */
+void ChangeDeflateChunkToNormal(ImageChunk* ch) {
+  if (ch->type != CHUNK_DEFLATE) return;
+  ch->type = CHUNK_NORMAL;
+  free(ch->data);
+  ch->data = ch->deflate_data;
+  ch->len = ch->deflate_len;
+}
+
+/*
+ * Return true if the data in the chunk is identical (including the
+ * compressed representation, for gzip chunks).
+ */
+int AreChunksEqual(ImageChunk* a, ImageChunk* b) {
+    if (a->type != b->type) return 0;
+
+    switch (a->type) {
+        case CHUNK_NORMAL:
+            return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
+
+        case CHUNK_DEFLATE:
+            return a->deflate_len == b->deflate_len &&
+                memcmp(a->deflate_data, b->deflate_data, a->deflate_len) == 0;
+
+        default:
+            printf("unknown chunk type %d\n", a->type);
+            return 0;
+    }
+}
+
+/*
+ * Look for runs of adjacent normal chunks and compress them down into
+ * a single chunk.  (Such runs can be produced when deflate chunks are
+ * changed to normal chunks.)
+ */
+void MergeAdjacentNormalChunks(ImageChunk* chunks, int* num_chunks) {
+  int out = 0;
+  int in_start = 0, in_end;
+  while (in_start < *num_chunks) {
+    if (chunks[in_start].type != CHUNK_NORMAL) {
+      in_end = in_start+1;
+    } else {
+      // in_start is a normal chunk.  Look for a run of normal chunks
+      // that constitute a solid block of data (ie, each chunk begins
+      // where the previous one ended).
+      for (in_end = in_start+1;
+           in_end < *num_chunks && chunks[in_end].type == CHUNK_NORMAL &&
+             (chunks[in_end].start ==
+              chunks[in_end-1].start + chunks[in_end-1].len &&
+              chunks[in_end].data ==
+              chunks[in_end-1].data + chunks[in_end-1].len);
+           ++in_end);
+    }
+
+    if (in_end == in_start+1) {
+#if 0
+      printf("chunk %d is now %d\n", in_start, out);
+#endif
+      if (out != in_start) {
+        memcpy(chunks+out, chunks+in_start, sizeof(ImageChunk));
+      }
+    } else {
+#if 0
+      printf("collapse normal chunks %d-%d into %d\n", in_start, in_end-1, out);
+#endif
+
+      // Merge chunks [in_start, in_end-1] into one chunk.  Since the
+      // data member of each chunk is just a pointer into an in-memory
+      // copy of the file, this can be done without recopying (the
+      // output chunk has the first chunk's start location and data
+      // pointer, and length equal to the sum of the input chunk
+      // lengths).
+      chunks[out].type = CHUNK_NORMAL;
+      chunks[out].start = chunks[in_start].start;
+      chunks[out].data = chunks[in_start].data;
+      chunks[out].len = chunks[in_end-1].len +
+        (chunks[in_end-1].start - chunks[in_start].start);
+    }
+
+    ++out;
+    in_start = in_end;
+  }
+  *num_chunks = out;
+}
+
+ImageChunk* FindChunkByName(const char* name,
+                            ImageChunk* chunks, int num_chunks) {
+  int i;
+  for (i = 0; i < num_chunks; ++i) {
+    if (chunks[i].type == CHUNK_DEFLATE && chunks[i].filename &&
+        strcmp(name, chunks[i].filename) == 0) {
+      return chunks+i;
+    }
+  }
+  return NULL;
+}
+
+void DumpChunks(ImageChunk* chunks, int num_chunks) {
+    for (int i = 0; i < num_chunks; ++i) {
+        printf("chunk %d: type %d start %zu len %zu\n",
+               i, chunks[i].type, chunks[i].start, chunks[i].len);
+    }
+}
+
+int main(int argc, char** argv) {
+  int zip_mode = 0;
+
+  if (argc >= 2 && strcmp(argv[1], "-z") == 0) {
+    zip_mode = 1;
+    --argc;
+    ++argv;
+  }
+
+  size_t bonus_size = 0;
+  unsigned char* bonus_data = NULL;
+  if (argc >= 3 && strcmp(argv[1], "-b") == 0) {
+    struct stat st;
+    if (stat(argv[2], &st) != 0) {
+      printf("failed to stat bonus file %s: %s\n", argv[2], strerror(errno));
+      return 1;
+    }
+    bonus_size = st.st_size;
+    bonus_data = reinterpret_cast<unsigned char*>(malloc(bonus_size));
+    FILE* f = fopen(argv[2], "rb");
+    if (f == NULL) {
+      printf("failed to open bonus file %s: %s\n", argv[2], strerror(errno));
+      return 1;
+    }
+    if (fread(bonus_data, 1, bonus_size, f) != bonus_size) {
+      printf("failed to read bonus file %s: %s\n", argv[2], strerror(errno));
+      return 1;
+    }
+    fclose(f);
+
+    argc -= 2;
+    argv += 2;
+  }
+
+  if (argc != 4) {
+    printf("usage: %s [-z] [-b <bonus-file>] <src-img> <tgt-img> <patch-file>\n",
+            argv[0]);
+    return 2;
+  }
+
+  int num_src_chunks;
+  ImageChunk* src_chunks;
+  int num_tgt_chunks;
+  ImageChunk* tgt_chunks;
+  int i;
+
+  if (zip_mode) {
+    if (ReadZip(argv[1], &num_src_chunks, &src_chunks, 1) == NULL) {
+      printf("failed to break apart source zip file\n");
+      return 1;
+    }
+    if (ReadZip(argv[2], &num_tgt_chunks, &tgt_chunks, 0) == NULL) {
+      printf("failed to break apart target zip file\n");
+      return 1;
+    }
+  } else {
+    if (ReadImage(argv[1], &num_src_chunks, &src_chunks) == NULL) {
+      printf("failed to break apart source image\n");
+      return 1;
+    }
+    if (ReadImage(argv[2], &num_tgt_chunks, &tgt_chunks) == NULL) {
+      printf("failed to break apart target image\n");
+      return 1;
+    }
+
+    // Verify that the source and target images have the same chunk
+    // structure (ie, the same sequence of deflate and normal chunks).
+
+    if (!zip_mode) {
+        // Merge the gzip header and footer in with any adjacent
+        // normal chunks.
+        MergeAdjacentNormalChunks(tgt_chunks, &num_tgt_chunks);
+        MergeAdjacentNormalChunks(src_chunks, &num_src_chunks);
+    }
+
+    if (num_src_chunks != num_tgt_chunks) {
+      printf("source and target don't have same number of chunks!\n");
+      printf("source chunks:\n");
+      DumpChunks(src_chunks, num_src_chunks);
+      printf("target chunks:\n");
+      DumpChunks(tgt_chunks, num_tgt_chunks);
+      return 1;
+    }
+    for (i = 0; i < num_src_chunks; ++i) {
+      if (src_chunks[i].type != tgt_chunks[i].type) {
+        printf("source and target don't have same chunk "
+                "structure! (chunk %d)\n", i);
+        printf("source chunks:\n");
+        DumpChunks(src_chunks, num_src_chunks);
+        printf("target chunks:\n");
+        DumpChunks(tgt_chunks, num_tgt_chunks);
+        return 1;
+      }
+    }
+  }
+
+  for (i = 0; i < num_tgt_chunks; ++i) {
+    if (tgt_chunks[i].type == CHUNK_DEFLATE) {
+      // Confirm that given the uncompressed chunk data in the target, we
+      // can recompress it and get exactly the same bits as are in the
+      // input target image.  If this fails, treat the chunk as a normal
+      // non-deflated chunk.
+      if (ReconstructDeflateChunk(tgt_chunks+i) < 0) {
+        printf("failed to reconstruct target deflate chunk %d [%s]; "
+               "treating as normal\n", i, tgt_chunks[i].filename);
+        ChangeDeflateChunkToNormal(tgt_chunks+i);
+        if (zip_mode) {
+          ImageChunk* src = FindChunkByName(tgt_chunks[i].filename, src_chunks, num_src_chunks);
+          if (src) {
+            ChangeDeflateChunkToNormal(src);
+          }
+        } else {
+          ChangeDeflateChunkToNormal(src_chunks+i);
+        }
+        continue;
+      }
+
+      // If two deflate chunks are identical (eg, the kernel has not
+      // changed between two builds), treat them as normal chunks.
+      // This makes applypatch much faster -- it can apply a trivial
+      // patch to the compressed data, rather than uncompressing and
+      // recompressing to apply the trivial patch to the uncompressed
+      // data.
+      ImageChunk* src;
+      if (zip_mode) {
+        src = FindChunkByName(tgt_chunks[i].filename, src_chunks, num_src_chunks);
+      } else {
+        src = src_chunks+i;
+      }
+
+      if (src == NULL || AreChunksEqual(tgt_chunks+i, src)) {
+        ChangeDeflateChunkToNormal(tgt_chunks+i);
+        if (src) {
+          ChangeDeflateChunkToNormal(src);
+        }
+      }
+    }
+  }
+
+  // Merging neighboring normal chunks.
+  if (zip_mode) {
+    // For zips, we only need to do this to the target:  deflated
+    // chunks are matched via filename, and normal chunks are patched
+    // using the entire source file as the source.
+    MergeAdjacentNormalChunks(tgt_chunks, &num_tgt_chunks);
+  } else {
+    // For images, we need to maintain the parallel structure of the
+    // chunk lists, so do the merging in both the source and target
+    // lists.
+    MergeAdjacentNormalChunks(tgt_chunks, &num_tgt_chunks);
+    MergeAdjacentNormalChunks(src_chunks, &num_src_chunks);
+    if (num_src_chunks != num_tgt_chunks) {
+      // This shouldn't happen.
+      printf("merging normal chunks went awry\n");
+      return 1;
+    }
+  }
+
+  // Compute bsdiff patches for each chunk's data (the uncompressed
+  // data, in the case of deflate chunks).
+
+  DumpChunks(src_chunks, num_src_chunks);
+
+  printf("Construct patches for %d chunks...\n", num_tgt_chunks);
+  unsigned char** patch_data = reinterpret_cast<unsigned char**>(malloc(
+      num_tgt_chunks * sizeof(unsigned char*)));
+  size_t* patch_size = reinterpret_cast<size_t*>(malloc(num_tgt_chunks * sizeof(size_t)));
+  for (i = 0; i < num_tgt_chunks; ++i) {
+    if (zip_mode) {
+      ImageChunk* src;
+      if (tgt_chunks[i].type == CHUNK_DEFLATE &&
+          (src = FindChunkByName(tgt_chunks[i].filename, src_chunks,
+                                 num_src_chunks))) {
+        patch_data[i] = MakePatch(src, tgt_chunks+i, patch_size+i);
+      } else {
+        patch_data[i] = MakePatch(src_chunks, tgt_chunks+i, patch_size+i);
+      }
+    } else {
+      if (i == 1 && bonus_data) {
+        printf("  using %zu bytes of bonus data for chunk %d\n", bonus_size, i);
+        src_chunks[i].data = reinterpret_cast<unsigned char*>(realloc(src_chunks[i].data,
+            src_chunks[i].len + bonus_size));
+        memcpy(src_chunks[i].data+src_chunks[i].len, bonus_data, bonus_size);
+        src_chunks[i].len += bonus_size;
+     }
+
+      patch_data[i] = MakePatch(src_chunks+i, tgt_chunks+i, patch_size+i);
+    }
+    printf("patch %3d is %zu bytes (of %zu)\n",
+           i, patch_size[i], tgt_chunks[i].source_len);
+  }
+
+  // Figure out how big the imgdiff file header is going to be, so
+  // that we can correctly compute the offset of each bsdiff patch
+  // within the file.
+
+  size_t total_header_size = 12;
+  for (i = 0; i < num_tgt_chunks; ++i) {
+    total_header_size += 4;
+    switch (tgt_chunks[i].type) {
+      case CHUNK_NORMAL:
+        total_header_size += 8*3;
+        break;
+      case CHUNK_DEFLATE:
+        total_header_size += 8*5 + 4*5;
+        break;
+      case CHUNK_RAW:
+        total_header_size += 4 + patch_size[i];
+        break;
+    }
+  }
+
+  size_t offset = total_header_size;
+
+  FILE* f = fopen(argv[3], "wb");
+
+  // Write out the headers.
+
+  fwrite("IMGDIFF2", 1, 8, f);
+  Write4(num_tgt_chunks, f);
+  for (i = 0; i < num_tgt_chunks; ++i) {
+    Write4(tgt_chunks[i].type, f);
+
+    switch (tgt_chunks[i].type) {
+      case CHUNK_NORMAL:
+        printf("chunk %3d: normal   (%10zu, %10zu)  %10zu\n", i,
+               tgt_chunks[i].start, tgt_chunks[i].len, patch_size[i]);
+        Write8(tgt_chunks[i].source_start, f);
+        Write8(tgt_chunks[i].source_len, f);
+        Write8(offset, f);
+        offset += patch_size[i];
+        break;
+
+      case CHUNK_DEFLATE:
+        printf("chunk %3d: deflate  (%10zu, %10zu)  %10zu  %s\n", i,
+               tgt_chunks[i].start, tgt_chunks[i].deflate_len, patch_size[i],
+               tgt_chunks[i].filename);
+        Write8(tgt_chunks[i].source_start, f);
+        Write8(tgt_chunks[i].source_len, f);
+        Write8(offset, f);
+        Write8(tgt_chunks[i].source_uncompressed_len, f);
+        Write8(tgt_chunks[i].len, f);
+        Write4(tgt_chunks[i].level, f);
+        Write4(tgt_chunks[i].method, f);
+        Write4(tgt_chunks[i].windowBits, f);
+        Write4(tgt_chunks[i].memLevel, f);
+        Write4(tgt_chunks[i].strategy, f);
+        offset += patch_size[i];
+        break;
+
+      case CHUNK_RAW:
+        printf("chunk %3d: raw      (%10zu, %10zu)\n", i,
+               tgt_chunks[i].start, tgt_chunks[i].len);
+        Write4(patch_size[i], f);
+        fwrite(patch_data[i], 1, patch_size[i], f);
+        break;
+    }
+  }
+
+  // Append each chunk's bsdiff patch, in order.
+
+  for (i = 0; i < num_tgt_chunks; ++i) {
+    if (tgt_chunks[i].type != CHUNK_RAW) {
+      fwrite(patch_data[i], 1, patch_size[i], f);
+    }
+  }
+
+  fclose(f);
+
+  return 0;
+}
diff --git a/recovery/applypatch/imgdiff.h b/recovery/applypatch/imgdiff.h
new file mode 100644
index 0000000..f2069b4
--- /dev/null
+++ b/recovery/applypatch/imgdiff.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+// Image patch chunk types
+#define CHUNK_NORMAL   0
+#define CHUNK_GZIP     1   // version 1 only
+#define CHUNK_DEFLATE  2   // version 2 only
+#define CHUNK_RAW      3   // version 2 only
+
+// The gzip header size is actually variable, but we currently don't
+// support gzipped data with any of the optional fields, so for now it
+// will always be ten bytes.  See RFC 1952 for the definition of the
+// gzip format.
+#define GZIP_HEADER_LEN   10
+
+// The gzip footer size really is fixed.
+#define GZIP_FOOTER_LEN   8
diff --git a/recovery/applypatch/imgdiff_test.sh b/recovery/applypatch/imgdiff_test.sh
new file mode 100755
index 0000000..dcdb922
--- /dev/null
+++ b/recovery/applypatch/imgdiff_test.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+#
+# A script for testing imgdiff/applypatch.  It takes two full OTA
+# packages as arguments.  It generates (on the host) patches for all
+# the zip/jar/apk files they have in common, as well as boot and
+# recovery images.  It then applies the patches on the device (or
+# emulator) and checks that the resulting file is correct.
+
+EMULATOR_PORT=5580
+
+# set to 0 to use a device instead
+USE_EMULATOR=0
+
+# where on the device to do all the patching.
+WORK_DIR=/data/local/tmp
+
+START_OTA_PACKAGE=$1
+END_OTA_PACKAGE=$2
+
+# ------------------------
+
+tmpdir=$(mktemp -d)
+
+if [ "$USE_EMULATOR" == 1 ]; then
+  emulator -wipe-data -noaudio -no-window -port $EMULATOR_PORT &
+  pid_emulator=$!
+  ADB="adb -s emulator-$EMULATOR_PORT "
+else
+  ADB="adb -d "
+fi
+
+echo "waiting to connect to device"
+$ADB wait-for-device
+
+# run a command on the device; exit with the exit status of the device
+# command.
+run_command() {
+  $ADB shell "$@" \; echo \$? | awk '{if (b) {print a}; a=$0; b=1} END {exit a}'
+}
+
+testname() {
+  echo
+  echo "$1"...
+  testname="$1"
+}
+
+fail() {
+  echo
+  echo FAIL: $testname
+  echo
+  [ "$open_pid" == "" ] || kill $open_pid
+  [ "$pid_emulator" == "" ] || kill $pid_emulator
+  exit 1
+}
+
+sha1() {
+  sha1sum $1 | awk '{print $1}'
+}
+
+size() {
+  stat -c %s $1 | tr -d '\n'
+}
+
+cleanup() {
+  # not necessary if we're about to kill the emulator, but nice for
+  # running on real devices or already-running emulators.
+  testname "removing test files"
+  run_command rm $WORK_DIR/applypatch
+  run_command rm $WORK_DIR/source
+  run_command rm $WORK_DIR/target
+  run_command rm $WORK_DIR/patch
+
+  [ "$pid_emulator" == "" ] || kill $pid_emulator
+
+  rm -rf $tmpdir
+}
+
+$ADB push $ANDROID_PRODUCT_OUT/system/bin/applypatch $WORK_DIR/applypatch
+
+patch_and_apply() {
+  local fn=$1
+  shift
+
+  unzip -p $START_OTA_PACKAGE $fn > $tmpdir/source
+  unzip -p $END_OTA_PACKAGE $fn > $tmpdir/target
+  imgdiff "$@" $tmpdir/source $tmpdir/target $tmpdir/patch
+  bsdiff $tmpdir/source $tmpdir/target $tmpdir/patch.bs
+  echo "patch for $fn is $(size $tmpdir/patch) [of $(size $tmpdir/target)] ($(size $tmpdir/patch.bs) with bsdiff)"
+  echo "$fn $(size $tmpdir/patch) of $(size $tmpdir/target) bsdiff $(size $tmpdir/patch.bs)" >> /tmp/stats.txt
+  $ADB push $tmpdir/source $WORK_DIR/source || fail "source push failed"
+  run_command rm /data/local/tmp/target
+  $ADB push $tmpdir/patch $WORK_DIR/patch || fail "patch push failed"
+  run_command /data/local/tmp/applypatch /data/local/tmp/source \
+    /data/local/tmp/target $(sha1 $tmpdir/target) $(size $tmpdir/target) \
+    $(sha1 $tmpdir/source):/data/local/tmp/patch \
+    || fail "applypatch of $fn failed"
+  $ADB pull /data/local/tmp/target $tmpdir/result
+  diff -q $tmpdir/target $tmpdir/result || fail "patch output not correct!"
+}
+
+# --------------- basic execution ----------------------
+
+for i in $((zipinfo -1 $START_OTA_PACKAGE; zipinfo -1 $END_OTA_PACKAGE) | \
+           sort | uniq -d | egrep -e '[.](apk|jar|zip)$'); do
+  patch_and_apply $i -z
+done
+patch_and_apply boot.img
+patch_and_apply system/recovery.img
+
+
+# --------------- cleanup ----------------------
+
+cleanup
+
+echo
+echo PASS
+echo
+
diff --git a/recovery/applypatch/imgpatch.cpp b/recovery/applypatch/imgpatch.cpp
new file mode 100644
index 0000000..f5aed76
--- /dev/null
+++ b/recovery/applypatch/imgpatch.cpp
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+// See imgdiff.c in this directory for a description of the patch file
+// format.
+
+#include <stdio.h>
+#include <sys/cdefs.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <vector>
+
+#include "zlib.h"
+#include "openssl/sha.h"
+#include "applypatch/applypatch.h"
+#include "imgdiff.h"
+#include "utils.h"
+
+int ApplyImagePatch(const unsigned char* old_data, ssize_t old_size,
+                    const unsigned char* patch_data, ssize_t patch_size,
+                    SinkFn sink, void* token) {
+  Value patch = {VAL_BLOB, patch_size,
+      reinterpret_cast<char*>(const_cast<unsigned char*>(patch_data))};
+  return ApplyImagePatch(
+      old_data, old_size, &patch, sink, token, nullptr, nullptr);
+}
+
+/*
+ * Apply the patch given in 'patch_filename' to the source data given
+ * by (old_data, old_size).  Write the patched output to the 'output'
+ * file, and update the SHA context with the output data as well.
+ * Return 0 on success.
+ */
+int ApplyImagePatch(const unsigned char* old_data, ssize_t old_size,
+                    const Value* patch,
+                    SinkFn sink, void* token, SHA_CTX* ctx,
+                    const Value* bonus_data) {
+    ssize_t pos = 12;
+    char* header = patch->data;
+    if (patch->size < 12) {
+        printf("patch too short to contain header\n");
+        return -1;
+    }
+
+    // IMGDIFF2 uses CHUNK_NORMAL, CHUNK_DEFLATE, and CHUNK_RAW.
+    // (IMGDIFF1, which is no longer supported, used CHUNK_NORMAL and
+    // CHUNK_GZIP.)
+    if (memcmp(header, "IMGDIFF2", 8) != 0) {
+        printf("corrupt patch file header (magic number)\n");
+        return -1;
+    }
+
+    int num_chunks = Read4(header+8);
+
+    int i;
+    for (i = 0; i < num_chunks; ++i) {
+        // each chunk's header record starts with 4 bytes.
+        if (pos + 4 > patch->size) {
+            printf("failed to read chunk %d record\n", i);
+            return -1;
+        }
+        int type = Read4(patch->data + pos);
+        pos += 4;
+
+        if (type == CHUNK_NORMAL) {
+            char* normal_header = patch->data + pos;
+            pos += 24;
+            if (pos > patch->size) {
+                printf("failed to read chunk %d normal header data\n", i);
+                return -1;
+            }
+
+            size_t src_start = Read8(normal_header);
+            size_t src_len = Read8(normal_header+8);
+            size_t patch_offset = Read8(normal_header+16);
+
+            if (src_start + src_len > static_cast<size_t>(old_size)) {
+                printf("source data too short\n");
+                return -1;
+            }
+            ApplyBSDiffPatch(old_data + src_start, src_len,
+                             patch, patch_offset, sink, token, ctx);
+        } else if (type == CHUNK_RAW) {
+            char* raw_header = patch->data + pos;
+            pos += 4;
+            if (pos > patch->size) {
+                printf("failed to read chunk %d raw header data\n", i);
+                return -1;
+            }
+
+            ssize_t data_len = Read4(raw_header);
+
+            if (pos + data_len > patch->size) {
+                printf("failed to read chunk %d raw data\n", i);
+                return -1;
+            }
+            if (ctx) SHA1_Update(ctx, patch->data + pos, data_len);
+            if (sink((unsigned char*)patch->data + pos,
+                     data_len, token) != data_len) {
+                printf("failed to write chunk %d raw data\n", i);
+                return -1;
+            }
+            pos += data_len;
+        } else if (type == CHUNK_DEFLATE) {
+            // deflate chunks have an additional 60 bytes in their chunk header.
+            char* deflate_header = patch->data + pos;
+            pos += 60;
+            if (pos > patch->size) {
+                printf("failed to read chunk %d deflate header data\n", i);
+                return -1;
+            }
+
+            size_t src_start = Read8(deflate_header);
+            size_t src_len = Read8(deflate_header+8);
+            size_t patch_offset = Read8(deflate_header+16);
+            size_t expanded_len = Read8(deflate_header+24);
+            size_t target_len = Read8(deflate_header+32);
+            int level = Read4(deflate_header+40);
+            int method = Read4(deflate_header+44);
+            int windowBits = Read4(deflate_header+48);
+            int memLevel = Read4(deflate_header+52);
+            int strategy = Read4(deflate_header+56);
+
+            if (src_start + src_len > static_cast<size_t>(old_size)) {
+                printf("source data too short\n");
+                return -1;
+            }
+
+            // Decompress the source data; the chunk header tells us exactly
+            // how big we expect it to be when decompressed.
+
+            // Note: expanded_len will include the bonus data size if
+            // the patch was constructed with bonus data.  The
+            // deflation will come up 'bonus_size' bytes short; these
+            // must be appended from the bonus_data value.
+            size_t bonus_size = (i == 1 && bonus_data != NULL) ? bonus_data->size : 0;
+
+            std::vector<unsigned char> expanded_source(expanded_len);
+            z_stream strm;
+            strm.zalloc = Z_NULL;
+            strm.zfree = Z_NULL;
+            strm.opaque = Z_NULL;
+            strm.avail_in = src_len;
+            strm.next_in = (unsigned char*)(old_data + src_start);
+            strm.avail_out = expanded_len;
+            strm.next_out = expanded_source.data();
+
+            int ret;
+            ret = inflateInit2(&strm, -15);
+            if (ret != Z_OK) {
+                printf("failed to init source inflation: %d\n", ret);
+                return -1;
+            }
+
+            // Because we've provided enough room to accommodate the output
+            // data, we expect one call to inflate() to suffice.
+            ret = inflate(&strm, Z_SYNC_FLUSH);
+            if (ret != Z_STREAM_END) {
+                printf("source inflation returned %d\n", ret);
+                return -1;
+            }
+            // We should have filled the output buffer exactly, except
+            // for the bonus_size.
+            if (strm.avail_out != bonus_size) {
+                printf("source inflation short by %zu bytes\n", strm.avail_out-bonus_size);
+                return -1;
+            }
+            inflateEnd(&strm);
+
+            if (bonus_size) {
+                memcpy(expanded_source.data() + (expanded_len - bonus_size),
+                       bonus_data->data, bonus_size);
+            }
+
+            // Next, apply the bsdiff patch (in memory) to the uncompressed
+            // data.
+            std::vector<unsigned char> uncompressed_target_data;
+            if (ApplyBSDiffPatchMem(expanded_source.data(), expanded_len,
+                                    patch, patch_offset,
+                                    &uncompressed_target_data) != 0) {
+                return -1;
+            }
+            if (uncompressed_target_data.size() != target_len) {
+                printf("expected target len to be %zu, but it's %zu\n",
+                       target_len, uncompressed_target_data.size());
+                return -1;
+            }
+
+            // Now compress the target data and append it to the output.
+
+            // we're done with the expanded_source data buffer, so we'll
+            // reuse that memory to receive the output of deflate.
+            if (expanded_source.size() < 32768U) {
+                expanded_source.resize(32768U);
+            }
+            std::vector<unsigned char>& temp_data = expanded_source;
+
+            // now the deflate stream
+            strm.zalloc = Z_NULL;
+            strm.zfree = Z_NULL;
+            strm.opaque = Z_NULL;
+            strm.avail_in = uncompressed_target_data.size();
+            strm.next_in = uncompressed_target_data.data();
+            ret = deflateInit2(&strm, level, method, windowBits, memLevel, strategy);
+            if (ret != Z_OK) {
+                printf("failed to init uncompressed data deflation: %d\n", ret);
+                return -1;
+            }
+            do {
+                strm.avail_out = temp_data.size();
+                strm.next_out = temp_data.data();
+                ret = deflate(&strm, Z_FINISH);
+                ssize_t have = temp_data.size() - strm.avail_out;
+
+                if (sink(temp_data.data(), have, token) != have) {
+                    printf("failed to write %zd compressed bytes to output\n",
+                           have);
+                    return -1;
+                }
+                if (ctx) SHA1_Update(ctx, temp_data.data(), have);
+            } while (ret != Z_STREAM_END);
+            deflateEnd(&strm);
+        } else {
+            printf("patch chunk %d is unknown type %d\n", i, type);
+            return -1;
+        }
+    }
+
+    return 0;
+}
diff --git a/recovery/applypatch/include/applypatch/applypatch.h b/recovery/applypatch/include/applypatch/applypatch.h
new file mode 100644
index 0000000..9ee39d2
--- /dev/null
+++ b/recovery/applypatch/include/applypatch/applypatch.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#ifndef _APPLYPATCH_H
+#define _APPLYPATCH_H
+
+#include <stdint.h>
+#include <sys/stat.h>
+
+#include <vector>
+
+#include "openssl/sha.h"
+#include "edify/expr.h"
+
+struct FileContents {
+  uint8_t sha1[SHA_DIGEST_LENGTH];
+  std::vector<unsigned char> data;
+  struct stat st;
+};
+
+// When there isn't enough room on the target filesystem to hold the
+// patched version of the file, we copy the original here and delete
+// it to free up space.  If the expected source file doesn't exist, or
+// is corrupted, we look to see if this file contains the bits we want
+// and use it as the source instead.
+#define CACHE_TEMP_SOURCE "/cache/saved.file"
+
+typedef ssize_t (*SinkFn)(const unsigned char*, ssize_t, void*);
+
+// applypatch.c
+int ShowLicenses();
+size_t FreeSpaceForFile(const char* filename);
+int CacheSizeCheck(size_t bytes);
+int ParseSha1(const char* str, uint8_t* digest);
+
+int applypatch_flash(const char* source_filename, const char* target_filename,
+                     const char* target_sha1_str, size_t target_size);
+int applypatch(const char* source_filename,
+               const char* target_filename,
+               const char* target_sha1_str,
+               size_t target_size,
+               int num_patches,
+               char** const patch_sha1_str,
+               Value** patch_data,
+               Value* bonus_data);
+int applypatch_check(const char* filename,
+                     int num_patches,
+                     char** const patch_sha1_str);
+
+int LoadFileContents(const char* filename, FileContents* file);
+int SaveFileContents(const char* filename, const FileContents* file);
+void FreeFileContents(FileContents* file);
+int FindMatchingPatch(uint8_t* sha1, char* const * const patch_sha1_str,
+                      int num_patches);
+
+// bsdiff.cpp
+void ShowBSDiffLicense();
+int ApplyBSDiffPatch(const unsigned char* old_data, ssize_t old_size,
+                     const Value* patch, ssize_t patch_offset,
+                     SinkFn sink, void* token, SHA_CTX* ctx);
+int ApplyBSDiffPatchMem(const unsigned char* old_data, ssize_t old_size,
+                        const Value* patch, ssize_t patch_offset,
+                        std::vector<unsigned char>* new_data);
+
+// imgpatch.cpp
+int ApplyImagePatch(const unsigned char* old_data, ssize_t old_size,
+                    const Value* patch,
+                    SinkFn sink, void* token, SHA_CTX* ctx,
+                    const Value* bonus_data);
+
+// freecache.cpp
+int MakeFreeSpaceOnCache(size_t bytes_needed);
+
+#endif
diff --git a/recovery/applypatch/include/applypatch/imgpatch.h b/recovery/applypatch/include/applypatch/imgpatch.h
new file mode 100644
index 0000000..64d9aa9
--- /dev/null
+++ b/recovery/applypatch/include/applypatch/imgpatch.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#ifndef _IMGPATCH_H
+#define _IMGPATCH_H
+
+typedef ssize_t (*SinkFn)(const unsigned char*, ssize_t, void*);
+
+int ApplyImagePatch(const unsigned char* old_data, ssize_t old_size,
+                    const unsigned char* patch_data, ssize_t patch_size,
+                    SinkFn sink, void* token);
+
+#endif  //_IMGPATCH_H
diff --git a/recovery/applypatch/libimgpatch.pc b/recovery/applypatch/libimgpatch.pc
new file mode 100644
index 0000000..e500293
--- /dev/null
+++ b/recovery/applypatch/libimgpatch.pc
@@ -0,0 +1,6 @@
+# This file is for libimgpatch in Chrome OS.
+
+Name: libimgpatch
+Description: Apply imgdiff patch
+Version: 0.0.1
+Libs: -limgpatch -lbz2 -lz
diff --git a/recovery/applypatch/main.cpp b/recovery/applypatch/main.cpp
new file mode 100644
index 0000000..1968ae4
--- /dev/null
+++ b/recovery/applypatch/main.cpp
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <memory>
+#include <vector>
+
+#include "applypatch/applypatch.h"
+#include "edify/expr.h"
+#include "openssl/sha.h"
+
+static int CheckMode(int argc, char** argv) {
+    if (argc < 3) {
+        return 2;
+    }
+    return applypatch_check(argv[2], argc-3, argv+3);
+}
+
+static int SpaceMode(int argc, char** argv) {
+    if (argc != 3) {
+        return 2;
+    }
+    char* endptr;
+    size_t bytes = strtol(argv[2], &endptr, 10);
+    if (bytes == 0 && endptr == argv[2]) {
+        printf("can't parse \"%s\" as byte count\n\n", argv[2]);
+        return 1;
+    }
+    return CacheSizeCheck(bytes);
+}
+
+// Parse arguments (which should be of the form "<sha1>:<filename>"
+// into the new parallel arrays *sha1s and *files.Returns true on
+// success.
+static bool ParsePatchArgs(int argc, char** argv, std::vector<char*>* sha1s,
+                           std::vector<FileContents>* files) {
+    uint8_t digest[SHA_DIGEST_LENGTH];
+
+    for (int i = 0; i < argc; ++i) {
+        char* colon = strchr(argv[i], ':');
+        if (colon == nullptr) {
+            printf("no ':' in patch argument \"%s\"\n", argv[i]);
+            return false;
+        }
+        *colon = '\0';
+        ++colon;
+        if (ParseSha1(argv[i], digest) != 0) {
+            printf("failed to parse sha1 \"%s\"\n", argv[i]);
+            return false;
+        }
+
+        sha1s->push_back(argv[i]);
+        FileContents fc;
+        if (LoadFileContents(colon, &fc) != 0) {
+            return false;
+        }
+        files->push_back(std::move(fc));
+    }
+    return true;
+}
+
+static int FlashMode(const char* src_filename, const char* tgt_filename,
+                     const char* tgt_sha1, size_t tgt_size) {
+    return applypatch_flash(src_filename, tgt_filename, tgt_sha1, tgt_size);
+}
+
+static int PatchMode(int argc, char** argv) {
+    FileContents bonusFc;
+    Value bonusValue;
+    Value* bonus = nullptr;
+
+    if (argc >= 3 && strcmp(argv[1], "-b") == 0) {
+        if (LoadFileContents(argv[2], &bonusFc) != 0) {
+            printf("failed to load bonus file %s\n", argv[2]);
+            return 1;
+        }
+        bonus = &bonusValue;
+        bonus->type = VAL_BLOB;
+        bonus->size = bonusFc.data.size();
+        bonus->data = reinterpret_cast<char*>(bonusFc.data.data());
+        argc -= 2;
+        argv += 2;
+    }
+
+    if (argc < 4) {
+        return 2;
+    }
+
+    char* endptr;
+    size_t target_size = strtol(argv[4], &endptr, 10);
+    if (target_size == 0 && endptr == argv[4]) {
+        printf("can't parse \"%s\" as byte count\n\n", argv[4]);
+        return 1;
+    }
+
+    // If no <src-sha1>:<patch> is provided, it is in flash mode.
+    if (argc == 5) {
+        if (bonus != nullptr) {
+            printf("bonus file not supported in flash mode\n");
+            return 1;
+        }
+        return FlashMode(argv[1], argv[2], argv[3], target_size);
+    }
+    std::vector<char*> sha1s;
+    std::vector<FileContents> files;
+    if (!ParsePatchArgs(argc-5, argv+5, &sha1s, &files)) {
+        printf("failed to parse patch args\n");
+        return 1;
+    }
+    std::vector<Value> patches(files.size());
+    std::vector<Value*> patch_ptrs(files.size());
+    for (size_t i = 0; i < files.size(); ++i) {
+        patches[i].type = VAL_BLOB;
+        patches[i].size = files[i].data.size();
+        patches[i].data = reinterpret_cast<char*>(files[i].data.data());
+        patch_ptrs[i] = &patches[i];
+    }
+    return applypatch(argv[1], argv[2], argv[3], target_size,
+                      patch_ptrs.size(), sha1s.data(),
+                      patch_ptrs.data(), bonus);
+}
+
+// This program applies binary patches to files in a way that is safe
+// (the original file is not touched until we have the desired
+// replacement for it) and idempotent (it's okay to run this program
+// multiple times).
+//
+// - if the sha1 hash of <tgt-file> is <tgt-sha1>, does nothing and exits
+//   successfully.
+//
+// - otherwise, if no <src-sha1>:<patch> is provided, flashes <tgt-file> with
+//   <src-file>. <tgt-file> must be a partition name, while <src-file> must
+//   be a regular image file. <src-file> will not be deleted on success.
+//
+// - otherwise, if the sha1 hash of <src-file> is <src-sha1>, applies the
+//   bsdiff <patch> to <src-file> to produce a new file (the type of patch
+//   is automatically detected from the file header).  If that new
+//   file has sha1 hash <tgt-sha1>, moves it to replace <tgt-file>, and
+//   exits successfully.  Note that if <src-file> and <tgt-file> are
+//   not the same, <src-file> is NOT deleted on success.  <tgt-file>
+//   may be the string "-" to mean "the same as src-file".
+//
+// - otherwise, or if any error is encountered, exits with non-zero
+//   status.
+//
+// <src-file> (or <file> in check mode) may refer to an EMMC partition
+// to read the source data.  See the comments for the
+// LoadPartitionContents() function for the format of such a filename.
+
+int main(int argc, char** argv) {
+    if (argc < 2) {
+      usage:
+        printf(
+            "usage: %s [-b <bonus-file>] <src-file> <tgt-file> <tgt-sha1> <tgt-size> "
+            "[<src-sha1>:<patch> ...]\n"
+            "   or  %s -c <file> [<sha1> ...]\n"
+            "   or  %s -s <bytes>\n"
+            "   or  %s -l\n"
+            "\n"
+            "Filenames may be of the form\n"
+            "  EMMC:<partition>:<len_1>:<sha1_1>:<len_2>:<sha1_2>:...\n"
+            "to specify reading from or writing to an EMMC partition.\n\n",
+            argv[0], argv[0], argv[0], argv[0]);
+        return 2;
+    }
+
+    int result;
+
+    if (strncmp(argv[1], "-l", 3) == 0) {
+        result = ShowLicenses();
+    } else if (strncmp(argv[1], "-c", 3) == 0) {
+        result = CheckMode(argc, argv);
+    } else if (strncmp(argv[1], "-s", 3) == 0) {
+        result = SpaceMode(argc, argv);
+    } else {
+        result = PatchMode(argc, argv);
+    }
+
+    if (result == 2) {
+        goto usage;
+    }
+    return result;
+}
diff --git a/recovery/applypatch/testdata/new.file b/recovery/applypatch/testdata/new.file
new file mode 100644
index 0000000..cdeb8fd
--- /dev/null
+++ b/recovery/applypatch/testdata/new.file
Binary files differ
diff --git a/recovery/applypatch/testdata/old.file b/recovery/applypatch/testdata/old.file
new file mode 100644
index 0000000..166c873
--- /dev/null
+++ b/recovery/applypatch/testdata/old.file
Binary files differ
diff --git a/recovery/applypatch/testdata/patch.bsdiff b/recovery/applypatch/testdata/patch.bsdiff
new file mode 100644
index 0000000..b78d385
--- /dev/null
+++ b/recovery/applypatch/testdata/patch.bsdiff
Binary files differ
diff --git a/recovery/applypatch/utils.cpp b/recovery/applypatch/utils.cpp
new file mode 100644
index 0000000..fef250f
--- /dev/null
+++ b/recovery/applypatch/utils.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <stdio.h>
+
+#include "utils.h"
+
+/** Write a 4-byte value to f in little-endian order. */
+void Write4(int value, FILE* f) {
+  fputc(value & 0xff, f);
+  fputc((value >> 8) & 0xff, f);
+  fputc((value >> 16) & 0xff, f);
+  fputc((value >> 24) & 0xff, f);
+}
+
+/** Write an 8-byte value to f in little-endian order. */
+void Write8(int64_t value, FILE* f) {
+  fputc(value & 0xff, f);
+  fputc((value >> 8) & 0xff, f);
+  fputc((value >> 16) & 0xff, f);
+  fputc((value >> 24) & 0xff, f);
+  fputc((value >> 32) & 0xff, f);
+  fputc((value >> 40) & 0xff, f);
+  fputc((value >> 48) & 0xff, f);
+  fputc((value >> 56) & 0xff, f);
+}
+
+int Read2(void* pv) {
+    unsigned char* p = reinterpret_cast<unsigned char*>(pv);
+    return (int)(((unsigned int)p[1] << 8) |
+                 (unsigned int)p[0]);
+}
+
+int Read4(void* pv) {
+    unsigned char* p = reinterpret_cast<unsigned char*>(pv);
+    return (int)(((unsigned int)p[3] << 24) |
+                 ((unsigned int)p[2] << 16) |
+                 ((unsigned int)p[1] << 8) |
+                 (unsigned int)p[0]);
+}
+
+int64_t Read8(void* pv) {
+    unsigned char* p = reinterpret_cast<unsigned char*>(pv);
+    return (int64_t)(((uint64_t)p[7] << 56) |
+                       ((uint64_t)p[6] << 48) |
+                       ((uint64_t)p[5] << 40) |
+                       ((uint64_t)p[4] << 32) |
+                       ((uint64_t)p[3] << 24) |
+                       ((uint64_t)p[2] << 16) |
+                       ((uint64_t)p[1] << 8) |
+                       (uint64_t)p[0]);
+}
diff --git a/recovery/applypatch/utils.h b/recovery/applypatch/utils.h
new file mode 100644
index 0000000..1c34edd
--- /dev/null
+++ b/recovery/applypatch/utils.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#ifndef _BUILD_TOOLS_APPLYPATCH_UTILS_H
+#define _BUILD_TOOLS_APPLYPATCH_UTILS_H
+
+#include <inttypes.h>
+#include <stdio.h>
+
+// Read and write little-endian values of various sizes.
+
+void Write4(int value, FILE* f);
+void Write8(int64_t value, FILE* f);
+int Read2(void* p);
+int Read4(void* p);
+int64_t Read8(void* p);
+
+#endif //  _BUILD_TOOLS_APPLYPATCH_UTILS_H
diff --git a/recovery/asn1_decoder.cpp b/recovery/asn1_decoder.cpp
new file mode 100644
index 0000000..e7aef78
--- /dev/null
+++ b/recovery/asn1_decoder.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include <malloc.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "asn1_decoder.h"
+
+
+typedef struct asn1_context {
+    size_t length;
+    uint8_t* p;
+    int app_type;
+} asn1_context_t;
+
+
+static const int kMaskConstructed = 0xE0;
+static const int kMaskTag = 0x7F;
+static const int kMaskAppType = 0x1F;
+
+static const int kTagOctetString = 0x04;
+static const int kTagOid = 0x06;
+static const int kTagSequence = 0x30;
+static const int kTagSet = 0x31;
+static const int kTagConstructed = 0xA0;
+
+asn1_context_t* asn1_context_new(uint8_t* buffer, size_t length) {
+    asn1_context_t* ctx = (asn1_context_t*) calloc(1, sizeof(asn1_context_t));
+    if (ctx == NULL) {
+        return NULL;
+    }
+    ctx->p = buffer;
+    ctx->length = length;
+    return ctx;
+}
+
+void asn1_context_free(asn1_context_t* ctx) {
+    free(ctx);
+}
+
+static inline int peek_byte(asn1_context_t* ctx) {
+    if (ctx->length <= 0) {
+        return -1;
+    }
+    return *ctx->p;
+}
+
+static inline int get_byte(asn1_context_t* ctx) {
+    if (ctx->length <= 0) {
+        return -1;
+    }
+    int byte = *ctx->p;
+    ctx->p++;
+    ctx->length--;
+    return byte;
+}
+
+static inline bool skip_bytes(asn1_context_t* ctx, size_t num_skip) {
+    if (ctx->length < num_skip) {
+        return false;
+    }
+    ctx->p += num_skip;
+    ctx->length -= num_skip;
+    return true;
+}
+
+static bool decode_length(asn1_context_t* ctx, size_t* out_len) {
+    int num_octets = get_byte(ctx);
+    if (num_octets == -1) {
+        return false;
+    }
+    if ((num_octets & 0x80) == 0x00) {
+        *out_len = num_octets;
+        return 1;
+    }
+    num_octets &= kMaskTag;
+    if ((size_t)num_octets >= sizeof(size_t)) {
+        return false;
+    }
+    size_t length = 0;
+    for (int i = 0; i < num_octets; ++i) {
+        int byte = get_byte(ctx);
+        if (byte == -1) {
+            return false;
+        }
+        length <<= 8;
+        length += byte;
+    }
+    *out_len = length;
+    return true;
+}
+
+/**
+ * Returns the constructed type and advances the pointer. E.g. A0 -> 0
+ */
+asn1_context_t* asn1_constructed_get(asn1_context_t* ctx) {
+    int type = get_byte(ctx);
+    if (type == -1 || (type & kMaskConstructed) != kTagConstructed) {
+        return NULL;
+    }
+    size_t length;
+    if (!decode_length(ctx, &length) || length > ctx->length) {
+        return NULL;
+    }
+    asn1_context_t* app_ctx = asn1_context_new(ctx->p, length);
+    app_ctx->app_type = type & kMaskAppType;
+    return app_ctx;
+}
+
+bool asn1_constructed_skip_all(asn1_context_t* ctx) {
+    int byte = peek_byte(ctx);
+    while (byte != -1 && (byte & kMaskConstructed) == kTagConstructed) {
+        skip_bytes(ctx, 1);
+        size_t length;
+        if (!decode_length(ctx, &length) || !skip_bytes(ctx, length)) {
+            return false;
+        }
+        byte = peek_byte(ctx);
+    }
+    return byte != -1;
+}
+
+int asn1_constructed_type(asn1_context_t* ctx) {
+    return ctx->app_type;
+}
+
+asn1_context_t* asn1_sequence_get(asn1_context_t* ctx) {
+    if ((get_byte(ctx) & kMaskTag) != kTagSequence) {
+        return NULL;
+    }
+    size_t length;
+    if (!decode_length(ctx, &length) || length > ctx->length) {
+        return NULL;
+    }
+    return asn1_context_new(ctx->p, length);
+}
+
+asn1_context_t* asn1_set_get(asn1_context_t* ctx) {
+    if ((get_byte(ctx) & kMaskTag) != kTagSet) {
+        return NULL;
+    }
+    size_t length;
+    if (!decode_length(ctx, &length) || length > ctx->length) {
+        return NULL;
+    }
+    return asn1_context_new(ctx->p, length);
+}
+
+bool asn1_sequence_next(asn1_context_t* ctx) {
+    size_t length;
+    if (get_byte(ctx) == -1 || !decode_length(ctx, &length) || !skip_bytes(ctx, length)) {
+        return false;
+    }
+    return true;
+}
+
+bool asn1_oid_get(asn1_context_t* ctx, uint8_t** oid, size_t* length) {
+    if (get_byte(ctx) != kTagOid) {
+        return false;
+    }
+    if (!decode_length(ctx, length) || *length == 0 || *length > ctx->length) {
+        return false;
+    }
+    *oid = ctx->p;
+    return true;
+}
+
+bool asn1_octet_string_get(asn1_context_t* ctx, uint8_t** octet_string, size_t* length) {
+    if (get_byte(ctx) != kTagOctetString) {
+        return false;
+    }
+    if (!decode_length(ctx, length) || *length == 0 || *length > ctx->length) {
+        return false;
+    }
+    *octet_string = ctx->p;
+    return true;
+}
diff --git a/recovery/asn1_decoder.h b/recovery/asn1_decoder.h
new file mode 100644
index 0000000..b17141c
--- /dev/null
+++ b/recovery/asn1_decoder.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+
+#ifndef ASN1_DECODER_H_
+#define ASN1_DECODER_H_
+
+#include <stdint.h>
+
+typedef struct asn1_context asn1_context_t;
+
+asn1_context_t* asn1_context_new(uint8_t* buffer, size_t length);
+void asn1_context_free(asn1_context_t* ctx);
+asn1_context_t* asn1_constructed_get(asn1_context_t* ctx);
+bool asn1_constructed_skip_all(asn1_context_t* ctx);
+int asn1_constructed_type(asn1_context_t* ctx);
+asn1_context_t* asn1_sequence_get(asn1_context_t* ctx);
+asn1_context_t* asn1_set_get(asn1_context_t* ctx);
+bool asn1_sequence_next(asn1_context_t* seq);
+bool asn1_oid_get(asn1_context_t* ctx, uint8_t** oid, size_t* length);
+bool asn1_octet_string_get(asn1_context_t* ctx, uint8_t** octet_string, size_t* length);
+
+#endif /* ASN1_DECODER_H_ */
diff --git a/recovery/bootloader.cpp b/recovery/bootloader.cpp
new file mode 100644
index 0000000..783f56e
--- /dev/null
+++ b/recovery/bootloader.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <fs_mgr.h>
+
+#include "bootloader.h"
+#include "common.h"
+#include "roots.h"
+#include <android-base/unique_fd.h>
+
+static int get_bootloader_message_block(bootloader_message* out, const Volume* v);
+static int set_bootloader_message_block(const bootloader_message* in, const Volume* v);
+
+int get_bootloader_message(bootloader_message* out) {
+    Volume* v = volume_for_path("/misc");
+    if (v == nullptr) {
+        LOGE("Cannot load volume /misc!\n");
+        return -1;
+    }
+    if (strcmp(v->fs_type, "emmc") == 0) {
+        return get_bootloader_message_block(out, v);
+    }
+    LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
+    return -1;
+}
+
+int set_bootloader_message(const bootloader_message* in) {
+    Volume* v = volume_for_path("/misc");
+    if (v == nullptr) {
+        LOGE("Cannot load volume /misc!\n");
+        return -1;
+    }
+    if (strcmp(v->fs_type, "emmc") == 0) {
+        return set_bootloader_message_block(in, v);
+    }
+    LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
+    return -1;
+}
+
+// ------------------------------------
+// for misc partitions on block devices
+// ------------------------------------
+
+static void wait_for_device(const char* fn) {
+    int tries = 0;
+    int ret;
+    do {
+        ++tries;
+        struct stat buf;
+        ret = stat(fn, &buf);
+        if (ret == -1) {
+            printf("failed to stat \"%s\" try %d: %s\n", fn, tries, strerror(errno));
+            sleep(1);
+        }
+    } while (ret && tries < 10);
+
+    if (ret) {
+        printf("failed to stat \"%s\"\n", fn);
+    }
+}
+
+static int get_bootloader_message_block(bootloader_message* out,
+                                        const Volume* v) {
+    wait_for_device(v->blk_device);
+    FILE* f = fopen(v->blk_device, "rb");
+    if (f == nullptr) {
+        LOGE("failed to open \"%s\": %s\n", v->blk_device, strerror(errno));
+        return -1;
+    }
+    bootloader_message temp;
+    int count = fread(&temp, sizeof(temp), 1, f);
+    if (count != 1) {
+        LOGE("failed to read \"%s\": %s\n", v->blk_device, strerror(errno));
+        return -1;
+    }
+    if (fclose(f) != 0) {
+        LOGE("failed to close \"%s\": %s\n", v->blk_device, strerror(errno));
+        return -1;
+    }
+    memcpy(out, &temp, sizeof(temp));
+    return 0;
+}
+
+static int set_bootloader_message_block(const bootloader_message* in,
+                                        const Volume* v) {
+    wait_for_device(v->blk_device);
+    android::base::unique_fd fd(open(v->blk_device, O_WRONLY | O_SYNC));
+    if (fd == -1) {
+        LOGE("failed to open \"%s\": %s\n", v->blk_device, strerror(errno));
+        return -1;
+    }
+
+    size_t written = 0;
+    const uint8_t* start = reinterpret_cast<const uint8_t*>(in);
+    size_t total = sizeof(*in);
+    while (written < total) {
+        ssize_t wrote = TEMP_FAILURE_RETRY(write(fd, start + written, total - written));
+        if (wrote == -1) {
+            LOGE("failed to write %" PRId64 " bytes: %s\n",
+                 static_cast<off64_t>(written), strerror(errno));
+            return -1;
+        }
+        written += wrote;
+    }
+
+    if (fsync(fd) == -1) {
+        LOGE("failed to fsync \"%s\": %s\n", v->blk_device, strerror(errno));
+        return -1;
+    }
+    return 0;
+}
diff --git a/recovery/bootloader.h b/recovery/bootloader.h
new file mode 100644
index 0000000..1801705
--- /dev/null
+++ b/recovery/bootloader.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#ifndef _RECOVERY_BOOTLOADER_H
+#define _RECOVERY_BOOTLOADER_H
+
+#include <assert.h>
+
+/* Bootloader Message (2-KiB)
+ *
+ * This structure describes the content of a block in flash
+ * that is used for recovery and the bootloader to talk to
+ * each other.
+ *
+ * The command field is updated by linux when it wants to
+ * reboot into recovery or to update radio or bootloader firmware.
+ * It is also updated by the bootloader when firmware update
+ * is complete (to boot into recovery for any final cleanup)
+ *
+ * The status field is written by the bootloader after the
+ * completion of an "update-radio" or "update-hboot" command.
+ *
+ * The recovery field is only written by linux and used
+ * for the system to send a message to recovery or the
+ * other way around.
+ *
+ * The stage field is written by packages which restart themselves
+ * multiple times, so that the UI can reflect which invocation of the
+ * package it is.  If the value is of the format "#/#" (eg, "1/3"),
+ * the UI will add a simple indicator of that status.
+ *
+ * We used to have slot_suffix field for A/B boot control metadata in
+ * this struct, which gets unintentionally cleared by recovery or
+ * uncrypt. Move it into struct bootloader_message_ab to avoid the
+ * issue.
+ */
+struct bootloader_message {
+    char command[32];
+    char status[32];
+    char recovery[768];
+
+    // The 'recovery' field used to be 1024 bytes.  It has only ever
+    // been used to store the recovery command line, so 768 bytes
+    // should be plenty.  We carve off the last 256 bytes to store the
+    // stage string (for multistage packages) and possible future
+    // expansion.
+    char stage[32];
+
+    // The 'reserved' field used to be 224 bytes when it was initially
+    // carved off from the 1024-byte recovery field. Bump it up to
+    // 1184-byte so that the entire bootloader_message struct rounds up
+    // to 2048-byte.
+    char reserved[1184];
+};
+
+/**
+ * We must be cautious when changing the bootloader_message struct size,
+ * because A/B-specific fields may end up with different offsets.
+ */
+#if (__STDC_VERSION__ >= 201112L) || defined(__cplusplus)
+static_assert(sizeof(struct bootloader_message) == 2048,
+              "struct bootloader_message size changes, which may break A/B devices");
+#endif
+
+/**
+ * The A/B-specific bootloader message structure (4-KiB).
+ *
+ * We separate A/B boot control metadata from the regular bootloader
+ * message struct and keep it here. Everything that's A/B-specific
+ * stays after struct bootloader_message, which should be managed by
+ * the A/B-bootloader or boot control HAL.
+ *
+ * The slot_suffix field is used for A/B implementations where the
+ * bootloader does not set the androidboot.ro.boot.slot_suffix kernel
+ * commandline parameter. This is used by fs_mgr to mount /system and
+ * other partitions with the slotselect flag set in fstab. A/B
+ * implementations are free to use all 32 bytes and may store private
+ * data past the first NUL-byte in this field. It is encouraged, but
+ * not mandatory, to use 'struct bootloader_control' described below.
+ */
+struct bootloader_message_ab {
+    struct bootloader_message message;
+    char slot_suffix[32];
+
+    // Round up the entire struct to 4096-byte.
+    char reserved[2016];
+};
+
+/**
+ * Be cautious about the struct size change, in case we put anything post
+ * bootloader_message_ab struct (b/29159185).
+ */
+#if (__STDC_VERSION__ >= 201112L) || defined(__cplusplus)
+static_assert(sizeof(struct bootloader_message_ab) == 4096,
+              "struct bootloader_message_ab size changes");
+#endif
+
+#define BOOT_CTRL_MAGIC   0x42414342 /* Bootloader Control AB */
+#define BOOT_CTRL_VERSION 1
+
+struct slot_metadata {
+    // Slot priority with 15 meaning highest priority, 1 lowest
+    // priority and 0 the slot is unbootable.
+    uint8_t priority : 4;
+    // Number of times left attempting to boot this slot.
+    uint8_t tries_remaining : 3;
+    // 1 if this slot has booted successfully, 0 otherwise.
+    uint8_t successful_boot : 1;
+    // 1 if this slot is corrupted from a dm-verity corruption, 0
+    // otherwise.
+    uint8_t verity_corrupted : 1;
+    // Reserved for further use.
+    uint8_t reserved : 7;
+} __attribute__((packed));
+
+/* Bootloader Control AB
+ *
+ * This struct can be used to manage A/B metadata. It is designed to
+ * be put in the 'slot_suffix' field of the 'bootloader_message'
+ * structure described above. It is encouraged to use the
+ * 'bootloader_control' structure to store the A/B metadata, but not
+ * mandatory.
+ */
+struct bootloader_control {
+    // NUL terminated active slot suffix.
+    char slot_suffix[4];
+    // Bootloader Control AB magic number (see BOOT_CTRL_MAGIC).
+    uint32_t magic;
+    // Version of struct being used (see BOOT_CTRL_VERSION).
+    uint8_t version;
+    // Number of slots being managed.
+    uint8_t nb_slot : 3;
+    // Number of times left attempting to boot recovery.
+    uint8_t recovery_tries_remaining : 3;
+    // Ensure 4-bytes alignment for slot_info field.
+    uint8_t reserved0[2];
+    // Per-slot information.  Up to 4 slots.
+    struct slot_metadata slot_info[4];
+    // Reserved for further use.
+    uint8_t reserved1[8];
+    // CRC32 of all 28 bytes preceding this field (little endian
+    // format).
+    uint32_t crc32_le;
+} __attribute__((packed));
+
+#if (__STDC_VERSION__ >= 201112L) || defined(__cplusplus)
+static_assert(sizeof(struct bootloader_control) ==
+              sizeof(((struct bootloader_message_ab *)0)->slot_suffix),
+              "struct bootloader_control has wrong size");
+#endif
+
+/* Read and write the bootloader command from the "misc" partition.
+ * These return zero on success.
+ */
+int get_bootloader_message(struct bootloader_message *out);
+int set_bootloader_message(const struct bootloader_message *in);
+
+#endif
diff --git a/recovery/common.h b/recovery/common.h
new file mode 100644
index 0000000..de8b409
--- /dev/null
+++ b/recovery/common.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef RECOVERY_COMMON_H
+#define RECOVERY_COMMON_H
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+#define LOGE(...) ui_print("E:" __VA_ARGS__)
+#define LOGW(...) fprintf(stdout, "W:" __VA_ARGS__)
+#define LOGI(...) fprintf(stdout, "I:" __VA_ARGS__)
+
+#if 0
+#define LOGV(...) fprintf(stdout, "V:" __VA_ARGS__)
+#define LOGD(...) fprintf(stdout, "D:" __VA_ARGS__)
+#else
+#define LOGV(...) do {} while (0)
+#define LOGD(...) do {} while (0)
+#endif
+
+#define STRINGIFY(x) #x
+#define EXPAND(x) STRINGIFY(x)
+
+extern bool modified_flash;
+typedef struct fstab_rec Volume;
+
+// fopen a file, mounting volumes and making parent dirs as necessary.
+FILE* fopen_path(const char *path, const char *mode);
+
+void ui_print(const char* format, ...);
+
+bool is_ro_debuggable();
+
+#endif  // RECOVERY_COMMON_H
diff --git a/recovery/default_device.cpp b/recovery/default_device.cpp
new file mode 100644
index 0000000..a971866
--- /dev/null
+++ b/recovery/default_device.cpp
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include "device.h"
+#include "screen_ui.h"
+
+Device* make_device() {
+  return new Device(new ScreenRecoveryUI);
+}
diff --git a/recovery/device.cpp b/recovery/device.cpp
new file mode 100644
index 0000000..fd1a987
--- /dev/null
+++ b/recovery/device.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include "device.h"
+
+static const char* MENU_ITEMS[] = {
+    "Reboot system now",
+    "Reboot to bootloader",
+    "Apply update from ADB",
+    "Apply update from SD card",
+    "Wipe data/factory reset",
+    "Wipe cache partition",
+    "Mount /system",
+    "View recovery logs",
+    "Power off",
+    NULL
+};
+
+const char* const* Device::GetMenuItems() {
+  return MENU_ITEMS;
+}
+
+Device::BuiltinAction Device::InvokeMenuItem(int menu_position) {
+  switch (menu_position) {
+    case 0: return REBOOT;
+    case 1: return REBOOT_BOOTLOADER;
+    case 2: return APPLY_ADB_SIDELOAD;
+    case 3: return APPLY_SDCARD;
+    case 4: return WIPE_DATA;
+    case 5: return WIPE_CACHE;
+    case 6: return MOUNT_SYSTEM;
+    case 7: return VIEW_RECOVERY_LOGS;
+    case 8: return SHUTDOWN;
+    default: return NO_ACTION;
+  }
+}
+
+int Device::HandleMenuKey(int key, int visible) {
+  if (!visible) {
+    return kNoAction;
+  }
+
+  switch (key) {
+    case KEY_DOWN:
+    case KEY_VOLUMEDOWN:
+      return kHighlightDown;
+
+    case KEY_UP:
+    case KEY_VOLUMEUP:
+      return kHighlightUp;
+
+    case KEY_ENTER:
+    case KEY_POWER:
+      return kInvokeItem;
+
+    default:
+      // If you have all of the above buttons, any other buttons
+      // are ignored. Otherwise, any button cycles the highlight.
+      return ui_->HasThreeButtons() ? kNoAction : kHighlightDown;
+  }
+}
diff --git a/recovery/device.h b/recovery/device.h
new file mode 100644
index 0000000..e0a3ff7
--- /dev/null
+++ b/recovery/device.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#ifndef _RECOVERY_DEVICE_H
+#define _RECOVERY_DEVICE_H
+
+#include "ui.h"
+
+class Device {
+  public:
+    explicit Device(RecoveryUI* ui) : ui_(ui) { }
+    virtual ~Device() { }
+
+    // Called to obtain the UI object that should be used to display
+    // the recovery user interface for this device.  You should not
+    // have called Init() on the UI object already, the caller will do
+    // that after this method returns.
+    virtual RecoveryUI* GetUI() { return ui_; }
+
+    // Called when recovery starts up (after the UI has been obtained
+    // and initialized and after the arguments have been parsed, but
+    // before anything else).
+    virtual void StartRecovery() { };
+
+    // Called from the main thread when recovery is at the main menu
+    // and waiting for input, and a key is pressed.  (Note that "at"
+    // the main menu does not necessarily mean the menu is visible;
+    // recovery will be at the main menu with it invisible after an
+    // unsuccessful operation [ie OTA package failure], or if recovery
+    // is started with no command.)
+    //
+    // key is the code of the key just pressed.  (You can call
+    // IsKeyPressed() on the RecoveryUI object you returned from GetUI
+    // if you want to find out if other keys are held down.)
+    //
+    // visible is true if the menu is visible.
+    //
+    // Return one of the defined constants below in order to:
+    //
+    //   - move the menu highlight (kHighlight{Up,Down})
+    //   - invoke the highlighted item (kInvokeItem)
+    //   - do nothing (kNoAction)
+    //   - invoke a specific action (a menu position: any non-negative number)
+    virtual int HandleMenuKey(int key, int visible);
+
+    enum BuiltinAction {
+        NO_ACTION = 0,
+        REBOOT = 1,
+        APPLY_SDCARD = 2,
+        // APPLY_CACHE was 3.
+        APPLY_ADB_SIDELOAD = 4,
+        WIPE_DATA = 5,
+        WIPE_CACHE = 6,
+        REBOOT_BOOTLOADER = 7,
+        SHUTDOWN = 8,
+        VIEW_RECOVERY_LOGS = 9,
+        MOUNT_SYSTEM = 10,
+    };
+
+    // Return the list of menu items (an array of strings,
+    // NULL-terminated).  The menu_position passed to InvokeMenuItem
+    // will correspond to the indexes into this array.
+    virtual const char* const* GetMenuItems();
+
+    // Perform a recovery action selected from the menu.
+    // 'menu_position' will be the item number of the selected menu
+    // item, or a non-negative number returned from
+    // device_handle_key().  The menu will be hidden when this is
+    // called; implementations can call ui_print() to print
+    // information to the screen.  If the menu position is one of the
+    // builtin actions, you can just return the corresponding enum
+    // value.  If it is an action specific to your device, you
+    // actually perform it here and return NO_ACTION.
+    virtual BuiltinAction InvokeMenuItem(int menu_position);
+
+    static const int kNoAction = -1;
+    static const int kHighlightUp = -2;
+    static const int kHighlightDown = -3;
+    static const int kInvokeItem = -4;
+
+    // Called before and after we do a wipe data/factory reset operation,
+    // either via a reboot from the main system with the --wipe_data flag,
+    // or when the user boots into recovery image manually and selects the
+    // option from the menu, to perform whatever device-specific wiping
+    // actions are needed.
+    // Return true on success; returning false from PreWipeData will prevent
+    // the regular wipe, and returning false from PostWipeData will cause
+    // the wipe to be considered a failure.
+    virtual bool PreWipeData() { return true; }
+    virtual bool PostWipeData() { return true; }
+
+  private:
+    RecoveryUI* ui_;
+};
+
+// The device-specific library must define this function (or the
+// default one will be used, if there is no device-specific library).
+// It returns the Device object that recovery should use.
+Device* make_device();
+
+#endif  // _DEVICE_H
diff --git a/recovery/edify/Android.mk b/recovery/edify/Android.mk
new file mode 100644
index 0000000..038dec0
--- /dev/null
+++ b/recovery/edify/Android.mk
@@ -0,0 +1,40 @@
+# Copyright 2009 The Android Open Source Project
+
+LOCAL_PATH := $(call my-dir)
+
+edify_src_files := \
+	lexer.ll \
+	parser.yy \
+	expr.cpp
+
+#
+# Build the host-side command line tool
+#
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+		$(edify_src_files) \
+		main.cpp
+
+LOCAL_CPPFLAGS := -g -O0
+LOCAL_MODULE := edify
+LOCAL_YACCFLAGS := -v
+LOCAL_CPPFLAGS += -Wno-unused-parameter
+LOCAL_CPPFLAGS += -Wno-deprecated-register
+LOCAL_CLANG := true
+
+include $(BUILD_HOST_EXECUTABLE)
+
+#
+# Build the device-side library
+#
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(edify_src_files)
+
+LOCAL_CPPFLAGS := -Wno-unused-parameter
+LOCAL_CPPFLAGS += -Wno-deprecated-register
+LOCAL_MODULE := libedify
+LOCAL_CLANG := true
+
+include $(BUILD_STATIC_LIBRARY)
diff --git a/recovery/edify/README b/recovery/edify/README
new file mode 100644
index 0000000..810455c
--- /dev/null
+++ b/recovery/edify/README
@@ -0,0 +1,108 @@
+Update scripts (from donut onwards) are written in a new little
+scripting language ("edify") that is superficially somewhat similar to
+the old one ("amend").  This is a brief overview of the new language.
+
+- The entire script is a single expression.
+
+- All expressions are string-valued.
+
+- String literals appear in double quotes.  \n, \t, \", and \\ are
+  understood, as are hexadecimal escapes like \x4a.
+
+- String literals consisting of only letters, numbers, colons,
+  underscores, slashes, and periods don't need to be in double quotes.
+
+- The following words are reserved:
+
+       if    then    else   endif
+
+  They have special meaning when unquoted.  (In quotes, they are just
+  string literals.)
+
+- When used as a boolean, the empty string is "false" and all other
+  strings are "true".
+
+- All functions are actually macros (in the Lisp sense); the body of
+  the function can control which (if any) of the arguments are
+  evaluated.  This means that functions can act as control
+  structures.
+
+- Operators (like "&&" and "||") are just syntactic sugar for builtin
+  functions, so they can act as control structures as well.
+
+- ";" is a binary operator; evaluating it just means to first evaluate
+  the left side, then the right.  It can also appear after any
+  expression.
+
+- Comments start with "#" and run to the end of the line.
+
+
+
+Some examples:
+
+- There's no distinction between quoted and unquoted strings; the
+  quotes are only needed if you want characters like whitespace to
+  appear in the string.  The following expressions all evaluate to the
+  same string.
+
+     "a b"
+     a + " " + b
+     "a" + " " + "b"
+     "a\x20b"
+     a + "\x20b"
+     concat(a, " ", "b")
+     "concat"(a, " ", "b")
+
+  As shown in the last example, function names are just strings,
+  too.  They must be string *literals*, however.  This is not legal:
+
+     ("con" + "cat")(a, " ", b)         # syntax error!
+
+
+- The ifelse() builtin takes three arguments:  it evaluates exactly
+  one of the second and third, depending on whether the first one is
+  true.  There is also some syntactic sugar to make expressions that
+  look like if/else statements:
+
+     # these are all equivalent
+     ifelse(something(), "yes", "no")
+     if something() then yes else no endif
+     if something() then "yes" else "no" endif
+
+  The else part is optional.
+
+     if something() then "yes" endif    # if something() is false,
+                                        # evaluates to false
+
+     ifelse(condition(), "", abort())   # abort() only called if
+                                        # condition() is false
+
+  The last example is equivalent to:
+
+     assert(condition())
+
+
+- The && and || operators can be used similarly; they evaluate their
+  second argument only if it's needed to determine the truth of the
+  expression.  Their value is the value of the last-evaluated
+  argument:
+
+     file_exists("/data/system/bad") && delete("/data/system/bad")
+
+     file_exists("/data/system/missing") || create("/data/system/missing")
+
+     get_it() || "xxx"     # returns value of get_it() if that value is
+                           # true, otherwise returns "xxx"
+
+
+- The purpose of ";" is to simulate imperative statements, of course,
+  but the operator can be used anywhere.  Its value is the value of
+  its right side:
+
+     concat(a;b;c, d, e;f)     # evaluates to "cdf"
+
+  A more useful example might be something like:
+
+     ifelse(condition(),
+            (first_step(); second_step();),   # second ; is optional
+            alternative_procedure())
diff --git a/recovery/edify/expr.cpp b/recovery/edify/expr.cpp
new file mode 100644
index 0000000..c34342f
--- /dev/null
+++ b/recovery/edify/expr.cpp
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <string.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <unistd.h>
+
+#include "expr.h"
+
+// Functions should:
+//
+//    - return a malloc()'d string
+//    - if Evaluate() on any argument returns NULL, return NULL.
+
+int BooleanString(const char* s) {
+    return s[0] != '\0';
+}
+
+char* Evaluate(State* state, Expr* expr) {
+    Value* v = expr->fn(expr->name, state, expr->argc, expr->argv);
+    if (v == NULL) return NULL;
+    if (v->type != VAL_STRING) {
+        ErrorAbort(state, "expecting string, got value type %d", v->type);
+        FreeValue(v);
+        return NULL;
+    }
+    char* result = v->data;
+    free(v);
+    return result;
+}
+
+Value* EvaluateValue(State* state, Expr* expr) {
+    return expr->fn(expr->name, state, expr->argc, expr->argv);
+}
+
+Value* StringValue(char* str) {
+    if (str == NULL) return NULL;
+    Value* v = reinterpret_cast<Value*>(malloc(sizeof(Value)));
+    v->type = VAL_STRING;
+    v->size = strlen(str);
+    v->data = str;
+    return v;
+}
+
+void FreeValue(Value* v) {
+    if (v == NULL) return;
+    free(v->data);
+    free(v);
+}
+
+Value* ConcatFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc == 0) {
+        return StringValue(strdup(""));
+    }
+    char** strings = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
+    int i;
+    for (i = 0; i < argc; ++i) {
+        strings[i] = NULL;
+    }
+    char* result = NULL;
+    int length = 0;
+    for (i = 0; i < argc; ++i) {
+        strings[i] = Evaluate(state, argv[i]);
+        if (strings[i] == NULL) {
+            goto done;
+        }
+        length += strlen(strings[i]);
+    }
+
+    result = reinterpret_cast<char*>(malloc(length+1));
+    int p;
+    p = 0;
+    for (i = 0; i < argc; ++i) {
+        strcpy(result+p, strings[i]);
+        p += strlen(strings[i]);
+    }
+    result[p] = '\0';
+
+  done:
+    for (i = 0; i < argc; ++i) {
+        free(strings[i]);
+    }
+    free(strings);
+    return StringValue(result);
+}
+
+Value* IfElseFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2 && argc != 3) {
+        free(state->errmsg);
+        state->errmsg = strdup("ifelse expects 2 or 3 arguments");
+        return NULL;
+    }
+    char* cond = Evaluate(state, argv[0]);
+    if (cond == NULL) {
+        return NULL;
+    }
+
+    if (BooleanString(cond) == true) {
+        free(cond);
+        return EvaluateValue(state, argv[1]);
+    } else {
+        if (argc == 3) {
+            free(cond);
+            return EvaluateValue(state, argv[2]);
+        } else {
+            return StringValue(cond);
+        }
+    }
+}
+
+Value* AbortFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* msg = NULL;
+    if (argc > 0) {
+        msg = Evaluate(state, argv[0]);
+    }
+    free(state->errmsg);
+    if (msg) {
+        state->errmsg = msg;
+    } else {
+        state->errmsg = strdup("called abort()");
+    }
+    return NULL;
+}
+
+Value* AssertFn(const char* name, State* state, int argc, Expr* argv[]) {
+    int i;
+    for (i = 0; i < argc; ++i) {
+        char* v = Evaluate(state, argv[i]);
+        if (v == NULL) {
+            return NULL;
+        }
+        int b = BooleanString(v);
+        free(v);
+        if (!b) {
+            int prefix_len;
+            int len = argv[i]->end - argv[i]->start;
+            char* err_src = reinterpret_cast<char*>(malloc(len + 20));
+            strcpy(err_src, "assert failed: ");
+            prefix_len = strlen(err_src);
+            memcpy(err_src + prefix_len, state->script + argv[i]->start, len);
+            err_src[prefix_len + len] = '\0';
+            free(state->errmsg);
+            state->errmsg = err_src;
+            return NULL;
+        }
+    }
+    return StringValue(strdup(""));
+}
+
+Value* SleepFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* val = Evaluate(state, argv[0]);
+    if (val == NULL) {
+        return NULL;
+    }
+    int v = strtol(val, NULL, 10);
+    sleep(v);
+    return StringValue(val);
+}
+
+Value* StdoutFn(const char* name, State* state, int argc, Expr* argv[]) {
+    int i;
+    for (i = 0; i < argc; ++i) {
+        char* v = Evaluate(state, argv[i]);
+        if (v == NULL) {
+            return NULL;
+        }
+        fputs(v, stdout);
+        free(v);
+    }
+    return StringValue(strdup(""));
+}
+
+Value* LogicalAndFn(const char* name, State* state,
+                   int argc, Expr* argv[]) {
+    char* left = Evaluate(state, argv[0]);
+    if (left == NULL) return NULL;
+    if (BooleanString(left) == true) {
+        free(left);
+        return EvaluateValue(state, argv[1]);
+    } else {
+        return StringValue(left);
+    }
+}
+
+Value* LogicalOrFn(const char* name, State* state,
+                   int argc, Expr* argv[]) {
+    char* left = Evaluate(state, argv[0]);
+    if (left == NULL) return NULL;
+    if (BooleanString(left) == false) {
+        free(left);
+        return EvaluateValue(state, argv[1]);
+    } else {
+        return StringValue(left);
+    }
+}
+
+Value* LogicalNotFn(const char* name, State* state,
+                    int argc, Expr* argv[]) {
+    char* val = Evaluate(state, argv[0]);
+    if (val == NULL) return NULL;
+    bool bv = BooleanString(val);
+    free(val);
+    return StringValue(strdup(bv ? "" : "t"));
+}
+
+Value* SubstringFn(const char* name, State* state,
+                   int argc, Expr* argv[]) {
+    char* needle = Evaluate(state, argv[0]);
+    if (needle == NULL) return NULL;
+    char* haystack = Evaluate(state, argv[1]);
+    if (haystack == NULL) {
+        free(needle);
+        return NULL;
+    }
+
+    char* result = strdup(strstr(haystack, needle) ? "t" : "");
+    free(needle);
+    free(haystack);
+    return StringValue(result);
+}
+
+Value* EqualityFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* left = Evaluate(state, argv[0]);
+    if (left == NULL) return NULL;
+    char* right = Evaluate(state, argv[1]);
+    if (right == NULL) {
+        free(left);
+        return NULL;
+    }
+
+    char* result = strdup(strcmp(left, right) == 0 ? "t" : "");
+    free(left);
+    free(right);
+    return StringValue(result);
+}
+
+Value* InequalityFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* left = Evaluate(state, argv[0]);
+    if (left == NULL) return NULL;
+    char* right = Evaluate(state, argv[1]);
+    if (right == NULL) {
+        free(left);
+        return NULL;
+    }
+
+    char* result = strdup(strcmp(left, right) != 0 ? "t" : "");
+    free(left);
+    free(right);
+    return StringValue(result);
+}
+
+Value* SequenceFn(const char* name, State* state, int argc, Expr* argv[]) {
+    Value* left = EvaluateValue(state, argv[0]);
+    if (left == NULL) return NULL;
+    FreeValue(left);
+    return EvaluateValue(state, argv[1]);
+}
+
+Value* LessThanIntFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2) {
+        free(state->errmsg);
+        state->errmsg = strdup("less_than_int expects 2 arguments");
+        return NULL;
+    }
+
+    char* left;
+    char* right;
+    if (ReadArgs(state, argv, 2, &left, &right) < 0) return NULL;
+
+    bool result = false;
+    char* end;
+
+    // Parse up to at least long long or 64-bit integers.
+    int64_t l_int = static_cast<int64_t>(strtoll(left, &end, 10));
+    if (left[0] == '\0' || *end != '\0') {
+        goto done;
+    }
+
+    int64_t r_int;
+    r_int = static_cast<int64_t>(strtoll(right, &end, 10));
+    if (right[0] == '\0' || *end != '\0') {
+        goto done;
+    }
+
+    result = l_int < r_int;
+
+  done:
+    free(left);
+    free(right);
+    return StringValue(strdup(result ? "t" : ""));
+}
+
+Value* GreaterThanIntFn(const char* name, State* state,
+                        int argc, Expr* argv[]) {
+    if (argc != 2) {
+        free(state->errmsg);
+        state->errmsg = strdup("greater_than_int expects 2 arguments");
+        return NULL;
+    }
+
+    Expr* temp[2];
+    temp[0] = argv[1];
+    temp[1] = argv[0];
+
+    return LessThanIntFn(name, state, 2, temp);
+}
+
+Value* Literal(const char* name, State* state, int argc, Expr* argv[]) {
+    return StringValue(strdup(name));
+}
+
+Expr* Build(Function fn, YYLTYPE loc, int count, ...) {
+    va_list v;
+    va_start(v, count);
+    Expr* e = reinterpret_cast<Expr*>(malloc(sizeof(Expr)));
+    e->fn = fn;
+    e->name = "(operator)";
+    e->argc = count;
+    e->argv = reinterpret_cast<Expr**>(malloc(count * sizeof(Expr*)));
+    int i;
+    for (i = 0; i < count; ++i) {
+        e->argv[i] = va_arg(v, Expr*);
+    }
+    va_end(v);
+    e->start = loc.start;
+    e->end = loc.end;
+    return e;
+}
+
+// -----------------------------------------------------------------
+//   the function table
+// -----------------------------------------------------------------
+
+static int fn_entries = 0;
+static int fn_size = 0;
+NamedFunction* fn_table = NULL;
+
+void RegisterFunction(const char* name, Function fn) {
+    if (fn_entries >= fn_size) {
+        fn_size = fn_size*2 + 1;
+        fn_table = reinterpret_cast<NamedFunction*>(realloc(fn_table, fn_size * sizeof(NamedFunction)));
+    }
+    fn_table[fn_entries].name = name;
+    fn_table[fn_entries].fn = fn;
+    ++fn_entries;
+}
+
+static int fn_entry_compare(const void* a, const void* b) {
+    const char* na = ((const NamedFunction*)a)->name;
+    const char* nb = ((const NamedFunction*)b)->name;
+    return strcmp(na, nb);
+}
+
+void FinishRegistration() {
+    qsort(fn_table, fn_entries, sizeof(NamedFunction), fn_entry_compare);
+}
+
+Function FindFunction(const char* name) {
+    NamedFunction key;
+    key.name = name;
+    NamedFunction* nf = reinterpret_cast<NamedFunction*>(bsearch(&key, fn_table, fn_entries,
+            sizeof(NamedFunction), fn_entry_compare));
+    if (nf == NULL) {
+        return NULL;
+    }
+    return nf->fn;
+}
+
+void RegisterBuiltins() {
+    RegisterFunction("ifelse", IfElseFn);
+    RegisterFunction("abort", AbortFn);
+    RegisterFunction("assert", AssertFn);
+    RegisterFunction("concat", ConcatFn);
+    RegisterFunction("is_substring", SubstringFn);
+    RegisterFunction("stdout", StdoutFn);
+    RegisterFunction("sleep", SleepFn);
+
+    RegisterFunction("less_than_int", LessThanIntFn);
+    RegisterFunction("greater_than_int", GreaterThanIntFn);
+}
+
+
+// -----------------------------------------------------------------
+//   convenience methods for functions
+// -----------------------------------------------------------------
+
+// Evaluate the expressions in argv, giving 'count' char* (the ... is
+// zero or more char** to put them in).  If any expression evaluates
+// to NULL, free the rest and return -1.  Return 0 on success.
+int ReadArgs(State* state, Expr* argv[], int count, ...) {
+    char** args = reinterpret_cast<char**>(malloc(count * sizeof(char*)));
+    va_list v;
+    va_start(v, count);
+    int i;
+    for (i = 0; i < count; ++i) {
+        args[i] = Evaluate(state, argv[i]);
+        if (args[i] == NULL) {
+            va_end(v);
+            int j;
+            for (j = 0; j < i; ++j) {
+                free(args[j]);
+            }
+            free(args);
+            return -1;
+        }
+        *(va_arg(v, char**)) = args[i];
+    }
+    va_end(v);
+    free(args);
+    return 0;
+}
+
+// Evaluate the expressions in argv, giving 'count' Value* (the ... is
+// zero or more Value** to put them in).  If any expression evaluates
+// to NULL, free the rest and return -1.  Return 0 on success.
+int ReadValueArgs(State* state, Expr* argv[], int count, ...) {
+    Value** args = reinterpret_cast<Value**>(malloc(count * sizeof(Value*)));
+    va_list v;
+    va_start(v, count);
+    int i;
+    for (i = 0; i < count; ++i) {
+        args[i] = EvaluateValue(state, argv[i]);
+        if (args[i] == NULL) {
+            va_end(v);
+            int j;
+            for (j = 0; j < i; ++j) {
+                FreeValue(args[j]);
+            }
+            free(args);
+            return -1;
+        }
+        *(va_arg(v, Value**)) = args[i];
+    }
+    va_end(v);
+    free(args);
+    return 0;
+}
+
+// Evaluate the expressions in argv, returning an array of char*
+// results.  If any evaluate to NULL, free the rest and return NULL.
+// The caller is responsible for freeing the returned array and the
+// strings it contains.
+char** ReadVarArgs(State* state, int argc, Expr* argv[]) {
+    char** args = (char**)malloc(argc * sizeof(char*));
+    int i = 0;
+    for (i = 0; i < argc; ++i) {
+        args[i] = Evaluate(state, argv[i]);
+        if (args[i] == NULL) {
+            int j;
+            for (j = 0; j < i; ++j) {
+                free(args[j]);
+            }
+            free(args);
+            return NULL;
+        }
+    }
+    return args;
+}
+
+// Evaluate the expressions in argv, returning an array of Value*
+// results.  If any evaluate to NULL, free the rest and return NULL.
+// The caller is responsible for freeing the returned array and the
+// Values it contains.
+Value** ReadValueVarArgs(State* state, int argc, Expr* argv[]) {
+    Value** args = (Value**)malloc(argc * sizeof(Value*));
+    int i = 0;
+    for (i = 0; i < argc; ++i) {
+        args[i] = EvaluateValue(state, argv[i]);
+        if (args[i] == NULL) {
+            int j;
+            for (j = 0; j < i; ++j) {
+                FreeValue(args[j]);
+            }
+            free(args);
+            return NULL;
+        }
+    }
+    return args;
+}
+
+// Use printf-style arguments to compose an error message to put into
+// *state.  Returns NULL.
+Value* ErrorAbort(State* state, const char* format, ...) {
+    char* buffer = reinterpret_cast<char*>(malloc(4096));
+    va_list v;
+    va_start(v, format);
+    vsnprintf(buffer, 4096, format, v);
+    va_end(v);
+    free(state->errmsg);
+    state->errmsg = buffer;
+    return NULL;
+}
diff --git a/recovery/edify/expr.h b/recovery/edify/expr.h
new file mode 100644
index 0000000..36f8e96
--- /dev/null
+++ b/recovery/edify/expr.h
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#ifndef _EXPRESSION_H
+#define _EXPRESSION_H
+
+#include <unistd.h>
+
+#include "yydefs.h"
+
+#define MAX_STRING_LEN 1024
+
+typedef struct Expr Expr;
+
+typedef struct {
+    // Optional pointer to app-specific data; the core of edify never
+    // uses this value.
+    void* cookie;
+
+    // The source of the original script.  Must be NULL-terminated,
+    // and in writable memory (Evaluate may make temporary changes to
+    // it but will restore it when done).
+    char* script;
+
+    // The error message (if any) returned if the evaluation aborts.
+    // Should be NULL initially, will be either NULL or a malloc'd
+    // pointer after Evaluate() returns.
+    char* errmsg;
+} State;
+
+#define VAL_STRING  1  // data will be NULL-terminated; size doesn't count null
+#define VAL_BLOB    2
+
+typedef struct {
+    int type;
+    ssize_t size;
+    char* data;
+} Value;
+
+typedef Value* (*Function)(const char* name, State* state,
+                           int argc, Expr* argv[]);
+
+struct Expr {
+    Function fn;
+    const char* name;
+    int argc;
+    Expr** argv;
+    int start, end;
+};
+
+// Take one of the Expr*s passed to the function as an argument,
+// evaluate it, return the resulting Value.  The caller takes
+// ownership of the returned Value.
+Value* EvaluateValue(State* state, Expr* expr);
+
+// Take one of the Expr*s passed to the function as an argument,
+// evaluate it, assert that it is a string, and return the resulting
+// char*.  The caller takes ownership of the returned char*.  This is
+// a convenience function for older functions that want to deal only
+// with strings.
+char* Evaluate(State* state, Expr* expr);
+
+// Glue to make an Expr out of a literal.
+Value* Literal(const char* name, State* state, int argc, Expr* argv[]);
+
+// Functions corresponding to various syntactic sugar operators.
+// ("concat" is also available as a builtin function, to concatenate
+// more than two strings.)
+Value* ConcatFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* LogicalAndFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* LogicalOrFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* LogicalNotFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* SubstringFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* EqualityFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* InequalityFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* SequenceFn(const char* name, State* state, int argc, Expr* argv[]);
+
+// Convenience function for building expressions with a fixed number
+// of arguments.
+Expr* Build(Function fn, YYLTYPE loc, int count, ...);
+
+// Global builtins, registered by RegisterBuiltins().
+Value* IfElseFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* AssertFn(const char* name, State* state, int argc, Expr* argv[]);
+Value* AbortFn(const char* name, State* state, int argc, Expr* argv[]);
+
+
+// For setting and getting the global error string (when returning
+// NULL from a function).
+void SetError(const char* message);  // makes a copy
+const char* GetError();              // retains ownership
+void ClearError();
+
+
+typedef struct {
+  const char* name;
+  Function fn;
+} NamedFunction;
+
+// Register a new function.  The same Function may be registered under
+// multiple names, but a given name should only be used once.
+void RegisterFunction(const char* name, Function fn);
+
+// Register all the builtins.
+void RegisterBuiltins();
+
+// Call this after all calls to RegisterFunction() but before parsing
+// any scripts to finish building the function table.
+void FinishRegistration();
+
+// Find the Function for a given name; return NULL if no such function
+// exists.
+Function FindFunction(const char* name);
+
+
+// --- convenience functions for use in functions ---
+
+// Evaluate the expressions in argv, giving 'count' char* (the ... is
+// zero or more char** to put them in).  If any expression evaluates
+// to NULL, free the rest and return -1.  Return 0 on success.
+int ReadArgs(State* state, Expr* argv[], int count, ...);
+
+// Evaluate the expressions in argv, giving 'count' Value* (the ... is
+// zero or more Value** to put them in).  If any expression evaluates
+// to NULL, free the rest and return -1.  Return 0 on success.
+int ReadValueArgs(State* state, Expr* argv[], int count, ...);
+
+// Evaluate the expressions in argv, returning an array of char*
+// results.  If any evaluate to NULL, free the rest and return NULL.
+// The caller is responsible for freeing the returned array and the
+// strings it contains.
+char** ReadVarArgs(State* state, int argc, Expr* argv[]);
+
+// Evaluate the expressions in argv, returning an array of Value*
+// results.  If any evaluate to NULL, free the rest and return NULL.
+// The caller is responsible for freeing the returned array and the
+// Values it contains.
+Value** ReadValueVarArgs(State* state, int argc, Expr* argv[]);
+
+// Use printf-style arguments to compose an error message to put into
+// *state.  Returns NULL.
+Value* ErrorAbort(State* state, const char* format, ...) __attribute__((format(printf, 2, 3)));
+
+// Wrap a string into a Value, taking ownership of the string.
+Value* StringValue(char* str);
+
+// Free a Value object.
+void FreeValue(Value* v);
+
+int parse_string(const char* str, Expr** root, int* error_count);
+
+#endif  // _EXPRESSION_H
diff --git a/recovery/edify/lexer.ll b/recovery/edify/lexer.ll
new file mode 100644
index 0000000..b764d16
--- /dev/null
+++ b/recovery/edify/lexer.ll
@@ -0,0 +1,110 @@
+%{
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <string.h>
+#include <string>
+
+#include "expr.h"
+#include "yydefs.h"
+#include "parser.h"
+
+int gLine = 1;
+int gColumn = 1;
+int gPos = 0;
+
+std::string string_buffer;
+
+#define ADVANCE do {yylloc.start=gPos; yylloc.end=gPos+yyleng; \
+                    gColumn+=yyleng; gPos+=yyleng;} while(0)
+
+%}
+
+%x STR
+
+%option noyywrap
+
+%%
+
+
+\" {
+    BEGIN(STR);
+    string_buffer.clear();
+    yylloc.start = gPos;
+    ++gColumn;
+    ++gPos;
+}
+
+<STR>{
+  \" {
+      ++gColumn;
+      ++gPos;
+      BEGIN(INITIAL);
+      yylval.str = strdup(string_buffer.c_str());
+      yylloc.end = gPos;
+      return STRING;
+  }
+
+  \\n   { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\n'); }
+  \\t   { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\t'); }
+  \\\"  { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\"'); }
+  \\\\  { gColumn += yyleng; gPos += yyleng; string_buffer.push_back('\\'); }
+
+  \\x[0-9a-fA-F]{2} {
+      gColumn += yyleng;
+      gPos += yyleng;
+      int val;
+      sscanf(yytext+2, "%x", &val);
+      string_buffer.push_back(static_cast<char>(val));
+  }
+
+  \n {
+      ++gLine;
+      ++gPos;
+      gColumn = 1;
+      string_buffer.push_back(yytext[0]);
+  }
+
+  . {
+      ++gColumn;
+      ++gPos;
+      string_buffer.push_back(yytext[0]);
+  }
+}
+
+if                ADVANCE; return IF;
+then              ADVANCE; return THEN;
+else              ADVANCE; return ELSE;
+endif             ADVANCE; return ENDIF;
+
+[a-zA-Z0-9_:/.]+ {
+  ADVANCE;
+  yylval.str = strdup(yytext);
+  return STRING;
+}
+
+\&\&              ADVANCE; return AND;
+\|\|              ADVANCE; return OR;
+==                ADVANCE; return EQ;
+!=                ADVANCE; return NE;
+
+[+(),!;]          ADVANCE; return yytext[0];
+
+[ \t]+            ADVANCE;
+
+(#.*)?\n          gPos += yyleng; ++gLine; gColumn = 1;
+
+.                 return BAD;
diff --git a/recovery/edify/main.cpp b/recovery/edify/main.cpp
new file mode 100644
index 0000000..6007a3d
--- /dev/null
+++ b/recovery/edify/main.cpp
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <string>
+
+#include "expr.h"
+#include "parser.h"
+
+extern int yyparse(Expr** root, int* error_count);
+
+int expect(const char* expr_str, const char* expected, int* errors) {
+    Expr* e;
+    char* result;
+
+    printf(".");
+
+    int error_count = parse_string(expr_str, &e, &error_count);
+    if (error_count > 0) {
+        printf("error parsing \"%s\" (%d errors)\n",
+               expr_str, error_count);
+        ++*errors;
+        return 0;
+    }
+
+    State state;
+    state.cookie = NULL;
+    state.script = strdup(expr_str);
+    state.errmsg = NULL;
+
+    result = Evaluate(&state, e);
+    free(state.errmsg);
+    free(state.script);
+    if (result == NULL && expected != NULL) {
+        printf("error evaluating \"%s\"\n", expr_str);
+        ++*errors;
+        return 0;
+    }
+
+    if (result == NULL && expected == NULL) {
+        return 1;
+    }
+
+    if (strcmp(result, expected) != 0) {
+        printf("evaluating \"%s\": expected \"%s\", got \"%s\"\n",
+               expr_str, expected, result);
+        ++*errors;
+        free(result);
+        return 0;
+    }
+
+    free(result);
+    return 1;
+}
+
+int test() {
+    int errors = 0;
+
+    expect("a", "a", &errors);
+    expect("\"a\"", "a", &errors);
+    expect("\"\\x61\"", "a", &errors);
+    expect("# this is a comment\n"
+           "  a\n"
+           "   \n",
+           "a", &errors);
+
+
+    // sequence operator
+    expect("a; b; c", "c", &errors);
+
+    // string concat operator
+    expect("a + b", "ab", &errors);
+    expect("a + \n \"b\"", "ab", &errors);
+    expect("a + b +\nc\n", "abc", &errors);
+
+    // string concat function
+    expect("concat(a, b)", "ab", &errors);
+    expect("concat(a,\n \"b\")", "ab", &errors);
+    expect("concat(a + b,\nc,\"d\")", "abcd", &errors);
+    expect("\"concat\"(a + b,\nc,\"d\")", "abcd", &errors);
+
+    // logical and
+    expect("a && b", "b", &errors);
+    expect("a && \"\"", "", &errors);
+    expect("\"\" && b", "", &errors);
+    expect("\"\" && \"\"", "", &errors);
+    expect("\"\" && abort()", "", &errors);   // test short-circuiting
+    expect("t && abort()", NULL, &errors);
+
+    // logical or
+    expect("a || b", "a", &errors);
+    expect("a || \"\"", "a", &errors);
+    expect("\"\" || b", "b", &errors);
+    expect("\"\" || \"\"", "", &errors);
+    expect("a || abort()", "a", &errors);     // test short-circuiting
+    expect("\"\" || abort()", NULL, &errors);
+
+    // logical not
+    expect("!a", "", &errors);
+    expect("! \"\"", "t", &errors);
+    expect("!!a", "t", &errors);
+
+    // precedence
+    expect("\"\" == \"\" && b", "b", &errors);
+    expect("a + b == ab", "t", &errors);
+    expect("ab == a + b", "t", &errors);
+    expect("a + (b == ab)", "a", &errors);
+    expect("(ab == a) + b", "b", &errors);
+
+    // substring function
+    expect("is_substring(cad, abracadabra)", "t", &errors);
+    expect("is_substring(abrac, abracadabra)", "t", &errors);
+    expect("is_substring(dabra, abracadabra)", "t", &errors);
+    expect("is_substring(cad, abracxadabra)", "", &errors);
+    expect("is_substring(abrac, axbracadabra)", "", &errors);
+    expect("is_substring(dabra, abracadabrxa)", "", &errors);
+
+    // ifelse function
+    expect("ifelse(t, yes, no)", "yes", &errors);
+    expect("ifelse(!t, yes, no)", "no", &errors);
+    expect("ifelse(t, yes, abort())", "yes", &errors);
+    expect("ifelse(!t, abort(), no)", "no", &errors);
+
+    // if "statements"
+    expect("if t then yes else no endif", "yes", &errors);
+    expect("if \"\" then yes else no endif", "no", &errors);
+    expect("if \"\" then yes endif", "", &errors);
+    expect("if \"\"; t then yes endif", "yes", &errors);
+
+    // numeric comparisons
+    expect("less_than_int(3, 14)", "t", &errors);
+    expect("less_than_int(14, 3)", "", &errors);
+    expect("less_than_int(x, 3)", "", &errors);
+    expect("less_than_int(3, x)", "", &errors);
+    expect("greater_than_int(3, 14)", "", &errors);
+    expect("greater_than_int(14, 3)", "t", &errors);
+    expect("greater_than_int(x, 3)", "", &errors);
+    expect("greater_than_int(3, x)", "", &errors);
+
+    // big string
+    expect(std::string(8192, 's').c_str(), std::string(8192, 's').c_str(), &errors);
+
+    printf("\n");
+
+    return errors;
+}
+
+void ExprDump(int depth, Expr* n, char* script) {
+    printf("%*s", depth*2, "");
+    char temp = script[n->end];
+    script[n->end] = '\0';
+    printf("%s %p (%d-%d) \"%s\"\n",
+           n->name == NULL ? "(NULL)" : n->name, n->fn, n->start, n->end,
+           script+n->start);
+    script[n->end] = temp;
+    int i;
+    for (i = 0; i < n->argc; ++i) {
+        ExprDump(depth+1, n->argv[i], script);
+    }
+}
+
+int main(int argc, char** argv) {
+    RegisterBuiltins();
+    FinishRegistration();
+
+    if (argc == 1) {
+        return test() != 0;
+    }
+
+    FILE* f = fopen(argv[1], "r");
+    if (f == NULL) {
+        printf("%s: %s: No such file or directory\n", argv[0], argv[1]);
+        return 1;
+    }
+    char buffer[8192];
+    int size = fread(buffer, 1, 8191, f);
+    fclose(f);
+    buffer[size] = '\0';
+
+    Expr* root;
+    int error_count = 0;
+    int error = parse_string(buffer, &root, &error_count);
+    printf("parse returned %d; %d errors encountered\n", error, error_count);
+    if (error == 0 || error_count > 0) {
+
+        ExprDump(0, root, buffer);
+
+        State state;
+        state.cookie = NULL;
+        state.script = buffer;
+        state.errmsg = NULL;
+
+        char* result = Evaluate(&state, root);
+        if (result == NULL) {
+            printf("result was NULL, message is: %s\n",
+                   (state.errmsg == NULL ? "(NULL)" : state.errmsg));
+            free(state.errmsg);
+        } else {
+            printf("result is [%s]\n", result);
+        }
+    }
+    return 0;
+}
diff --git a/recovery/edify/parser.yy b/recovery/edify/parser.yy
new file mode 100644
index 0000000..098a637
--- /dev/null
+++ b/recovery/edify/parser.yy
@@ -0,0 +1,139 @@
+%{
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "expr.h"
+#include "yydefs.h"
+#include "parser.h"
+
+extern int gLine;
+extern int gColumn;
+
+void yyerror(Expr** root, int* error_count, const char* s);
+int yyparse(Expr** root, int* error_count);
+
+struct yy_buffer_state;
+void yy_switch_to_buffer(struct yy_buffer_state* new_buffer);
+struct yy_buffer_state* yy_scan_string(const char* yystr);
+
+%}
+
+%locations
+
+%union {
+    char* str;
+    Expr* expr;
+    struct {
+        int argc;
+        Expr** argv;
+    } args;
+}
+
+%token AND OR SUBSTR SUPERSTR EQ NE IF THEN ELSE ENDIF
+%token <str> STRING BAD
+%type <expr> expr
+%type <args> arglist
+
+%parse-param {Expr** root}
+%parse-param {int* error_count}
+%error-verbose
+
+/* declarations in increasing order of precedence */
+%left ';'
+%left ','
+%left OR
+%left AND
+%left EQ NE
+%left '+'
+%right '!'
+
+%%
+
+input:  expr           { *root = $1; }
+;
+
+expr:  STRING {
+    $$ = reinterpret_cast<Expr*>(malloc(sizeof(Expr)));
+    $$->fn = Literal;
+    $$->name = $1;
+    $$->argc = 0;
+    $$->argv = NULL;
+    $$->start = @$.start;
+    $$->end = @$.end;
+}
+|  '(' expr ')'                      { $$ = $2; $$->start=@$.start; $$->end=@$.end; }
+|  expr ';'                          { $$ = $1; $$->start=@1.start; $$->end=@1.end; }
+|  expr ';' expr                     { $$ = Build(SequenceFn, @$, 2, $1, $3); }
+|  error ';' expr                    { $$ = $3; $$->start=@$.start; $$->end=@$.end; }
+|  expr '+' expr                     { $$ = Build(ConcatFn, @$, 2, $1, $3); }
+|  expr EQ expr                      { $$ = Build(EqualityFn, @$, 2, $1, $3); }
+|  expr NE expr                      { $$ = Build(InequalityFn, @$, 2, $1, $3); }
+|  expr AND expr                     { $$ = Build(LogicalAndFn, @$, 2, $1, $3); }
+|  expr OR expr                      { $$ = Build(LogicalOrFn, @$, 2, $1, $3); }
+|  '!' expr                          { $$ = Build(LogicalNotFn, @$, 1, $2); }
+|  IF expr THEN expr ENDIF           { $$ = Build(IfElseFn, @$, 2, $2, $4); }
+|  IF expr THEN expr ELSE expr ENDIF { $$ = Build(IfElseFn, @$, 3, $2, $4, $6); }
+| STRING '(' arglist ')' {
+    $$ = reinterpret_cast<Expr*>(malloc(sizeof(Expr)));
+    $$->fn = FindFunction($1);
+    if ($$->fn == NULL) {
+        char buffer[256];
+        snprintf(buffer, sizeof(buffer), "unknown function \"%s\"", $1);
+        yyerror(root, error_count, buffer);
+        YYERROR;
+    }
+    $$->name = $1;
+    $$->argc = $3.argc;
+    $$->argv = $3.argv;
+    $$->start = @$.start;
+    $$->end = @$.end;
+}
+;
+
+arglist:    /* empty */ {
+    $$.argc = 0;
+    $$.argv = NULL;
+}
+| expr {
+    $$.argc = 1;
+    $$.argv = reinterpret_cast<Expr**>(malloc(sizeof(Expr*)));
+    $$.argv[0] = $1;
+}
+| arglist ',' expr {
+    $$.argc = $1.argc + 1;
+    $$.argv = reinterpret_cast<Expr**>(realloc($$.argv, $$.argc * sizeof(Expr*)));
+    $$.argv[$$.argc-1] = $3;
+}
+;
+
+%%
+
+void yyerror(Expr** root, int* error_count, const char* s) {
+  if (strlen(s) == 0) {
+    s = "syntax error";
+  }
+  printf("line %d col %d: %s\n", gLine, gColumn, s);
+  ++*error_count;
+}
+
+int parse_string(const char* str, Expr** root, int* error_count) {
+    yy_switch_to_buffer(yy_scan_string(str));
+    return yyparse(root, error_count);
+}
diff --git a/recovery/edify/yydefs.h b/recovery/edify/yydefs.h
new file mode 100644
index 0000000..aca398f
--- /dev/null
+++ b/recovery/edify/yydefs.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#ifndef _YYDEFS_H_
+#define _YYDEFS_H_
+
+#define YYLTYPE YYLTYPE
+typedef struct {
+    int start, end;
+} YYLTYPE;
+
+#define YYLLOC_DEFAULT(Current, Rhs, N) \
+    do { \
+        if (N) { \
+            (Current).start = YYRHSLOC(Rhs, 1).start; \
+            (Current).end = YYRHSLOC(Rhs, N).end; \
+        } else { \
+            (Current).start = YYRHSLOC(Rhs, 0).start; \
+            (Current).end = YYRHSLOC(Rhs, 0).end; \
+        } \
+    } while (0)
+
+int yylex();
+
+#endif
diff --git a/recovery/etc/init.rc b/recovery/etc/init.rc
new file mode 100644
index 0000000..29b088d
--- /dev/null
+++ b/recovery/etc/init.rc
@@ -0,0 +1,107 @@
+import /init.recovery.${ro.hardware}.rc
+
+on early-init
+    # Set the security context of /postinstall if present.
+    restorecon /postinstall
+
+    start ueventd
+    start healthd
+
+on init
+    export ANDROID_ROOT /system
+    export ANDROID_DATA /data
+    export EXTERNAL_STORAGE /sdcard
+
+    symlink /system/etc /etc
+
+    mount cgroup none /acct cpuacct
+    mkdir /acct/uid
+
+    mkdir /sdcard
+    mkdir /system
+    mkdir /data
+    mkdir /cache
+    mkdir /sideload
+    mount tmpfs tmpfs /tmp
+
+    chown root shell /tmp
+    chmod 0775 /tmp
+
+    write /proc/sys/kernel/panic_on_oops 1
+    write /proc/sys/vm/max_map_count 1000000
+
+on fs
+    mkdir /dev/usb-ffs 0770 shell shell
+    mkdir /dev/usb-ffs/adb 0770 shell shell
+    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
+
+    write /sys/class/android_usb/android0/enable 0
+    write /sys/class/android_usb/android0/idVendor 18D1
+    write /sys/class/android_usb/android0/idProduct D001
+    write /sys/class/android_usb/android0/f_ffs/aliases adb
+    write /sys/class/android_usb/android0/functions adb
+    write /sys/class/android_usb/android0/iManufacturer ${ro.product.manufacturer}
+    write /sys/class/android_usb/android0/iProduct ${ro.product.model}
+    write /sys/class/android_usb/android0/iSerial ${ro.serialno}
+
+on boot
+    ifup lo
+    hostname localhost
+    domainname localdomain
+
+    class_start default
+
+# Load properties from /system/ + /factory after fs mount.
+on load_system_props_action
+    load_system_props
+
+on firmware_mounts_complete
+   rm /dev/.booting
+
+# Mount filesystems and start core system services.
+on late-init
+    trigger early-fs
+    trigger fs
+    trigger post-fs
+    trigger post-fs-data
+
+    # Load properties from /system/ + /factory after fs mount. Place
+    # this in another action so that the load will be scheduled after the prior
+    # issued fs triggers have completed.
+    trigger load_system_props_action
+
+    # Remove a file to wake up anything waiting for firmware
+    trigger firmware_mounts_complete
+
+    trigger early-boot
+    trigger boot
+
+on property:sys.powerctl=*
+   powerctl ${sys.powerctl}
+
+service ueventd /sbin/ueventd
+    critical
+    seclabel u:r:ueventd:s0
+
+service healthd /sbin/healthd -r
+    critical
+    seclabel u:r:healthd:s0
+
+service recovery /sbin/recovery
+    seclabel u:r:recovery:s0
+
+service adbd /sbin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery
+    disabled
+    socket adbd stream 660 system system
+    seclabel u:r:adbd:s0
+
+# Always start adbd on userdebug and eng builds
+on property:ro.debuggable=1
+    write /sys/class/android_usb/android0/enable 1
+    start adbd
+
+# Restart adbd so it can run as root
+on property:service.adb.root=1
+    write /sys/class/android_usb/android0/enable 0
+    restart adbd
+    write /sys/class/android_usb/android0/enable 1
diff --git a/recovery/fonts/12x22.png b/recovery/fonts/12x22.png
new file mode 100644
index 0000000..ae826be
--- /dev/null
+++ b/recovery/fonts/12x22.png
Binary files differ
diff --git a/recovery/fonts/18x32.png b/recovery/fonts/18x32.png
new file mode 100644
index 0000000..d95408a
--- /dev/null
+++ b/recovery/fonts/18x32.png
Binary files differ
diff --git a/recovery/fonts/OFL.txt b/recovery/fonts/OFL.txt
new file mode 100644
index 0000000..b14edde
--- /dev/null
+++ b/recovery/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2011, Raph Levien (firstname.lastname@gmail.com), Copyright (c) 2012, Cyreal (cyreal.org)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/recovery/fonts/README b/recovery/fonts/README
new file mode 100644
index 0000000..d0748d2
--- /dev/null
+++ b/recovery/fonts/README
@@ -0,0 +1,6 @@
+The images in this directory were generated using the font
+Inconsolata, which is released under the OFL license and was obtained
+from:
+
+  https://code.google.com/p/googlefontdirectory/source/browse/ofl/inconsolata/
+
diff --git a/recovery/fuse_sdcard_provider.cpp b/recovery/fuse_sdcard_provider.cpp
new file mode 100644
index 0000000..df96312
--- /dev/null
+++ b/recovery/fuse_sdcard_provider.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "fuse_sideload.h"
+
+struct file_data {
+    int fd;  // the underlying sdcard file
+
+    uint64_t file_size;
+    uint32_t block_size;
+};
+
+static int read_block_file(void* cookie, uint32_t block, uint8_t* buffer, uint32_t fetch_size) {
+    file_data* fd = reinterpret_cast<file_data*>(cookie);
+
+    off64_t offset = ((off64_t) block) * fd->block_size;
+    if (TEMP_FAILURE_RETRY(lseek64(fd->fd, offset, SEEK_SET)) == -1) {
+        fprintf(stderr, "seek on sdcard failed: %s\n", strerror(errno));
+        return -EIO;
+    }
+
+    while (fetch_size > 0) {
+        ssize_t r = TEMP_FAILURE_RETRY(read(fd->fd, buffer, fetch_size));
+        if (r == -1) {
+            fprintf(stderr, "read on sdcard failed: %s\n", strerror(errno));
+            return -EIO;
+        }
+        fetch_size -= r;
+        buffer += r;
+    }
+
+    return 0;
+}
+
+static void close_file(void* cookie) {
+    file_data* fd = reinterpret_cast<file_data*>(cookie);
+    close(fd->fd);
+}
+
+bool start_sdcard_fuse(const char* path) {
+    struct stat sb;
+    if (stat(path, &sb) == -1) {
+        fprintf(stderr, "failed to stat %s: %s\n", path, strerror(errno));
+        return false;
+    }
+
+    file_data fd;
+    fd.fd = open(path, O_RDONLY);
+    if (fd.fd == -1) {
+        fprintf(stderr, "failed to open %s: %s\n", path, strerror(errno));
+        return false;
+    }
+    fd.file_size = sb.st_size;
+    fd.block_size = 65536;
+
+    provider_vtab vtab;
+    vtab.read_block = read_block_file;
+    vtab.close = close_file;
+
+    // The installation process expects to find the sdcard unmounted.
+    // Unmount it with MNT_DETACH so that our open file continues to
+    // work but new references see it as unmounted.
+    umount2("/sdcard", MNT_DETACH);
+
+    return run_fuse_sideload(&vtab, &fd, fd.file_size, fd.block_size) == 0;
+}
diff --git a/recovery/fuse_sdcard_provider.h b/recovery/fuse_sdcard_provider.h
new file mode 100644
index 0000000..bdc60f2
--- /dev/null
+++ b/recovery/fuse_sdcard_provider.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef __FUSE_SDCARD_PROVIDER_H
+#define __FUSE_SDCARD_PROVIDER_H
+
+bool start_sdcard_fuse(const char* path);
+
+#endif
diff --git a/recovery/fuse_sideload.cpp b/recovery/fuse_sideload.cpp
new file mode 100644
index 0000000..1725e88
--- /dev/null
+++ b/recovery/fuse_sideload.cpp
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+// This module creates a special filesystem containing two files.
+//
+// "/sideload/package.zip" appears to be a normal file, but reading
+// from it causes data to be fetched from the adb host.  We can use
+// this to sideload packages over an adb connection without having to
+// store the entire package in RAM on the device.
+//
+// Because we may not trust the adb host, this filesystem maintains
+// the following invariant: each read of a given position returns the
+// same data as the first read at that position.  That is, once a
+// section of the file is read, future reads of that section return
+// the same data.  (Otherwise, a malicious adb host process could
+// return one set of bits when the package is read for signature
+// verification, and then different bits for when the package is
+// accessed by the installer.)  If the adb host returns something
+// different than it did on the first read, the reader of the file
+// will see their read fail with EINVAL.
+//
+// The other file, "/sideload/exit", is used to control the subprocess
+// that creates this filesystem.  Calling stat() on the exit file
+// causes the filesystem to be unmounted and the adb process on the
+// device shut down.
+//
+// Note that only the minimal set of file operations needed for these
+// two files is implemented.  In particular, you can't opendir() or
+// readdir() on the "/sideload" directory; ls on it won't work.
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/fuse.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/mount.h>
+#include <sys/param.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <openssl/sha.h>
+
+#include "fuse_sideload.h"
+
+#define PACKAGE_FILE_ID   (FUSE_ROOT_ID+1)
+#define EXIT_FLAG_ID      (FUSE_ROOT_ID+2)
+
+#define NO_STATUS         1
+#define NO_STATUS_EXIT    2
+
+struct fuse_data {
+    int ffd;   // file descriptor for the fuse socket
+
+    struct provider_vtab* vtab;
+    void* cookie;
+
+    uint64_t file_size;     // bytes
+
+    uint32_t block_size;    // block size that the adb host is using to send the file to us
+    uint32_t file_blocks;   // file size in block_size blocks
+
+    uid_t uid;
+    gid_t gid;
+
+    uint32_t curr_block;    // cache the block most recently read from the host
+    uint8_t* block_data;
+
+    uint8_t* extra_block;   // another block of storage for reads that
+                            // span two blocks
+
+    uint8_t* hashes;        // SHA-256 hash of each block (all zeros
+                            // if block hasn't been read yet)
+};
+
+static void fuse_reply(struct fuse_data* fd, __u64 unique, const void *data, size_t len)
+{
+    struct fuse_out_header hdr;
+    struct iovec vec[2];
+    int res;
+
+    hdr.len = len + sizeof(hdr);
+    hdr.error = 0;
+    hdr.unique = unique;
+
+    vec[0].iov_base = &hdr;
+    vec[0].iov_len = sizeof(hdr);
+    vec[1].iov_base = /* const_cast */(void*)(data);
+    vec[1].iov_len = len;
+
+    res = writev(fd->ffd, vec, 2);
+    if (res < 0) {
+        printf("*** REPLY FAILED *** %s\n", strerror(errno));
+    }
+}
+
+static int handle_init(void* data, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    const struct fuse_init_in* req = reinterpret_cast<const struct fuse_init_in*>(data);
+    struct fuse_init_out out;
+    size_t fuse_struct_size;
+
+
+    /* Kernel 2.6.16 is the first stable kernel with struct fuse_init_out
+     * defined (fuse version 7.6). The structure is the same from 7.6 through
+     * 7.22. Beginning with 7.23, the structure increased in size and added
+     * new parameters.
+     */
+    if (req->major != FUSE_KERNEL_VERSION || req->minor < 6) {
+        printf("Fuse kernel version mismatch: Kernel version %d.%d, Expected at least %d.6",
+               req->major, req->minor, FUSE_KERNEL_VERSION);
+        return -1;
+    }
+
+    out.minor = MIN(req->minor, FUSE_KERNEL_MINOR_VERSION);
+    fuse_struct_size = sizeof(out);
+#if defined(FUSE_COMPAT_22_INIT_OUT_SIZE)
+    /* FUSE_KERNEL_VERSION >= 23. */
+
+    /* If the kernel only works on minor revs older than or equal to 22,
+     * then use the older structure size since this code only uses the 7.22
+     * version of the structure. */
+    if (req->minor <= 22) {
+        fuse_struct_size = FUSE_COMPAT_22_INIT_OUT_SIZE;
+    }
+#endif
+
+    out.major = FUSE_KERNEL_VERSION;
+    out.max_readahead = req->max_readahead;
+    out.flags = 0;
+    out.max_background = 32;
+    out.congestion_threshold = 32;
+    out.max_write = 4096;
+    fuse_reply(fd, hdr->unique, &out, fuse_struct_size);
+
+    return NO_STATUS;
+}
+
+static void fill_attr(struct fuse_attr* attr, struct fuse_data* fd,
+                      uint64_t nodeid, uint64_t size, uint32_t mode) {
+    memset(attr, 0, sizeof(*attr));
+    attr->nlink = 1;
+    attr->uid = fd->uid;
+    attr->gid = fd->gid;
+    attr->blksize = 4096;
+
+    attr->ino = nodeid;
+    attr->size = size;
+    attr->blocks = (size == 0) ? 0 : (((size-1) / attr->blksize) + 1);
+    attr->mode = mode;
+}
+
+static int handle_getattr(void* /* data */, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    struct fuse_attr_out out;
+    memset(&out, 0, sizeof(out));
+    out.attr_valid = 10;
+
+    if (hdr->nodeid == FUSE_ROOT_ID) {
+        fill_attr(&(out.attr), fd, hdr->nodeid, 4096, S_IFDIR | 0555);
+    } else if (hdr->nodeid == PACKAGE_FILE_ID) {
+        fill_attr(&(out.attr), fd, PACKAGE_FILE_ID, fd->file_size, S_IFREG | 0444);
+    } else if (hdr->nodeid == EXIT_FLAG_ID) {
+        fill_attr(&(out.attr), fd, EXIT_FLAG_ID, 0, S_IFREG | 0);
+    } else {
+        return -ENOENT;
+    }
+
+    fuse_reply(fd, hdr->unique, &out, sizeof(out));
+    return (hdr->nodeid == EXIT_FLAG_ID) ? NO_STATUS_EXIT : NO_STATUS;
+}
+
+static int handle_lookup(void* data, struct fuse_data* fd,
+                         const struct fuse_in_header* hdr) {
+    struct fuse_entry_out out;
+    memset(&out, 0, sizeof(out));
+    out.entry_valid = 10;
+    out.attr_valid = 10;
+
+    if (strncmp(FUSE_SIDELOAD_HOST_FILENAME, reinterpret_cast<const char*>(data),
+                sizeof(FUSE_SIDELOAD_HOST_FILENAME)) == 0) {
+        out.nodeid = PACKAGE_FILE_ID;
+        out.generation = PACKAGE_FILE_ID;
+        fill_attr(&(out.attr), fd, PACKAGE_FILE_ID, fd->file_size, S_IFREG | 0444);
+    } else if (strncmp(FUSE_SIDELOAD_HOST_EXIT_FLAG, reinterpret_cast<const char*>(data),
+                       sizeof(FUSE_SIDELOAD_HOST_EXIT_FLAG)) == 0) {
+        out.nodeid = EXIT_FLAG_ID;
+        out.generation = EXIT_FLAG_ID;
+        fill_attr(&(out.attr), fd, EXIT_FLAG_ID, 0, S_IFREG | 0);
+    } else {
+        return -ENOENT;
+    }
+
+    fuse_reply(fd, hdr->unique, &out, sizeof(out));
+    return (out.nodeid == EXIT_FLAG_ID) ? NO_STATUS_EXIT : NO_STATUS;
+}
+
+static int handle_open(void* /* data */, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    if (hdr->nodeid == EXIT_FLAG_ID) return -EPERM;
+    if (hdr->nodeid != PACKAGE_FILE_ID) return -ENOENT;
+
+    struct fuse_open_out out;
+    memset(&out, 0, sizeof(out));
+    out.fh = 10;  // an arbitrary number; we always use the same handle
+    fuse_reply(fd, hdr->unique, &out, sizeof(out));
+    return NO_STATUS;
+}
+
+static int handle_flush(void* data, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    return 0;
+}
+
+static int handle_release(void* data, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    return 0;
+}
+
+// Fetch a block from the host into fd->curr_block and fd->block_data.
+// Returns 0 on successful fetch, negative otherwise.
+static int fetch_block(struct fuse_data* fd, uint32_t block) {
+    if (block == fd->curr_block) {
+        return 0;
+    }
+
+    if (block >= fd->file_blocks) {
+        memset(fd->block_data, 0, fd->block_size);
+        fd->curr_block = block;
+        return 0;
+    }
+
+    size_t fetch_size = fd->block_size;
+    if (block * fd->block_size + fetch_size > fd->file_size) {
+        // If we're reading the last (partial) block of the file,
+        // expect a shorter response from the host, and pad the rest
+        // of the block with zeroes.
+        fetch_size = fd->file_size - (block * fd->block_size);
+        memset(fd->block_data + fetch_size, 0, fd->block_size - fetch_size);
+    }
+
+    int result = fd->vtab->read_block(fd->cookie, block, fd->block_data, fetch_size);
+    if (result < 0) return result;
+
+    fd->curr_block = block;
+
+    // Verify the hash of the block we just got from the host.
+    //
+    // - If the hash of the just-received data matches the stored hash
+    //   for the block, accept it.
+    // - If the stored hash is all zeroes, store the new hash and
+    //   accept the block (this is the first time we've read this
+    //   block).
+    // - Otherwise, return -EINVAL for the read.
+
+    uint8_t hash[SHA256_DIGEST_LENGTH];
+    SHA256(fd->block_data, fd->block_size, hash);
+    uint8_t* blockhash = fd->hashes + block * SHA256_DIGEST_LENGTH;
+    if (memcmp(hash, blockhash, SHA256_DIGEST_LENGTH) == 0) {
+        return 0;
+    }
+
+    int i;
+    for (i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
+        if (blockhash[i] != 0) {
+            fd->curr_block = -1;
+            return -EIO;
+        }
+    }
+
+    memcpy(blockhash, hash, SHA256_DIGEST_LENGTH);
+    return 0;
+}
+
+static int handle_read(void* data, struct fuse_data* fd, const struct fuse_in_header* hdr) {
+    const struct fuse_read_in* req = reinterpret_cast<const struct fuse_read_in*>(data);
+    struct fuse_out_header outhdr;
+    struct iovec vec[3];
+    int vec_used;
+    int result;
+
+    if (hdr->nodeid != PACKAGE_FILE_ID) return -ENOENT;
+
+    uint64_t offset = req->offset;
+    uint32_t size = req->size;
+
+    // The docs on the fuse kernel interface are vague about what to
+    // do when a read request extends past the end of the file.  We
+    // can return a short read -- the return structure does include a
+    // length field -- but in testing that caused the program using
+    // the file to segfault.  (I speculate that this is due to the
+    // reading program accessing it via mmap; maybe mmap dislikes when
+    // you return something short of a whole page?)  To fix this we
+    // zero-pad reads that extend past the end of the file so we're
+    // always returning exactly as many bytes as were requested.
+    // (Users of the mapped file have to know its real length anyway.)
+
+    outhdr.len = sizeof(outhdr) + size;
+    outhdr.error = 0;
+    outhdr.unique = hdr->unique;
+    vec[0].iov_base = &outhdr;
+    vec[0].iov_len = sizeof(outhdr);
+
+    uint32_t block = offset / fd->block_size;
+    result = fetch_block(fd, block);
+    if (result != 0) return result;
+
+    // Two cases:
+    //
+    //   - the read request is entirely within this block.  In this
+    //     case we can reply immediately.
+    //
+    //   - the read request goes over into the next block.  Note that
+    //     since we mount the filesystem with max_read=block_size, a
+    //     read can never span more than two blocks.  In this case we
+    //     copy the block to extra_block and issue a fetch for the
+    //     following block.
+
+    uint32_t block_offset = offset - (block * fd->block_size);
+
+    if (size + block_offset <= fd->block_size) {
+        // First case: the read fits entirely in the first block.
+
+        vec[1].iov_base = fd->block_data + block_offset;
+        vec[1].iov_len = size;
+        vec_used = 2;
+    } else {
+        // Second case: the read spills over into the next block.
+
+        memcpy(fd->extra_block, fd->block_data + block_offset,
+               fd->block_size - block_offset);
+        vec[1].iov_base = fd->extra_block;
+        vec[1].iov_len = fd->block_size - block_offset;
+
+        result = fetch_block(fd, block+1);
+        if (result != 0) return result;
+        vec[2].iov_base = fd->block_data;
+        vec[2].iov_len = size - vec[1].iov_len;
+        vec_used = 3;
+    }
+
+    if (writev(fd->ffd, vec, vec_used) < 0) {
+        printf("*** READ REPLY FAILED: %s ***\n", strerror(errno));
+    }
+    return NO_STATUS;
+}
+
+int run_fuse_sideload(struct provider_vtab* vtab, void* cookie,
+                      uint64_t file_size, uint32_t block_size)
+{
+    int result;
+
+    // If something's already mounted on our mountpoint, try to remove
+    // it.  (Mostly in case of a previous abnormal exit.)
+    umount2(FUSE_SIDELOAD_HOST_MOUNTPOINT, MNT_FORCE);
+
+    if (block_size < 1024) {
+        fprintf(stderr, "block size (%u) is too small\n", block_size);
+        return -1;
+    }
+    if (block_size > (1<<22)) {   // 4 MiB
+        fprintf(stderr, "block size (%u) is too large\n", block_size);
+        return -1;
+    }
+
+    struct fuse_data fd;
+    memset(&fd, 0, sizeof(fd));
+    fd.vtab = vtab;
+    fd.cookie = cookie;
+    fd.file_size = file_size;
+    fd.block_size = block_size;
+    fd.file_blocks = (file_size == 0) ? 0 : (((file_size-1) / block_size) + 1);
+
+    if (fd.file_blocks > (1<<18)) {
+        fprintf(stderr, "file has too many blocks (%u)\n", fd.file_blocks);
+        result = -1;
+        goto done;
+    }
+
+    fd.hashes = (uint8_t*)calloc(fd.file_blocks, SHA256_DIGEST_LENGTH);
+    if (fd.hashes == NULL) {
+        fprintf(stderr, "failed to allocate %d bites for hashes\n",
+                fd.file_blocks * SHA256_DIGEST_LENGTH);
+        result = -1;
+        goto done;
+    }
+
+    fd.uid = getuid();
+    fd.gid = getgid();
+
+    fd.curr_block = -1;
+    fd.block_data = (uint8_t*)malloc(block_size);
+    if (fd.block_data == NULL) {
+        fprintf(stderr, "failed to allocate %d bites for block_data\n", block_size);
+        result = -1;
+        goto done;
+    }
+    fd.extra_block = (uint8_t*)malloc(block_size);
+    if (fd.extra_block == NULL) {
+        fprintf(stderr, "failed to allocate %d bites for extra_block\n", block_size);
+        result = -1;
+        goto done;
+    }
+
+    fd.ffd = open("/dev/fuse", O_RDWR);
+    if (fd.ffd < 0) {
+        perror("open /dev/fuse");
+        result = -1;
+        goto done;
+    }
+
+    char opts[256];
+    snprintf(opts, sizeof(opts),
+             ("fd=%d,user_id=%d,group_id=%d,max_read=%u,"
+              "allow_other,rootmode=040000"),
+             fd.ffd, fd.uid, fd.gid, block_size);
+
+    result = mount("/dev/fuse", FUSE_SIDELOAD_HOST_MOUNTPOINT,
+                   "fuse", MS_NOSUID | MS_NODEV | MS_RDONLY | MS_NOEXEC, opts);
+    if (result < 0) {
+        perror("mount");
+        goto done;
+    }
+    uint8_t request_buffer[sizeof(struct fuse_in_header) + PATH_MAX*8];
+    for (;;) {
+        ssize_t len = TEMP_FAILURE_RETRY(read(fd.ffd, request_buffer, sizeof(request_buffer)));
+        if (len == -1) {
+            perror("read request");
+            if (errno == ENODEV) {
+                result = -1;
+                break;
+            }
+            continue;
+        }
+
+        if ((size_t)len < sizeof(struct fuse_in_header)) {
+            fprintf(stderr, "request too short: len=%zu\n", (size_t)len);
+            continue;
+        }
+
+        struct fuse_in_header* hdr = (struct fuse_in_header*) request_buffer;
+        void* data = request_buffer + sizeof(struct fuse_in_header);
+
+        result = -ENOSYS;
+
+        switch (hdr->opcode) {
+             case FUSE_INIT:
+                result = handle_init(data, &fd, hdr);
+                break;
+
+             case FUSE_LOOKUP:
+                result = handle_lookup(data, &fd, hdr);
+                break;
+
+            case FUSE_GETATTR:
+                result = handle_getattr(data, &fd, hdr);
+                break;
+
+            case FUSE_OPEN:
+                result = handle_open(data, &fd, hdr);
+                break;
+
+            case FUSE_READ:
+                result = handle_read(data, &fd, hdr);
+                break;
+
+            case FUSE_FLUSH:
+                result = handle_flush(data, &fd, hdr);
+                break;
+
+            case FUSE_RELEASE:
+                result = handle_release(data, &fd, hdr);
+                break;
+
+            default:
+                fprintf(stderr, "unknown fuse request opcode %d\n", hdr->opcode);
+                break;
+        }
+
+        if (result == NO_STATUS_EXIT) {
+            result = 0;
+            break;
+        }
+
+        if (result != NO_STATUS) {
+            struct fuse_out_header outhdr;
+            outhdr.len = sizeof(outhdr);
+            outhdr.error = result;
+            outhdr.unique = hdr->unique;
+            TEMP_FAILURE_RETRY(write(fd.ffd, &outhdr, sizeof(outhdr)));
+        }
+    }
+
+  done:
+    fd.vtab->close(fd.cookie);
+
+    result = umount2(FUSE_SIDELOAD_HOST_MOUNTPOINT, MNT_DETACH);
+    if (result < 0) {
+        printf("fuse_sideload umount failed: %s\n", strerror(errno));
+    }
+
+    if (fd.ffd) close(fd.ffd);
+    free(fd.hashes);
+    free(fd.block_data);
+    free(fd.extra_block);
+
+    return result;
+}
diff --git a/recovery/fuse_sideload.h b/recovery/fuse_sideload.h
new file mode 100644
index 0000000..c0b16ef
--- /dev/null
+++ b/recovery/fuse_sideload.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef __FUSE_SIDELOAD_H
+#define __FUSE_SIDELOAD_H
+
+// define the filenames created by the sideload FUSE filesystem
+#define FUSE_SIDELOAD_HOST_MOUNTPOINT "/sideload"
+#define FUSE_SIDELOAD_HOST_FILENAME "package.zip"
+#define FUSE_SIDELOAD_HOST_PATHNAME (FUSE_SIDELOAD_HOST_MOUNTPOINT "/" FUSE_SIDELOAD_HOST_FILENAME)
+#define FUSE_SIDELOAD_HOST_EXIT_FLAG "exit"
+#define FUSE_SIDELOAD_HOST_EXIT_PATHNAME (FUSE_SIDELOAD_HOST_MOUNTPOINT "/" FUSE_SIDELOAD_HOST_EXIT_FLAG)
+
+struct provider_vtab {
+    // read a block
+    int (*read_block)(void* cookie, uint32_t block, uint8_t* buffer, uint32_t fetch_size);
+
+    // close down
+    void (*close)(void* cookie);
+};
+
+int run_fuse_sideload(struct provider_vtab* vtab, void* cookie,
+                      uint64_t file_size, uint32_t block_size);
+
+#endif
diff --git a/recovery/install.cpp b/recovery/install.cpp
new file mode 100644
index 0000000..fa7b73e
--- /dev/null
+++ b/recovery/install.cpp
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include "common.h"
+#include "install.h"
+#include "minui/minui.h"
+#include "minzip/SysUtil.h"
+#include "minzip/Zip.h"
+#include "roots.h"
+#include "ui.h"
+#include "verifier.h"
+
+extern RecoveryUI* ui;
+
+#define ASSUMED_UPDATE_BINARY_NAME  "META-INF/com/google/android/update-binary"
+#define PUBLIC_KEYS_FILE "/res/keys"
+
+// Default allocation of progress bar segments to operations
+static const int VERIFICATION_PROGRESS_TIME = 60;
+static const float VERIFICATION_PROGRESS_FRACTION = 0.25;
+static const float DEFAULT_FILES_PROGRESS_FRACTION = 0.4;
+static const float DEFAULT_IMAGE_PROGRESS_FRACTION = 0.1;
+
+// If the package contains an update binary, extract it and run it.
+static int
+try_update_binary(const char* path, ZipArchive* zip, bool* wipe_cache) {
+    const ZipEntry* binary_entry =
+            mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
+    if (binary_entry == NULL) {
+        mzCloseZipArchive(zip);
+        return INSTALL_CORRUPT;
+    }
+
+    const char* binary = "/tmp/update_binary";
+    unlink(binary);
+    int fd = creat(binary, 0755);
+    if (fd < 0) {
+        mzCloseZipArchive(zip);
+        LOGE("Can't make %s\n", binary);
+        return INSTALL_ERROR;
+    }
+    bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);
+    close(fd);
+    mzCloseZipArchive(zip);
+
+    if (!ok) {
+        LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME);
+        return INSTALL_ERROR;
+    }
+
+    int pipefd[2];
+    pipe(pipefd);
+
+    // When executing the update binary contained in the package, the
+    // arguments passed are:
+    //
+    //   - the version number for this interface
+    //
+    //   - an fd to which the program can write in order to update the
+    //     progress bar.  The program can write single-line commands:
+    //
+    //        progress <frac> <secs>
+    //            fill up the next <frac> part of of the progress bar
+    //            over <secs> seconds.  If <secs> is zero, use
+    //            set_progress commands to manually control the
+    //            progress of this segment of the bar.
+    //
+    //        set_progress <frac>
+    //            <frac> should be between 0.0 and 1.0; sets the
+    //            progress bar within the segment defined by the most
+    //            recent progress command.
+    //
+    //        firmware <"hboot"|"radio"> <filename>
+    //            arrange to install the contents of <filename> in the
+    //            given partition on reboot.
+    //
+    //            (API v2: <filename> may start with "PACKAGE:" to
+    //            indicate taking a file from the OTA package.)
+    //
+    //            (API v3: this command no longer exists.)
+    //
+    //        ui_print <string>
+    //            display <string> on the screen.
+    //
+    //        wipe_cache
+    //            a wipe of cache will be performed following a successful
+    //            installation.
+    //
+    //        clear_display
+    //            turn off the text display.
+    //
+    //        enable_reboot
+    //            packages can explicitly request that they want the user
+    //            to be able to reboot during installation (useful for
+    //            debugging packages that don't exit).
+    //
+    //   - the name of the package zip file.
+    //
+
+    const char* args[5];
+    args[0] = binary;
+    args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk
+    char temp[16];
+    snprintf(temp, sizeof(temp), "%d", pipefd[1]);
+    args[2] = temp;
+    args[3] = path;
+    args[4] = NULL;
+
+    pid_t pid = fork();
+    if (pid == 0) {
+        umask(022);
+        close(pipefd[0]);
+        execv(binary, const_cast<char**>(args));
+        fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));
+        _exit(-1);
+    }
+    close(pipefd[1]);
+
+    *wipe_cache = false;
+    bool retry_update = false;
+
+    char buffer[1024];
+    FILE* from_child = fdopen(pipefd[0], "r");
+    while (fgets(buffer, sizeof(buffer), from_child) != NULL) {
+        char* command = strtok(buffer, " \n");
+        if (command == NULL) {
+            continue;
+        } else if (strcmp(command, "progress") == 0) {
+            char* fraction_s = strtok(NULL, " \n");
+            char* seconds_s = strtok(NULL, " \n");
+
+            float fraction = strtof(fraction_s, NULL);
+            int seconds = strtol(seconds_s, NULL, 10);
+
+            ui->ShowProgress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), seconds);
+        } else if (strcmp(command, "set_progress") == 0) {
+            char* fraction_s = strtok(NULL, " \n");
+            float fraction = strtof(fraction_s, NULL);
+            ui->SetProgress(fraction);
+        } else if (strcmp(command, "ui_print") == 0) {
+            char* str = strtok(NULL, "\n");
+            if (str) {
+                ui->PrintOnScreenOnly("%s", str);
+            } else {
+                ui->PrintOnScreenOnly("\n");
+            }
+            fflush(stdout);
+        } else if (strcmp(command, "wipe_cache") == 0) {
+            *wipe_cache = true;
+        } else if (strcmp(command, "clear_display") == 0) {
+            ui->SetBackground(RecoveryUI::NONE);
+        } else if (strcmp(command, "enable_reboot") == 0) {
+            // packages can explicitly request that they want the user
+            // to be able to reboot during installation (useful for
+            // debugging packages that don't exit).
+            ui->SetEnableReboot(true);
+        } else if (strcmp(command, "retry_update") == 0) {
+            retry_update = true;
+        } else {
+            LOGE("unknown command [%s]\n", command);
+        }
+    }
+    fclose(from_child);
+
+    int status;
+    waitpid(pid, &status, 0);
+    if (retry_update) {
+        return INSTALL_RETRY;
+    }
+    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+        LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status));
+        return INSTALL_ERROR;
+    }
+
+    return INSTALL_SUCCESS;
+}
+
+static int
+really_install_package(const char *path, bool* wipe_cache, bool needs_mount)
+{
+    ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
+    ui->Print("Finding update package...\n");
+    // Give verification half the progress bar...
+    ui->SetProgressType(RecoveryUI::DETERMINATE);
+    ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
+    LOGI("Update location: %s\n", path);
+
+    // Map the update package into memory.
+    ui->Print("Opening update package...\n");
+
+    if (path && needs_mount) {
+        if (path[0] == '@') {
+            ensure_path_mounted(path+1);
+        } else {
+            ensure_path_mounted(path);
+        }
+    }
+
+    MemMapping map;
+    if (sysMapFile(path, &map) != 0) {
+        LOGE("failed to map file\n");
+        return INSTALL_CORRUPT;
+    }
+
+    std::vector<Certificate> loadedKeys;
+    if (!load_keys(PUBLIC_KEYS_FILE, loadedKeys)) {
+        LOGE("Failed to load keys\n");
+        sysReleaseMap(&map);
+        return INSTALL_CORRUPT;
+    }
+    LOGI("%zu key(s) loaded from %s\n", loadedKeys.size(), PUBLIC_KEYS_FILE);
+
+    ui->Print("Verifying update package...\n");
+
+    int err = verify_file(map.addr, map.length, loadedKeys);
+    LOGI("verify_file returned %d\n", err);
+    if (err != VERIFY_SUCCESS) {
+        LOGE("signature verification failed\n");
+        sysReleaseMap(&map);
+        return INSTALL_CORRUPT;
+    }
+
+    /* Try to open the package.
+     */
+    ZipArchive zip;
+    err = mzOpenZipArchive(map.addr, map.length, &zip);
+    if (err != 0) {
+        LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
+        sysReleaseMap(&map);
+        return INSTALL_CORRUPT;
+    }
+
+    /* Verify and install the contents of the package.
+     */
+    ui->Print("Installing update...\n");
+    ui->SetEnableReboot(false);
+    int result = try_update_binary(path, &zip, wipe_cache);
+    ui->SetEnableReboot(true);
+    ui->Print("\n");
+
+    sysReleaseMap(&map);
+
+    return result;
+}
+
+int
+install_package(const char* path, bool* wipe_cache, const char* install_file,
+                bool needs_mount)
+{
+    modified_flash = true;
+
+    FILE* install_log = fopen_path(install_file, "w");
+    if (install_log) {
+        fputs(path, install_log);
+        fputc('\n', install_log);
+    } else {
+        LOGE("failed to open last_install: %s\n", strerror(errno));
+    }
+    int result;
+    if (setup_install_mounts() != 0) {
+        LOGE("failed to set up expected mounts for install; aborting\n");
+        result = INSTALL_ERROR;
+    } else {
+        result = really_install_package(path, wipe_cache, needs_mount);
+    }
+    if (install_log) {
+        fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
+        fputc('\n', install_log);
+        fclose(install_log);
+    }
+    return result;
+}
diff --git a/recovery/install.h b/recovery/install.h
new file mode 100644
index 0000000..fd08e3c
--- /dev/null
+++ b/recovery/install.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef RECOVERY_INSTALL_H_
+#define RECOVERY_INSTALL_H_
+
+#include "common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED,
+        INSTALL_RETRY };
+// Install the package specified by root_path.  If INSTALL_SUCCESS is
+// returned and *wipe_cache is true on exit, caller should wipe the
+// cache partition.
+int install_package(const char* root_path, bool* wipe_cache,
+                    const char* install_file, bool needs_mount);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // RECOVERY_INSTALL_H_
diff --git a/recovery/interlace-frames.py b/recovery/interlace-frames.py
new file mode 100644
index 0000000..3e777b4
--- /dev/null
+++ b/recovery/interlace-frames.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2014 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.
+
+"""
+Script to take a set of frames (PNG files) for a recovery animation and turn
+it into a single output image which contains the input frames interlaced by
+row. Run with the names of all the input frames on the command line. Specify
+the name of the output file with -o (or --output), and optionally specify the
+number of frames per second (FPS) with --fps (default: 20).
+
+e.g.
+interlace-frames.py --fps 20 --output output.png frame0.png frame1.png frame3.png
+"""
+
+from __future__ import print_function
+
+import argparse
+import sys
+try:
+  import Image
+  import PngImagePlugin
+except ImportError:
+  print("This script requires the Python Imaging Library to be installed.")
+  sys.exit(1)
+
+
+def interlace(output, fps, inputs):
+  frames = [Image.open(fn).convert("RGB") for fn in inputs]
+  assert len(frames) > 0, "Must have at least one input frame."
+  sizes = set()
+  for fr in frames:
+    sizes.add(fr.size)
+
+  assert len(sizes) == 1, "All input images must have the same size."
+  w, h = sizes.pop()
+  N = len(frames)
+
+  out = Image.new("RGB", (w, h*N))
+  for j in range(h):
+    for i in range(w):
+      for fn, f in enumerate(frames):
+        out.putpixel((i, j*N+fn), f.getpixel((i, j)))
+
+  # When loading this image, the graphics library expects to find a text
+  # chunk that specifies how many frames this animation represents.  If
+  # you post-process the output of this script with some kind of
+  # optimizer tool (eg pngcrush or zopflipng) make sure that your
+  # optimizer preserves this text chunk.
+
+  meta = PngImagePlugin.PngInfo()
+  meta.add_text("Frames", str(N))
+  meta.add_text("FPS", str(fps))
+
+  out.save(output, pnginfo=meta)
+
+
+def main(argv):
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--fps', default=20)
+  parser.add_argument('--output', '-o', required=True)
+  parser.add_argument('input', nargs='+')
+  args = parser.parse_args(argv)
+
+  interlace(args.output, args.fps, args.input)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
+
diff --git a/recovery/minadbd/Android.mk b/recovery/minadbd/Android.mk
new file mode 100644
index 0000000..34631a9
--- /dev/null
+++ b/recovery/minadbd/Android.mk
@@ -0,0 +1,38 @@
+# Copyright 2005 The Android Open Source Project
+
+LOCAL_PATH:= $(call my-dir)
+
+minadbd_cflags := \
+    -Wall -Werror \
+    -Wno-unused-parameter \
+    -Wno-missing-field-initializers \
+    -DADB_HOST=0 \
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+    adb_main.cpp \
+    fuse_adb_provider.cpp \
+    minadbd_services.cpp \
+
+LOCAL_CLANG := true
+LOCAL_MODULE := libminadbd
+LOCAL_CFLAGS := $(minadbd_cflags)
+LOCAL_CONLY_FLAGS := -Wimplicit-function-declaration
+LOCAL_C_INCLUDES := bootable/recovery system/core/adb
+LOCAL_WHOLE_STATIC_LIBRARIES := libadbd
+LOCAL_STATIC_LIBRARIES := libcrypto libbase
+
+include $(BUILD_STATIC_LIBRARY)
+
+include $(CLEAR_VARS)
+
+LOCAL_CLANG := true
+LOCAL_MODULE := minadbd_test
+LOCAL_SRC_FILES := fuse_adb_provider_test.cpp
+LOCAL_CFLAGS := $(minadbd_cflags)
+LOCAL_C_INCLUDES := $(LOCAL_PATH) system/core/adb
+LOCAL_STATIC_LIBRARIES := libminadbd
+LOCAL_SHARED_LIBRARIES := liblog libbase libcutils
+
+include $(BUILD_NATIVE_TEST)
diff --git a/recovery/minadbd/README.txt b/recovery/minadbd/README.txt
new file mode 100644
index 0000000..e69dc87
--- /dev/null
+++ b/recovery/minadbd/README.txt
@@ -0,0 +1,8 @@
+minadbd is now mostly built from libadbd. The fuse features are unique to
+minadbd, and services.c has been modified as follows:
+
+  - all services removed
+  - all host mode support removed
+  - sideload_service() added; this is the only service supported.  It
+    receives a single blob of data, writes it to a fixed filename, and
+    makes the process exit.
diff --git a/recovery/minadbd/adb_main.cpp b/recovery/minadbd/adb_main.cpp
new file mode 100644
index 0000000..8e581c2
--- /dev/null
+++ b/recovery/minadbd/adb_main.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "adb.h"
+#include "adb_auth.h"
+#include "transport.h"
+
+int adb_server_main(int is_daemon, int server_port, int /* reply_fd */) {
+    adb_device_banner = "sideload";
+
+    signal(SIGPIPE, SIG_IGN);
+
+    // We can't require authentication for sideloading. http://b/22025550.
+    auth_required = false;
+
+    init_transport_registration();
+    usb_init();
+
+    VLOG(ADB) << "Event loop starting";
+    fdevent_loop();
+
+    return 0;
+}
diff --git a/recovery/minadbd/fuse_adb_provider.cpp b/recovery/minadbd/fuse_adb_provider.cpp
new file mode 100644
index 0000000..0f4c256
--- /dev/null
+++ b/recovery/minadbd/fuse_adb_provider.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include "adb.h"
+#include "adb_io.h"
+#include "fuse_adb_provider.h"
+#include "fuse_sideload.h"
+
+int read_block_adb(void* data, uint32_t block, uint8_t* buffer, uint32_t fetch_size) {
+    adb_data* ad = reinterpret_cast<adb_data*>(data);
+
+    if (!WriteFdFmt(ad->sfd, "%08u", block)) {
+        fprintf(stderr, "failed to write to adb host: %s\n", strerror(errno));
+        return -EIO;
+    }
+
+    if (!ReadFdExactly(ad->sfd, buffer, fetch_size)) {
+        fprintf(stderr, "failed to read from adb host: %s\n", strerror(errno));
+        return -EIO;
+    }
+
+    return 0;
+}
+
+static void close_adb(void* data) {
+    adb_data* ad = reinterpret_cast<adb_data*>(data);
+    WriteFdExactly(ad->sfd, "DONEDONE");
+}
+
+int run_adb_fuse(int sfd, uint64_t file_size, uint32_t block_size) {
+    adb_data ad;
+    ad.sfd = sfd;
+    ad.file_size = file_size;
+    ad.block_size = block_size;
+
+    provider_vtab vtab;
+    vtab.read_block = read_block_adb;
+    vtab.close = close_adb;
+
+    return run_fuse_sideload(&vtab, &ad, file_size, block_size);
+}
diff --git a/recovery/minadbd/fuse_adb_provider.h b/recovery/minadbd/fuse_adb_provider.h
new file mode 100644
index 0000000..9941709
--- /dev/null
+++ b/recovery/minadbd/fuse_adb_provider.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef __FUSE_ADB_PROVIDER_H
+#define __FUSE_ADB_PROVIDER_H
+
+#include <stdint.h>
+
+struct adb_data {
+    int sfd;  // file descriptor for the adb channel
+
+    uint64_t file_size;
+    uint32_t block_size;
+};
+
+int read_block_adb(void* cookie, uint32_t block, uint8_t* buffer, uint32_t fetch_size);
+int run_adb_fuse(int sfd, uint64_t file_size, uint32_t block_size);
+
+#endif
diff --git a/recovery/minadbd/fuse_adb_provider_test.cpp b/recovery/minadbd/fuse_adb_provider_test.cpp
new file mode 100644
index 0000000..0f2e881
--- /dev/null
+++ b/recovery/minadbd/fuse_adb_provider_test.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include "fuse_adb_provider.h"
+
+#include <gtest/gtest.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+
+#include <string>
+
+#include "adb_io.h"
+
+TEST(fuse_adb_provider, read_block_adb) {
+  adb_data data = {};
+  int sockets[2];
+
+  ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
+  data.sfd = sockets[0];
+
+  int host_socket = sockets[1];
+  fcntl(host_socket, F_SETFL, O_NONBLOCK);
+
+  const char expected_data[] = "foobar";
+  char block_data[sizeof(expected_data)] = {};
+
+  // If we write the result of read_block_adb's request before the request is
+  // actually made we can avoid needing an extra thread for this test.
+  ASSERT_TRUE(WriteFdExactly(host_socket, expected_data,
+                             strlen(expected_data)));
+
+  uint32_t block = 1234U;
+  const char expected_block[] = "00001234";
+  ASSERT_EQ(0, read_block_adb(reinterpret_cast<void*>(&data), block,
+                              reinterpret_cast<uint8_t*>(block_data),
+                              sizeof(expected_data) - 1));
+
+  // Check that read_block_adb requested the right block.
+  char block_req[sizeof(expected_block)] = {};
+  ASSERT_TRUE(ReadFdExactly(host_socket, block_req, 8));
+  ASSERT_EQ(0, block_req[8]);
+  ASSERT_EQ(8U, strlen(block_req));
+  ASSERT_STREQ(expected_block, block_req);
+
+  // Check that read_block_adb returned the right data.
+  ASSERT_EQ(0, block_req[8]);
+  ASSERT_STREQ(expected_data, block_data);
+
+  // Check that nothing else was written to the socket.
+  char tmp;
+  errno = 0;
+  ASSERT_EQ(-1, read(host_socket, &tmp, 1));
+  ASSERT_EQ(EWOULDBLOCK, errno);
+
+  close(sockets[0]);
+  close(sockets[1]);
+}
+
+TEST(fuse_adb_provider, read_block_adb_fail_write) {
+  adb_data data = {};
+  int sockets[2];
+
+  ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sockets));
+  data.sfd = sockets[0];
+
+  ASSERT_EQ(0, close(sockets[1]));
+
+  char buf[1];
+  ASSERT_EQ(-EIO, read_block_adb(reinterpret_cast<void*>(&data), 0,
+                                 reinterpret_cast<uint8_t*>(buf), 1));
+
+  close(sockets[0]);
+}
diff --git a/recovery/minadbd/minadbd_services.cpp b/recovery/minadbd/minadbd_services.cpp
new file mode 100644
index 0000000..003b519
--- /dev/null
+++ b/recovery/minadbd/minadbd_services.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "adb.h"
+#include "fdevent.h"
+#include "fuse_adb_provider.h"
+#include "sysdeps.h"
+
+typedef struct stinfo stinfo;
+
+struct stinfo {
+    void (*func)(int fd, void *cookie);
+    int fd;
+    void *cookie;
+};
+
+void service_bootstrap_func(void* x) {
+    stinfo* sti = reinterpret_cast<stinfo*>(x);
+    sti->func(sti->fd, sti->cookie);
+    free(sti);
+}
+
+static void sideload_host_service(int sfd, void* data) {
+    char* args = reinterpret_cast<char*>(data);
+    int file_size;
+    int block_size;
+    if (sscanf(args, "%d:%d", &file_size, &block_size) != 2) {
+        printf("bad sideload-host arguments: %s\n", args);
+        exit(1);
+    }
+    free(args);
+
+    printf("sideload-host file size %d block size %d\n", file_size, block_size);
+
+    int result = run_adb_fuse(sfd, file_size, block_size);
+
+    printf("sideload_host finished\n");
+    sleep(1);
+    exit(result == 0 ? 0 : 1);
+}
+
+static int create_service_thread(void (*func)(int, void *), void *cookie) {
+    int s[2];
+    if (adb_socketpair(s)) {
+        printf("cannot create service socket pair\n");
+        return -1;
+    }
+
+    stinfo* sti = reinterpret_cast<stinfo*>(malloc(sizeof(stinfo)));
+    if(sti == 0) fatal("cannot allocate stinfo");
+    sti->func = func;
+    sti->cookie = cookie;
+    sti->fd = s[1];
+
+    if (!adb_thread_create(service_bootstrap_func, sti)) {
+        free(sti);
+        adb_close(s[0]);
+        adb_close(s[1]);
+        printf("cannot create service thread\n");
+        return -1;
+    }
+
+    VLOG(SERVICES) << "service thread started, " << s[0] << ":" << s[1];
+    return s[0];
+}
+
+int service_to_fd(const char* name, const atransport* transport) {
+    int ret = -1;
+
+    if (!strncmp(name, "sideload:", 9)) {
+        // this exit status causes recovery to print a special error
+        // message saying to use a newer adb (that supports
+        // sideload-host).
+        exit(3);
+    } else if (!strncmp(name, "sideload-host:", 14)) {
+        char* arg = strdup(name + 14);
+        ret = create_service_thread(sideload_host_service, arg);
+    }
+    if (ret >= 0) {
+        close_on_exec(ret);
+    }
+    return ret;
+}
diff --git a/recovery/minui/Android.mk b/recovery/minui/Android.mk
new file mode 100644
index 0000000..380ec2b
--- /dev/null
+++ b/recovery/minui/Android.mk
@@ -0,0 +1,49 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+    events.cpp \
+    graphics.cpp \
+    graphics_adf.cpp \
+    graphics_drm.cpp \
+    graphics_fbdev.cpp \
+    resources.cpp \
+
+LOCAL_WHOLE_STATIC_LIBRARIES += libadf
+LOCAL_WHOLE_STATIC_LIBRARIES += libdrm
+LOCAL_WHOLE_STATIC_LIBRARIES += libsync_recovery
+LOCAL_STATIC_LIBRARIES += libpng
+
+LOCAL_MODULE := libminui
+
+LOCAL_CLANG := true
+
+# This used to compare against values in double-quotes (which are just
+# ordinary characters in this context).  Strip double-quotes from the
+# value so that either will work.
+
+ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),ABGR_8888)
+  LOCAL_CFLAGS += -DRECOVERY_ABGR
+endif
+ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),RGBX_8888)
+  LOCAL_CFLAGS += -DRECOVERY_RGBX
+endif
+ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),BGRA_8888)
+  LOCAL_CFLAGS += -DRECOVERY_BGRA
+endif
+
+ifneq ($(TARGET_RECOVERY_OVERSCAN_PERCENT),)
+  LOCAL_CFLAGS += -DOVERSCAN_PERCENT=$(TARGET_RECOVERY_OVERSCAN_PERCENT)
+else
+  LOCAL_CFLAGS += -DOVERSCAN_PERCENT=0
+endif
+
+include $(BUILD_STATIC_LIBRARY)
+
+# Used by OEMs for factory test images.
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_MODULE := libminui
+LOCAL_WHOLE_STATIC_LIBRARIES += libminui
+LOCAL_SHARED_LIBRARIES := libpng
+include $(BUILD_SHARED_LIBRARY)
diff --git a/recovery/minui/events.cpp b/recovery/minui/events.cpp
new file mode 100644
index 0000000..e6e7bd2
--- /dev/null
+++ b/recovery/minui/events.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <unistd.h>
+
+#include <linux/input.h>
+
+#include "minui.h"
+
+#define MAX_DEVICES 16
+#define MAX_MISC_FDS 16
+
+#define BITS_PER_LONG (sizeof(unsigned long) * 8)
+#define BITS_TO_LONGS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG)
+
+struct fd_info {
+    int fd;
+    ev_callback cb;
+    void* data;
+};
+
+static int g_epoll_fd;
+static epoll_event polledevents[MAX_DEVICES + MAX_MISC_FDS];
+static int npolledevents;
+
+static fd_info ev_fdinfo[MAX_DEVICES + MAX_MISC_FDS];
+
+static unsigned ev_count = 0;
+static unsigned ev_dev_count = 0;
+static unsigned ev_misc_count = 0;
+
+static bool test_bit(size_t bit, unsigned long* array) { // NOLINT
+    return (array[bit/BITS_PER_LONG] & (1UL << (bit % BITS_PER_LONG))) != 0;
+}
+
+int ev_init(ev_callback input_cb, void* data) {
+    bool epollctlfail = false;
+
+    g_epoll_fd = epoll_create(MAX_DEVICES + MAX_MISC_FDS);
+    if (g_epoll_fd == -1) {
+        return -1;
+    }
+
+    DIR* dir = opendir("/dev/input");
+    if (dir != NULL) {
+        dirent* de;
+        while ((de = readdir(dir))) {
+            // Use unsigned long to match ioctl's parameter type.
+            unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT
+
+//            fprintf(stderr,"/dev/input/%s\n", de->d_name);
+            if (strncmp(de->d_name, "event", 5)) continue;
+            int fd = openat(dirfd(dir), de->d_name, O_RDONLY);
+            if (fd == -1) continue;
+
+            // Read the evbits of the input device.
+            if (ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) {
+                close(fd);
+                continue;
+            }
+
+            // We assume that only EV_KEY, EV_REL, and EV_SW event types are ever needed.
+            if (!test_bit(EV_KEY, ev_bits) && !test_bit(EV_REL, ev_bits) && !test_bit(EV_SW, ev_bits)) {
+                close(fd);
+                continue;
+            }
+
+            epoll_event ev;
+            ev.events = EPOLLIN | EPOLLWAKEUP;
+            ev.data.ptr = &ev_fdinfo[ev_count];
+            if (epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
+                close(fd);
+                epollctlfail = true;
+                continue;
+            }
+
+            ev_fdinfo[ev_count].fd = fd;
+            ev_fdinfo[ev_count].cb = input_cb;
+            ev_fdinfo[ev_count].data = data;
+            ev_count++;
+            ev_dev_count++;
+            if (ev_dev_count == MAX_DEVICES) break;
+        }
+
+        closedir(dir);
+    }
+
+    if (epollctlfail && !ev_count) {
+        close(g_epoll_fd);
+        g_epoll_fd = -1;
+        return -1;
+    }
+
+    return 0;
+}
+
+int ev_get_epollfd(void) {
+    return g_epoll_fd;
+}
+
+int ev_add_fd(int fd, ev_callback cb, void* data) {
+    if (ev_misc_count == MAX_MISC_FDS || cb == NULL) {
+        return -1;
+    }
+
+    epoll_event ev;
+    ev.events = EPOLLIN | EPOLLWAKEUP;
+    ev.data.ptr = (void *)&ev_fdinfo[ev_count];
+    int ret = epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, fd, &ev);
+    if (!ret) {
+        ev_fdinfo[ev_count].fd = fd;
+        ev_fdinfo[ev_count].cb = cb;
+        ev_fdinfo[ev_count].data = data;
+        ev_count++;
+        ev_misc_count++;
+    }
+
+    return ret;
+}
+
+void ev_exit(void) {
+    while (ev_count > 0) {
+        close(ev_fdinfo[--ev_count].fd);
+    }
+    ev_misc_count = 0;
+    ev_dev_count = 0;
+    close(g_epoll_fd);
+}
+
+int ev_wait(int timeout) {
+    npolledevents = epoll_wait(g_epoll_fd, polledevents, ev_count, timeout);
+    if (npolledevents <= 0) {
+        return -1;
+    }
+    return 0;
+}
+
+void ev_dispatch(void) {
+    for (int n = 0; n < npolledevents; n++) {
+        fd_info* fdi = reinterpret_cast<fd_info*>(polledevents[n].data.ptr);
+        ev_callback cb = fdi->cb;
+        if (cb) {
+            cb(fdi->fd, polledevents[n].events, fdi->data);
+        }
+    }
+}
+
+int ev_get_input(int fd, uint32_t epevents, input_event* ev) {
+    if (epevents & EPOLLIN) {
+        ssize_t r = TEMP_FAILURE_RETRY(read(fd, ev, sizeof(*ev)));
+        if (r == sizeof(*ev)) {
+            return 0;
+        }
+    }
+    return -1;
+}
+
+int ev_sync_key_state(ev_set_key_callback set_key_cb, void* data) {
+    // Use unsigned long to match ioctl's parameter type.
+    unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT
+    unsigned long key_bits[BITS_TO_LONGS(KEY_MAX)]; // NOLINT
+
+    for (size_t i = 0; i < ev_dev_count; ++i) {
+        memset(ev_bits, 0, sizeof(ev_bits));
+        memset(key_bits, 0, sizeof(key_bits));
+
+        if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) {
+            continue;
+        }
+        if (!test_bit(EV_KEY, ev_bits)) {
+            continue;
+        }
+        if (ioctl(ev_fdinfo[i].fd, EVIOCGKEY(sizeof(key_bits)), key_bits) == -1) {
+            continue;
+        }
+
+        for (int code = 0; code <= KEY_MAX; code++) {
+            if (test_bit(code, key_bits)) {
+                set_key_cb(code, 1, data);
+            }
+        }
+    }
+
+    return 0;
+}
+
+void ev_iterate_available_keys(const std::function<void(int)>& f) {
+    // Use unsigned long to match ioctl's parameter type.
+    unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT
+    unsigned long key_bits[BITS_TO_LONGS(KEY_MAX)]; // NOLINT
+
+    for (size_t i = 0; i < ev_dev_count; ++i) {
+        memset(ev_bits, 0, sizeof(ev_bits));
+        memset(key_bits, 0, sizeof(key_bits));
+
+        // Does this device even have keys?
+        if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) {
+            continue;
+        }
+        if (!test_bit(EV_KEY, ev_bits)) {
+            continue;
+        }
+
+        int rc = ioctl(ev_fdinfo[i].fd, EVIOCGBIT(EV_KEY, KEY_MAX), key_bits);
+        if (rc == -1) {
+            continue;
+        }
+
+        for (int key_code = 0; key_code <= KEY_MAX; ++key_code) {
+            if (test_bit(key_code, key_bits)) {
+                f(key_code);
+            }
+        }
+    }
+}
diff --git a/recovery/minui/font_10x18.h b/recovery/minui/font_10x18.h
new file mode 100644
index 0000000..29d7053
--- /dev/null
+++ b/recovery/minui/font_10x18.h
@@ -0,0 +1,214 @@
+struct {
+  unsigned width;
+  unsigned height;
+  unsigned cwidth;
+  unsigned cheight;
+  unsigned char rundata[2973];
+} font = {
+  .width = 960,
+  .height = 18,
+  .cwidth = 10,
+  .cheight = 18,
+  .rundata = {
+0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x55,0x82,0x06,0x82,0x02,0x82,0x10,0x82,
+0x11,0x83,0x08,0x82,0x0a,0x82,0x04,0x82,0x46,0x82,0x08,0x82,0x07,0x84,0x06,
+0x84,0x0a,0x81,0x03,0x88,0x04,0x84,0x04,0x88,0x04,0x84,0x06,0x84,0x1e,0x81,
+0x0e,0x81,0x0a,0x84,0x06,0x84,0x07,0x82,0x05,0x85,0x07,0x84,0x04,0x86,0x04,
+0x88,0x02,0x88,0x04,0x84,0x04,0x82,0x04,0x82,0x02,0x88,0x05,0x86,0x01,0x82,
+0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,
+0x86,0x06,0x84,0x04,0x86,0x06,0x84,0x04,0x88,0x02,0x82,0x04,0x82,0x02,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
+0x88,0x03,0x86,0x0e,0x86,0x06,0x82,0x11,0x82,0x10,0x82,0x18,0x82,0x0f,0x84,
+0x0d,0x82,0x1c,0x82,0x09,0x84,0x7f,0x16,0x84,0x05,0x82,0x05,0x84,0x07,0x83,
+0x02,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x03,0x86,0x04,
+0x83,0x02,0x82,0x03,0x82,0x01,0x82,0x07,0x82,0x09,0x82,0x06,0x82,0x3e,0x82,
+0x04,0x84,0x06,0x83,0x06,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x03,
+0x82,0x09,0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
+0x1c,0x82,0x0e,0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x05,0x84,0x04,
+0x82,0x02,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
+0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x04,
+0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x03,0x82,0x02,0x82,
+0x03,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x02,
+0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
+0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x03,0x82,0x08,0x82,0x0c,
+0x82,0x05,0x84,0x11,0x82,0x0f,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,
+0x1c,0x82,0x0b,0x82,0x7f,0x15,0x82,0x08,0x82,0x08,0x82,0x05,0x82,0x01,0x82,
+0x01,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x02,0x82,0x01,
+0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x07,0x82,
+0x08,0x82,0x08,0x82,0x3d,0x82,0x03,0x82,0x02,0x82,0x04,0x84,0x05,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x06,0x83,0x03,0x82,0x08,0x82,0x04,0x81,0x09,0x82,
+0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x1a,0x82,0x10,0x82,0x06,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,
+0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,
+0x02,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
+0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x08,0x82,0x0c,0x82,0x04,0x82,0x02,0x82,
+0x11,0x82,0x0e,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,0x0b,0x82,0x0b,
+0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x05,0x82,
+0x02,0x83,0x1a,0x82,0x07,0x81,0x02,0x81,0x07,0x82,0x01,0x82,0x02,0x82,0x01,
+0x82,0x05,0x82,0x01,0x84,0x04,0x82,0x01,0x82,0x07,0x82,0x08,0x82,0x08,0x82,
+0x06,0x82,0x02,0x82,0x06,0x82,0x28,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,
+0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x84,0x03,0x82,0x08,0x82,
+0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x19,0x82,0x12,0x82,0x05,
+0x82,0x04,0x82,0x02,0x82,0x02,0x84,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
+0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,
+0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,0x02,0x83,
+0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,0x05,0x82,0x04,0x82,0x02,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
+0x82,0x04,0x82,0x09,0x82,0x0b,0x82,0x03,0x82,0x04,0x82,0x20,0x82,0x18,0x82,
+0x0e,0x82,0x10,0x82,0x0b,0x82,0x0b,0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,
+0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x88,0x01,0x82,0x01,0x82,0x06,0x83,
+0x01,0x82,0x04,0x84,0x08,0x81,0x08,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,
+0x82,0x28,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x08,0x82,0x04,0x82,
+0x01,0x82,0x03,0x82,0x08,0x82,0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x18,0x82,0x06,0x88,0x06,0x82,0x04,0x82,0x04,0x82,0x02,0x82,0x01,0x85,
+0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,
+0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,
+0x02,0x82,0x04,0x82,0x08,0x88,0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,
+0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,
+0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x84,0x06,
+0x84,0x08,0x82,0x05,0x82,0x09,0x82,0x0b,0x82,0x2b,0x82,0x18,0x82,0x0e,0x82,
+0x10,0x82,0x1c,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x26,
+0x82,0x11,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x09,0x82,0x06,0x82,0x12,0x82,
+0x0a,0x82,0x06,0x84,0x07,0x82,0x27,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,
+0x82,0x07,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,0x83,0x04,0x82,0x01,0x83,
+0x08,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x83,0x07,0x83,0x05,
+0x82,0x16,0x82,0x08,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
+0x02,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,
+0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,
+0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
+0x0a,0x82,0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,
+0x82,0x04,0x84,0x06,0x84,0x08,0x82,0x05,0x82,0x0a,0x82,0x0a,0x82,0x23,0x85,
+0x03,0x82,0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x84,0x04,0x86,0x05,
+0x85,0x01,0x81,0x02,0x82,0x01,0x83,0x05,0x84,0x09,0x84,0x02,0x82,0x03,0x82,
+0x06,0x82,0x05,0x81,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x83,0x06,0x84,0x04,
+0x82,0x01,0x83,0x06,0x83,0x01,0x82,0x02,0x82,0x01,0x84,0x04,0x86,0x03,0x86,
+0x04,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x03,0x87,0x05,0x82,0x08,0x82,0x08,0x82,0x26,0x82,
+0x11,0x82,0x01,0x82,0x04,0x86,0x07,0x82,0x05,0x83,0x12,0x82,0x0a,0x82,0x04,
+0x88,0x02,0x88,0x0c,0x88,0x10,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,
+0x06,0x83,0x04,0x82,0x03,0x82,0x03,0x83,0x02,0x82,0x03,0x83,0x02,0x82,0x07,
+0x82,0x06,0x84,0x05,0x82,0x02,0x83,0x05,0x83,0x07,0x83,0x04,0x82,0x18,0x82,
+0x06,0x82,0x04,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x86,0x04,
+0x82,0x08,0x82,0x04,0x82,0x02,0x86,0x04,0x86,0x04,0x82,0x02,0x84,0x02,0x88,
+0x05,0x82,0x0a,0x82,0x03,0x85,0x05,0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,
+0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
+0x04,0x82,0x02,0x82,0x03,0x82,0x05,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x03,
+0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x08,0x82,0x08,0x82,
+0x06,0x82,0x0a,0x82,0x0a,0x82,0x22,0x82,0x03,0x82,0x02,0x83,0x02,0x82,0x04,
+0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x02,0x82,0x05,0x82,0x06,0x82,
+0x03,0x83,0x02,0x83,0x02,0x82,0x06,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,0x07,
+0x82,0x05,0x88,0x02,0x83,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,
+0x04,0x82,0x02,0x83,0x03,0x83,0x02,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,
+0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
+0x03,0x82,0x04,0x82,0x08,0x82,0x02,0x84,0x09,0x82,0x09,0x84,0x23,0x82,0x11,
+0x82,0x01,0x82,0x06,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x01,0x82,0x11,0x82,
+0x0a,0x82,0x06,0x84,0x07,0x82,0x26,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x08,
+0x83,0x09,0x82,0x03,0x82,0x03,0x82,0x09,0x82,0x02,0x82,0x04,0x82,0x05,0x82,
+0x06,0x82,0x02,0x82,0x05,0x83,0x01,0x82,0x17,0x82,0x16,0x82,0x06,0x82,0x05,
+0x82,0x01,0x82,0x01,0x82,0x02,0x88,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
+0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
+0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
+0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x86,0x04,0x82,0x04,0x82,0x02,
+0x86,0x09,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,
+0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,0x82,0x27,
+0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
+0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
+0x82,0x01,0x82,0x08,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
+0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x07,
+0x82,0x0a,0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,
+0x04,0x84,0x04,0x82,0x04,0x82,0x07,0x82,0x06,0x82,0x08,0x82,0x08,0x82,0x26,
+0x82,0x0f,0x88,0x05,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x02,0x82,0x01,0x82,
+0x0d,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,0x82,0x26,0x82,0x05,0x82,0x04,
+0x82,0x05,0x82,0x07,0x82,0x0c,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,
+0x05,0x82,0x05,0x82,0x04,0x82,0x08,0x82,0x18,0x82,0x14,0x82,0x07,0x82,0x05,
+0x82,0x01,0x84,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,
+0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
+0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
+0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,
+0x82,0x02,0x82,0x0a,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,
+0x01,0x82,0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,
+0x82,0x22,0x87,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x88,
+0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
+0x84,0x09,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x08,0x86,0x05,
+0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,
+0x05,0x82,0x05,0x82,0x04,0x82,0x06,0x82,0x07,0x82,0x08,0x82,0x08,0x82,0x26,
+0x82,0x10,0x82,0x01,0x82,0x07,0x82,0x01,0x82,0x04,0x82,0x01,0x83,0x02,0x82,
+0x03,0x83,0x0f,0x82,0x08,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x25,0x82,0x07,
+0x82,0x02,0x82,0x06,0x82,0x06,0x82,0x07,0x82,0x04,0x82,0x07,0x82,0x09,0x82,
+0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,0x82,0x08,0x82,0x19,0x82,0x05,
+0x88,0x05,0x82,0x08,0x82,0x05,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x02,0x82,
+0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
+0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,
+0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x03,0x82,0x09,0x82,0x05,0x82,
+0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x06,
+0x82,0x06,0x82,0x08,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,0x04,0x82,0x02,0x82,
+0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,0x82,0x06,0x82,0x03,
+0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x85,0x08,0x82,0x05,0x82,
+0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,0x06,0x82,0x04,0x82,
+0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,
+0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x38,0x82,0x01,0x82,0x04,0x82,0x01,0x82,
+0x01,0x82,0x04,0x84,0x01,0x82,0x01,0x82,0x03,0x82,0x10,0x82,0x08,0x82,0x30,
+0x83,0x06,0x82,0x07,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x08,0x82,0x04,0x82,
+0x07,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,
+0x82,0x03,0x81,0x04,0x82,0x1a,0x82,0x10,0x82,0x10,0x82,0x08,0x82,0x04,0x82,
+0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
+0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,
+0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,
+0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x02,0x84,0x02,0x82,0x03,0x82,0x03,0x82,
+0x04,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x05,0x83,0x02,0x83,0x03,
+0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x09,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,
+0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,
+0x82,0x07,0x85,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,
+0x07,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,
+0x06,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x04,0x84,0x04,
+0x82,0x04,0x82,0x04,0x82,0x09,0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x82,
+0x01,0x82,0x05,0x86,0x04,0x82,0x01,0x82,0x01,0x82,0x01,0x83,0x01,0x84,0x10,
+0x82,0x06,0x82,0x1d,0x83,0x11,0x83,0x05,0x82,0x09,0x84,0x07,0x82,0x05,0x82,
+0x09,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
+0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x06,0x83,0x07,0x83,0x09,0x82,
+0x0e,0x82,0x0a,0x82,0x06,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,
+0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x09,0x82,
+0x02,0x83,0x02,0x82,0x04,0x82,0x05,0x82,0x06,0x82,0x01,0x82,0x04,0x82,0x04,
+0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
+0x03,0x82,0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x06,
+0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
+0x05,0x82,0x05,0x82,0x09,0x82,0x0d,0x82,0x07,0x82,0x21,0x82,0x04,0x82,0x02,
+0x83,0x02,0x82,0x04,0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x03,0x82,
+0x04,0x82,0x06,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x03,
+0x82,0x06,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
+0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x07,0x82,0x04,
+0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x02,0x83,0x05,0x82,0x05,0x88,0x03,0x82,
+0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x0a,0x82,0x08,0x82,0x08,0x82,0x26,
+0x82,0x1c,0x82,0x06,0x82,0x02,0x83,0x03,0x84,0x02,0x82,0x10,0x82,0x04,0x82,
+0x1e,0x83,0x11,0x83,0x05,0x82,0x0a,0x82,0x05,0x88,0x02,0x88,0x04,0x84,0x09,
+0x82,0x05,0x84,0x06,0x84,0x05,0x82,0x09,0x84,0x06,0x84,0x07,0x83,0x07,0x83,
+0x0a,0x81,0x0e,0x81,0x0b,0x82,0x07,0x85,0x03,0x82,0x04,0x82,0x02,0x86,0x06,
+0x84,0x04,0x86,0x04,0x88,0x02,0x82,0x0a,0x84,0x01,0x81,0x02,0x82,0x04,0x82,
+0x02,0x88,0x04,0x83,0x05,0x82,0x04,0x82,0x02,0x88,0x02,0x82,0x04,0x82,0x02,
+0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x0a,0x85,0x03,0x82,0x04,0x82,0x04,0x84,
+0x07,0x82,0x07,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
+0x82,0x05,0x88,0x03,0x86,0x09,0x82,0x03,0x86,0x22,0x85,0x01,0x81,0x02,0x82,
+0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x85,0x05,0x82,0x07,0x86,0x03,
+0x82,0x04,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x88,0x02,0x82,
+0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x83,0x06,
+0x83,0x01,0x82,0x03,0x82,0x08,0x86,0x06,0x84,0x05,0x83,0x01,0x82,0x05,0x82,
+0x06,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x83,0x01,0x82,0x03,0x87,0x06,
+0x84,0x05,0x82,0x05,0x84,0x7f,0x15,0x83,0x7f,0x14,0x83,0x7f,0x5e,0x82,0x7f,
+0x05,0x89,0x47,0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x4e,
+0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,0x82,0x04,0x82,0x17,0x82,0x03,0x82,
+0x34,0x82,0x0e,0x82,0x48,0x82,0x04,0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,
+0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x49,0x82,0x02,0x82,
+0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0c,0x86,0x19,0x85,0x35,0x82,0x0e,0x82,0x4a,
+0x84,0x3f,
+0x00,
+  }
+};
diff --git a/recovery/minui/graphics.cpp b/recovery/minui/graphics.cpp
new file mode 100644
index 0000000..c0eea9e
--- /dev/null
+++ b/recovery/minui/graphics.cpp
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <stdio.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <linux/fb.h>
+#include <linux/kd.h>
+
+#include <time.h>
+
+#include "font_10x18.h"
+#include "minui.h"
+#include "graphics.h"
+
+struct GRFont {
+    GRSurface* texture;
+    int cwidth;
+    int cheight;
+};
+
+static GRFont* gr_font = NULL;
+static minui_backend* gr_backend = NULL;
+
+static int overscan_percent = OVERSCAN_PERCENT;
+static int overscan_offset_x = 0;
+static int overscan_offset_y = 0;
+
+static unsigned char gr_current_r = 255;
+static unsigned char gr_current_g = 255;
+static unsigned char gr_current_b = 255;
+static unsigned char gr_current_a = 255;
+
+static GRSurface* gr_draw = NULL;
+
+static bool outside(int x, int y)
+{
+    return x < 0 || x >= gr_draw->width || y < 0 || y >= gr_draw->height;
+}
+
+int gr_measure(const char *s)
+{
+    return gr_font->cwidth * strlen(s);
+}
+
+void gr_font_size(int *x, int *y)
+{
+    *x = gr_font->cwidth;
+    *y = gr_font->cheight;
+}
+
+static void text_blend(unsigned char* src_p, int src_row_bytes,
+                       unsigned char* dst_p, int dst_row_bytes,
+                       int width, int height)
+{
+    for (int j = 0; j < height; ++j) {
+        unsigned char* sx = src_p;
+        unsigned char* px = dst_p;
+        for (int i = 0; i < width; ++i) {
+            unsigned char a = *sx++;
+            if (gr_current_a < 255) a = ((int)a * gr_current_a) / 255;
+            if (a == 255) {
+                *px++ = gr_current_r;
+                *px++ = gr_current_g;
+                *px++ = gr_current_b;
+                px++;
+            } else if (a > 0) {
+                *px = (*px * (255-a) + gr_current_r * a) / 255;
+                ++px;
+                *px = (*px * (255-a) + gr_current_g * a) / 255;
+                ++px;
+                *px = (*px * (255-a) + gr_current_b * a) / 255;
+                ++px;
+                ++px;
+            } else {
+                px += 4;
+            }
+        }
+        src_p += src_row_bytes;
+        dst_p += dst_row_bytes;
+    }
+}
+
+void gr_text(int x, int y, const char *s, bool bold)
+{
+    GRFont* font = gr_font;
+
+    if (!font->texture || gr_current_a == 0) return;
+
+    bold = bold && (font->texture->height != font->cheight);
+
+    x += overscan_offset_x;
+    y += overscan_offset_y;
+
+    unsigned char ch;
+    while ((ch = *s++)) {
+        if (outside(x, y) || outside(x+font->cwidth-1, y+font->cheight-1)) break;
+
+        if (ch < ' ' || ch > '~') {
+            ch = '?';
+        }
+
+        unsigned char* src_p = font->texture->data + ((ch - ' ') * font->cwidth) +
+                               (bold ? font->cheight * font->texture->row_bytes : 0);
+        unsigned char* dst_p = gr_draw->data + y*gr_draw->row_bytes + x*gr_draw->pixel_bytes;
+
+        text_blend(src_p, font->texture->row_bytes,
+                   dst_p, gr_draw->row_bytes,
+                   font->cwidth, font->cheight);
+
+        x += font->cwidth;
+    }
+}
+
+void gr_texticon(int x, int y, GRSurface* icon) {
+    if (icon == NULL) return;
+
+    if (icon->pixel_bytes != 1) {
+        printf("gr_texticon: source has wrong format\n");
+        return;
+    }
+
+    x += overscan_offset_x;
+    y += overscan_offset_y;
+
+    if (outside(x, y) || outside(x+icon->width-1, y+icon->height-1)) return;
+
+    unsigned char* src_p = icon->data;
+    unsigned char* dst_p = gr_draw->data + y*gr_draw->row_bytes + x*gr_draw->pixel_bytes;
+
+    text_blend(src_p, icon->row_bytes,
+               dst_p, gr_draw->row_bytes,
+               icon->width, icon->height);
+}
+
+void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
+{
+#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
+    gr_current_r = b;
+    gr_current_g = g;
+    gr_current_b = r;
+    gr_current_a = a;
+#else
+    gr_current_r = r;
+    gr_current_g = g;
+    gr_current_b = b;
+    gr_current_a = a;
+#endif
+}
+
+void gr_clear()
+{
+    if (gr_current_r == gr_current_g && gr_current_r == gr_current_b) {
+        memset(gr_draw->data, gr_current_r, gr_draw->height * gr_draw->row_bytes);
+    } else {
+        unsigned char* px = gr_draw->data;
+        for (int y = 0; y < gr_draw->height; ++y) {
+            for (int x = 0; x < gr_draw->width; ++x) {
+                *px++ = gr_current_r;
+                *px++ = gr_current_g;
+                *px++ = gr_current_b;
+                px++;
+            }
+            px += gr_draw->row_bytes - (gr_draw->width * gr_draw->pixel_bytes);
+        }
+    }
+}
+
+void gr_fill(int x1, int y1, int x2, int y2)
+{
+    x1 += overscan_offset_x;
+    y1 += overscan_offset_y;
+
+    x2 += overscan_offset_x;
+    y2 += overscan_offset_y;
+
+    if (outside(x1, y1) || outside(x2-1, y2-1)) return;
+
+    unsigned char* p = gr_draw->data + y1 * gr_draw->row_bytes + x1 * gr_draw->pixel_bytes;
+    if (gr_current_a == 255) {
+        int x, y;
+        for (y = y1; y < y2; ++y) {
+            unsigned char* px = p;
+            for (x = x1; x < x2; ++x) {
+                *px++ = gr_current_r;
+                *px++ = gr_current_g;
+                *px++ = gr_current_b;
+                px++;
+            }
+            p += gr_draw->row_bytes;
+        }
+    } else if (gr_current_a > 0) {
+        int x, y;
+        for (y = y1; y < y2; ++y) {
+            unsigned char* px = p;
+            for (x = x1; x < x2; ++x) {
+                *px = (*px * (255-gr_current_a) + gr_current_r * gr_current_a) / 255;
+                ++px;
+                *px = (*px * (255-gr_current_a) + gr_current_g * gr_current_a) / 255;
+                ++px;
+                *px = (*px * (255-gr_current_a) + gr_current_b * gr_current_a) / 255;
+                ++px;
+                ++px;
+            }
+            p += gr_draw->row_bytes;
+        }
+    }
+}
+
+void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
+    if (source == NULL) return;
+
+    if (gr_draw->pixel_bytes != source->pixel_bytes) {
+        printf("gr_blit: source has wrong format\n");
+        return;
+    }
+
+    dx += overscan_offset_x;
+    dy += overscan_offset_y;
+
+    if (outside(dx, dy) || outside(dx+w-1, dy+h-1)) return;
+
+    unsigned char* src_p = source->data + sy*source->row_bytes + sx*source->pixel_bytes;
+    unsigned char* dst_p = gr_draw->data + dy*gr_draw->row_bytes + dx*gr_draw->pixel_bytes;
+
+    int i;
+    for (i = 0; i < h; ++i) {
+        memcpy(dst_p, src_p, w * source->pixel_bytes);
+        src_p += source->row_bytes;
+        dst_p += gr_draw->row_bytes;
+    }
+}
+
+unsigned int gr_get_width(GRSurface* surface) {
+    if (surface == NULL) {
+        return 0;
+    }
+    return surface->width;
+}
+
+unsigned int gr_get_height(GRSurface* surface) {
+    if (surface == NULL) {
+        return 0;
+    }
+    return surface->height;
+}
+
+static void gr_init_font(void)
+{
+    gr_font = reinterpret_cast<GRFont*>(calloc(sizeof(*gr_font), 1));
+
+    int res = res_create_alpha_surface("font", &(gr_font->texture));
+    if (res == 0) {
+        // The font image should be a 96x2 array of character images.  The
+        // columns are the printable ASCII characters 0x20 - 0x7f.  The
+        // top row is regular text; the bottom row is bold.
+        gr_font->cwidth = gr_font->texture->width / 96;
+        gr_font->cheight = gr_font->texture->height / 2;
+    } else {
+        printf("failed to read font: res=%d\n", res);
+
+        // fall back to the compiled-in font.
+        gr_font->texture = reinterpret_cast<GRSurface*>(malloc(sizeof(*gr_font->texture)));
+        gr_font->texture->width = font.width;
+        gr_font->texture->height = font.height;
+        gr_font->texture->row_bytes = font.width;
+        gr_font->texture->pixel_bytes = 1;
+
+        unsigned char* bits = reinterpret_cast<unsigned char*>(malloc(font.width * font.height));
+        gr_font->texture->data = reinterpret_cast<unsigned char*>(bits);
+
+        unsigned char data;
+        unsigned char* in = font.rundata;
+        while((data = *in++)) {
+            memset(bits, (data & 0x80) ? 255 : 0, data & 0x7f);
+            bits += (data & 0x7f);
+        }
+
+        gr_font->cwidth = font.cwidth;
+        gr_font->cheight = font.cheight;
+    }
+}
+
+#if 0
+// Exercises many of the gr_*() functions; useful for testing.
+static void gr_test() {
+    GRSurface** images;
+    int frames;
+    int result = res_create_multi_surface("icon_installing", &frames, &images);
+    if (result < 0) {
+        printf("create surface %d\n", result);
+        gr_exit();
+        return;
+    }
+
+    time_t start = time(NULL);
+    int x;
+    for (x = 0; x <= 1200; ++x) {
+        if (x < 400) {
+            gr_color(0, 0, 0, 255);
+        } else {
+            gr_color(0, (x-400)%128, 0, 255);
+        }
+        gr_clear();
+
+        gr_color(255, 0, 0, 255);
+        GRSurface* frame = images[x%frames];
+        gr_blit(frame, 0, 0, frame->width, frame->height, x, 0);
+
+        gr_color(255, 0, 0, 128);
+        gr_fill(400, 150, 600, 350);
+
+        gr_color(255, 255, 255, 255);
+        gr_text(500, 225, "hello, world!", 0);
+        gr_color(255, 255, 0, 128);
+        gr_text(300+x, 275, "pack my box with five dozen liquor jugs", 1);
+
+        gr_color(0, 0, 255, 128);
+        gr_fill(gr_draw->width - 200 - x, 300, gr_draw->width - x, 500);
+
+        gr_draw = gr_backend->flip(gr_backend);
+    }
+    printf("getting end time\n");
+    time_t end = time(NULL);
+    printf("got end time\n");
+    printf("start %ld end %ld\n", (long)start, (long)end);
+    if (end > start) {
+        printf("%.2f fps\n", ((double)x) / (end-start));
+    }
+}
+#endif
+
+void gr_flip() {
+    gr_draw = gr_backend->flip(gr_backend);
+}
+
+int gr_init(void)
+{
+    gr_init_font();
+
+    gr_backend = open_adf();
+    if (gr_backend) {
+        gr_draw = gr_backend->init(gr_backend);
+        if (!gr_draw) {
+            gr_backend->exit(gr_backend);
+        }
+    }
+
+    if (!gr_draw) {
+        gr_backend = open_drm();
+        gr_draw = gr_backend->init(gr_backend);
+    }
+
+    if (!gr_draw) {
+        gr_backend = open_fbdev();
+        gr_draw = gr_backend->init(gr_backend);
+        if (gr_draw == NULL) {
+            return -1;
+        }
+    }
+
+    overscan_offset_x = gr_draw->width * overscan_percent / 100;
+    overscan_offset_y = gr_draw->height * overscan_percent / 100;
+
+    gr_flip();
+    gr_flip();
+
+    return 0;
+}
+
+void gr_exit(void)
+{
+    gr_backend->exit(gr_backend);
+}
+
+int gr_fb_width(void)
+{
+    return gr_draw->width - 2*overscan_offset_x;
+}
+
+int gr_fb_height(void)
+{
+    return gr_draw->height - 2*overscan_offset_y;
+}
+
+void gr_fb_blank(bool blank)
+{
+    gr_backend->blank(gr_backend, blank);
+}
diff --git a/recovery/minui/graphics.h b/recovery/minui/graphics.h
new file mode 100644
index 0000000..52968eb
--- /dev/null
+++ b/recovery/minui/graphics.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef _GRAPHICS_H_
+#define _GRAPHICS_H_
+
+#include "minui.h"
+
+// TODO: lose the function pointers.
+struct minui_backend {
+    // Initializes the backend and returns a GRSurface* to draw into.
+    GRSurface* (*init)(minui_backend*);
+
+    // Causes the current drawing surface (returned by the most recent
+    // call to flip() or init()) to be displayed, and returns a new
+    // drawing surface.
+    GRSurface* (*flip)(minui_backend*);
+
+    // Blank (or unblank) the screen.
+    void (*blank)(minui_backend*, bool);
+
+    // Device cleanup when drawing is done.
+    void (*exit)(minui_backend*);
+};
+
+minui_backend* open_fbdev();
+minui_backend* open_adf();
+minui_backend* open_drm();
+
+#endif
diff --git a/recovery/minui/graphics_adf.cpp b/recovery/minui/graphics_adf.cpp
new file mode 100644
index 0000000..3c35410
--- /dev/null
+++ b/recovery/minui/graphics_adf.cpp
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/cdefs.h>
+#include <sys/mman.h>
+
+#include <adf/adf.h>
+#include <sync/sync.h>
+
+#include "graphics.h"
+
+struct adf_surface_pdata {
+    GRSurface base;
+    int fence_fd;
+    int fd;
+    __u32 offset;
+    __u32 pitch;
+};
+
+struct adf_pdata {
+    minui_backend base;
+    int intf_fd;
+    adf_id_t eng_id;
+    __u32 format;
+
+    adf_device dev;
+
+    unsigned int current_surface;
+    unsigned int n_surfaces;
+    adf_surface_pdata surfaces[2];
+};
+
+static GRSurface* adf_flip(minui_backend *backend);
+static void adf_blank(minui_backend *backend, bool blank);
+
+static int adf_surface_init(adf_pdata *pdata, drm_mode_modeinfo *mode, adf_surface_pdata *surf) {
+    memset(surf, 0, sizeof(*surf));
+
+    surf->fence_fd = -1;
+    surf->fd = adf_interface_simple_buffer_alloc(pdata->intf_fd, mode->hdisplay,
+            mode->vdisplay, pdata->format, &surf->offset, &surf->pitch);
+    if (surf->fd < 0)
+        return surf->fd;
+
+    surf->base.width = mode->hdisplay;
+    surf->base.height = mode->vdisplay;
+    surf->base.row_bytes = surf->pitch;
+    surf->base.pixel_bytes = (pdata->format == DRM_FORMAT_RGB565) ? 2 : 4;
+
+    surf->base.data = reinterpret_cast<uint8_t*>(mmap(NULL,
+                                                      surf->pitch * surf->base.height, PROT_WRITE,
+                                                      MAP_SHARED, surf->fd, surf->offset));
+    if (surf->base.data == MAP_FAILED) {
+        close(surf->fd);
+        return -errno;
+    }
+
+    return 0;
+}
+
+static int adf_interface_init(adf_pdata *pdata)
+{
+    adf_interface_data intf_data;
+    int ret = 0;
+    int err;
+
+    err = adf_get_interface_data(pdata->intf_fd, &intf_data);
+    if (err < 0)
+        return err;
+
+    err = adf_surface_init(pdata, &intf_data.current_mode, &pdata->surfaces[0]);
+    if (err < 0) {
+        fprintf(stderr, "allocating surface 0 failed: %s\n", strerror(-err));
+        ret = err;
+        goto done;
+    }
+
+    err = adf_surface_init(pdata, &intf_data.current_mode,
+            &pdata->surfaces[1]);
+    if (err < 0) {
+        fprintf(stderr, "allocating surface 1 failed: %s\n", strerror(-err));
+        memset(&pdata->surfaces[1], 0, sizeof(pdata->surfaces[1]));
+        pdata->n_surfaces = 1;
+    } else {
+        pdata->n_surfaces = 2;
+    }
+
+done:
+    adf_free_interface_data(&intf_data);
+    return ret;
+}
+
+static int adf_device_init(adf_pdata *pdata, adf_device *dev)
+{
+    adf_id_t intf_id;
+    int intf_fd;
+    int err;
+
+    err = adf_find_simple_post_configuration(dev, &pdata->format, 1, &intf_id,
+            &pdata->eng_id);
+    if (err < 0)
+        return err;
+
+    err = adf_device_attach(dev, pdata->eng_id, intf_id);
+    if (err < 0 && err != -EALREADY)
+        return err;
+
+    pdata->intf_fd = adf_interface_open(dev, intf_id, O_RDWR);
+    if (pdata->intf_fd < 0)
+        return pdata->intf_fd;
+
+    err = adf_interface_init(pdata);
+    if (err < 0) {
+        close(pdata->intf_fd);
+        pdata->intf_fd = -1;
+    }
+
+    return err;
+}
+
+static GRSurface* adf_init(minui_backend *backend)
+{
+    adf_pdata *pdata = (adf_pdata *)backend;
+    adf_id_t *dev_ids = NULL;
+    ssize_t n_dev_ids, i;
+    GRSurface* ret;
+
+#if defined(RECOVERY_ABGR)
+    pdata->format = DRM_FORMAT_ABGR8888;
+#elif defined(RECOVERY_BGRA)
+    pdata->format = DRM_FORMAT_BGRA8888;
+#elif defined(RECOVERY_RGBX)
+    pdata->format = DRM_FORMAT_RGBX8888;
+#else
+    pdata->format = DRM_FORMAT_RGB565;
+#endif
+
+    n_dev_ids = adf_devices(&dev_ids);
+    if (n_dev_ids == 0) {
+        return NULL;
+    } else if (n_dev_ids < 0) {
+        fprintf(stderr, "enumerating adf devices failed: %s\n",
+                strerror(-n_dev_ids));
+        return NULL;
+    }
+
+    pdata->intf_fd = -1;
+
+    for (i = 0; i < n_dev_ids && pdata->intf_fd < 0; i++) {
+
+        int err = adf_device_open(dev_ids[i], O_RDWR, &pdata->dev);
+        if (err < 0) {
+            fprintf(stderr, "opening adf device %u failed: %s\n", dev_ids[i],
+                    strerror(-err));
+            continue;
+        }
+
+        err = adf_device_init(pdata, &pdata->dev);
+        if (err < 0) {
+            fprintf(stderr, "initializing adf device %u failed: %s\n",
+                    dev_ids[i], strerror(-err));
+            adf_device_close(&pdata->dev);
+        }
+    }
+
+    free(dev_ids);
+
+    if (pdata->intf_fd < 0)
+        return NULL;
+
+    ret = adf_flip(backend);
+
+    adf_blank(backend, true);
+    adf_blank(backend, false);
+
+    return ret;
+}
+
+static void adf_sync(adf_surface_pdata *surf)
+{
+    unsigned int warningTimeout = 3000;
+
+    if (surf == NULL)
+        return;
+
+    if (surf->fence_fd >= 0){
+        int err = sync_wait(surf->fence_fd, warningTimeout);
+        if (err < 0)
+            perror("adf sync fence wait error\n");
+
+        close(surf->fence_fd);
+        surf->fence_fd = -1;
+    }
+}
+
+static GRSurface* adf_flip(minui_backend *backend)
+{
+    adf_pdata *pdata = (adf_pdata *)backend;
+    adf_surface_pdata *surf = &pdata->surfaces[pdata->current_surface];
+
+    int fence_fd = adf_interface_simple_post(pdata->intf_fd, pdata->eng_id,
+            surf->base.width, surf->base.height, pdata->format, surf->fd,
+            surf->offset, surf->pitch, -1);
+    if (fence_fd >= 0)
+        surf->fence_fd = fence_fd;
+
+    pdata->current_surface = (pdata->current_surface + 1) % pdata->n_surfaces;
+    adf_sync(&pdata->surfaces[pdata->current_surface]);
+    return &pdata->surfaces[pdata->current_surface].base;
+}
+
+static void adf_blank(minui_backend *backend, bool blank)
+{
+    adf_pdata *pdata = (adf_pdata *)backend;
+    adf_interface_blank(pdata->intf_fd,
+            blank ? DRM_MODE_DPMS_OFF : DRM_MODE_DPMS_ON);
+}
+
+static void adf_surface_destroy(adf_surface_pdata *surf)
+{
+    munmap(surf->base.data, surf->pitch * surf->base.height);
+    close(surf->fence_fd);
+    close(surf->fd);
+}
+
+static void adf_exit(minui_backend *backend)
+{
+    adf_pdata *pdata = (adf_pdata *)backend;
+    unsigned int i;
+
+    adf_device_close(&pdata->dev);
+    for (i = 0; i < pdata->n_surfaces; i++)
+        adf_surface_destroy(&pdata->surfaces[i]);
+    if (pdata->intf_fd >= 0)
+        close(pdata->intf_fd);
+    free(pdata);
+}
+
+minui_backend *open_adf()
+{
+    adf_pdata* pdata = reinterpret_cast<adf_pdata*>(calloc(1, sizeof(*pdata)));
+    if (!pdata) {
+        perror("allocating adf backend failed");
+        return NULL;
+    }
+
+    pdata->base.init = adf_init;
+    pdata->base.flip = adf_flip;
+    pdata->base.blank = adf_blank;
+    pdata->base.exit = adf_exit;
+    return &pdata->base;
+}
diff --git a/recovery/minui/graphics_drm.cpp b/recovery/minui/graphics_drm.cpp
new file mode 100644
index 0000000..03e33b7
--- /dev/null
+++ b/recovery/minui/graphics_drm.cpp
@@ -0,0 +1,476 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <drm_fourcc.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/cdefs.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+#include "minui.h"
+#include "graphics.h"
+
+#define ARRAY_SIZE(A) (sizeof(A)/sizeof(*(A)))
+
+struct drm_surface {
+    GRSurface base;
+    uint32_t fb_id;
+    uint32_t handle;
+};
+
+static drm_surface *drm_surfaces[2];
+static int current_buffer;
+
+static drmModeCrtc *main_monitor_crtc;
+static drmModeConnector *main_monitor_connector;
+
+static int drm_fd = -1;
+
+static void drm_disable_crtc(int drm_fd, drmModeCrtc *crtc) {
+    if (crtc) {
+        drmModeSetCrtc(drm_fd, crtc->crtc_id,
+                       0, // fb_id
+                       0, 0,  // x,y
+                       NULL,  // connectors
+                       0,     // connector_count
+                       NULL); // mode
+    }
+}
+
+static void drm_enable_crtc(int drm_fd, drmModeCrtc *crtc,
+                            struct drm_surface *surface) {
+    int32_t ret;
+
+    ret = drmModeSetCrtc(drm_fd, crtc->crtc_id,
+                         surface->fb_id,
+                         0, 0,  // x,y
+                         &main_monitor_connector->connector_id,
+                         1,  // connector_count
+                         &main_monitor_crtc->mode);
+
+    if (ret)
+        printf("drmModeSetCrtc failed ret=%d\n", ret);
+}
+
+static void drm_blank(minui_backend* backend __unused, bool blank) {
+    if (blank)
+        drm_disable_crtc(drm_fd, main_monitor_crtc);
+    else
+        drm_enable_crtc(drm_fd, main_monitor_crtc,
+                        drm_surfaces[current_buffer]);
+}
+
+static void drm_destroy_surface(struct drm_surface *surface) {
+    struct drm_gem_close gem_close;
+    int ret;
+
+    if(!surface)
+        return;
+
+    if (surface->base.data)
+        munmap(surface->base.data,
+               surface->base.row_bytes * surface->base.height);
+
+    if (surface->fb_id) {
+        ret = drmModeRmFB(drm_fd, surface->fb_id);
+        if (ret)
+            printf("drmModeRmFB failed ret=%d\n", ret);
+    }
+
+    if (surface->handle) {
+        memset(&gem_close, 0, sizeof(gem_close));
+        gem_close.handle = surface->handle;
+
+        ret = drmIoctl(drm_fd, DRM_IOCTL_GEM_CLOSE, &gem_close);
+        if (ret)
+            printf("DRM_IOCTL_GEM_CLOSE failed ret=%d\n", ret);
+    }
+
+    free(surface);
+}
+
+static int drm_format_to_bpp(uint32_t format) {
+    switch(format) {
+        case DRM_FORMAT_ABGR8888:
+        case DRM_FORMAT_BGRA8888:
+        case DRM_FORMAT_RGBX8888:
+        case DRM_FORMAT_BGRX8888:
+        case DRM_FORMAT_XBGR8888:
+        case DRM_FORMAT_XRGB8888:
+            return 32;
+        case DRM_FORMAT_RGB565:
+            return 16;
+        default:
+            printf("Unknown format %d\n", format);
+            return 32;
+    }
+}
+
+static drm_surface *drm_create_surface(int width, int height) {
+    struct drm_surface *surface;
+    struct drm_mode_create_dumb create_dumb;
+    uint32_t format;
+    int ret;
+
+    surface = (struct drm_surface*)calloc(1, sizeof(*surface));
+    if (!surface) {
+        printf("Can't allocate memory\n");
+        return NULL;
+    }
+
+#if defined(RECOVERY_ABGR)
+    format = DRM_FORMAT_RGBA8888;
+#elif defined(RECOVERY_BGRA)
+    format = DRM_FORMAT_ARGB8888;
+#elif defined(RECOVERY_RGBX)
+    format = DRM_FORMAT_XBGR8888;
+#else
+    format = DRM_FORMAT_RGB565;
+#endif
+
+    memset(&create_dumb, 0, sizeof(create_dumb));
+    create_dumb.height = height;
+    create_dumb.width = width;
+    create_dumb.bpp = drm_format_to_bpp(format);
+    create_dumb.flags = 0;
+
+    ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
+    if (ret) {
+        printf("DRM_IOCTL_MODE_CREATE_DUMB failed ret=%d\n",ret);
+        drm_destroy_surface(surface);
+        return NULL;
+    }
+    surface->handle = create_dumb.handle;
+
+    uint32_t handles[4], pitches[4], offsets[4];
+
+    handles[0] = surface->handle;
+    pitches[0] = create_dumb.pitch;
+    offsets[0] = 0;
+
+    ret = drmModeAddFB2(drm_fd, width, height,
+            format, handles, pitches, offsets,
+            &(surface->fb_id), 0);
+    if (ret) {
+        printf("drmModeAddFB2 failed ret=%d\n", ret);
+        drm_destroy_surface(surface);
+        return NULL;
+    }
+
+    struct drm_mode_map_dumb map_dumb;
+    memset(&map_dumb, 0, sizeof(map_dumb));
+    map_dumb.handle = create_dumb.handle;
+    ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb);
+    if (ret) {
+        printf("DRM_IOCTL_MODE_MAP_DUMB failed ret=%d\n",ret);
+        drm_destroy_surface(surface);
+        return NULL;;
+    }
+
+    surface->base.height = height;
+    surface->base.width = width;
+    surface->base.row_bytes = create_dumb.pitch;
+    surface->base.pixel_bytes = create_dumb.bpp / 8;
+    surface->base.data = (unsigned char*)
+                         mmap(NULL,
+                              surface->base.height * surface->base.row_bytes,
+                              PROT_READ | PROT_WRITE, MAP_SHARED,
+                              drm_fd, map_dumb.offset);
+    if (surface->base.data == MAP_FAILED) {
+        perror("mmap() failed");
+        drm_destroy_surface(surface);
+        return NULL;
+    }
+
+    return surface;
+}
+
+static drmModeCrtc *find_crtc_for_connector(int fd,
+                            drmModeRes *resources,
+                            drmModeConnector *connector) {
+    int i, j;
+    drmModeEncoder *encoder;
+    int32_t crtc;
+
+    /*
+     * Find the encoder. If we already have one, just use it.
+     */
+    if (connector->encoder_id)
+        encoder = drmModeGetEncoder(fd, connector->encoder_id);
+    else
+        encoder = NULL;
+
+    if (encoder && encoder->crtc_id) {
+        crtc = encoder->crtc_id;
+        drmModeFreeEncoder(encoder);
+        return drmModeGetCrtc(fd, crtc);
+    }
+
+    /*
+     * Didn't find anything, try to find a crtc and encoder combo.
+     */
+    crtc = -1;
+    for (i = 0; i < connector->count_encoders; i++) {
+        encoder = drmModeGetEncoder(fd, connector->encoders[i]);
+
+        if (encoder) {
+            for (j = 0; j < resources->count_crtcs; j++) {
+                if (!(encoder->possible_crtcs & (1 << j)))
+                    continue;
+                crtc = resources->crtcs[j];
+                break;
+            }
+            if (crtc >= 0) {
+                drmModeFreeEncoder(encoder);
+                return drmModeGetCrtc(fd, crtc);
+            }
+        }
+    }
+
+    return NULL;
+}
+
+static drmModeConnector *find_used_connector_by_type(int fd,
+                                 drmModeRes *resources,
+                                 unsigned type) {
+    int i;
+    for (i = 0; i < resources->count_connectors; i++) {
+        drmModeConnector *connector;
+
+        connector = drmModeGetConnector(fd, resources->connectors[i]);
+        if (connector) {
+            if ((connector->connector_type == type) &&
+                    (connector->connection == DRM_MODE_CONNECTED) &&
+                    (connector->count_modes > 0))
+                return connector;
+
+            drmModeFreeConnector(connector);
+        }
+    }
+    return NULL;
+}
+
+static drmModeConnector *find_first_connected_connector(int fd,
+                             drmModeRes *resources) {
+    int i;
+    for (i = 0; i < resources->count_connectors; i++) {
+        drmModeConnector *connector;
+
+        connector = drmModeGetConnector(fd, resources->connectors[i]);
+        if (connector) {
+            if ((connector->count_modes > 0) &&
+                    (connector->connection == DRM_MODE_CONNECTED))
+                return connector;
+
+            drmModeFreeConnector(connector);
+        }
+    }
+    return NULL;
+}
+
+static drmModeConnector *find_main_monitor(int fd, drmModeRes *resources,
+        uint32_t *mode_index) {
+    unsigned i = 0;
+    int modes;
+    /* Look for LVDS/eDP/DSI connectors. Those are the main screens. */
+    unsigned kConnectorPriority[] = {
+        DRM_MODE_CONNECTOR_LVDS,
+        DRM_MODE_CONNECTOR_eDP,
+        DRM_MODE_CONNECTOR_DSI,
+    };
+
+    drmModeConnector *main_monitor_connector = NULL;
+    do {
+        main_monitor_connector = find_used_connector_by_type(fd,
+                                         resources,
+                                         kConnectorPriority[i]);
+        i++;
+    } while (!main_monitor_connector && i < ARRAY_SIZE(kConnectorPriority));
+
+    /* If we didn't find a connector, grab the first one that is connected. */
+    if (!main_monitor_connector)
+        main_monitor_connector =
+                find_first_connected_connector(fd, resources);
+
+    /* If we still didn't find a connector, give up and return. */
+    if (!main_monitor_connector)
+        return NULL;
+
+    *mode_index = 0;
+    for (modes = 0; modes < main_monitor_connector->count_modes; modes++) {
+        if (main_monitor_connector->modes[modes].type &
+                DRM_MODE_TYPE_PREFERRED) {
+            *mode_index = modes;
+            break;
+        }
+    }
+
+    return main_monitor_connector;
+}
+
+static void disable_non_main_crtcs(int fd,
+                    drmModeRes *resources,
+                    drmModeCrtc* main_crtc) {
+    int i;
+    drmModeCrtc* crtc;
+
+    for (i = 0; i < resources->count_connectors; i++) {
+        drmModeConnector *connector;
+
+        connector = drmModeGetConnector(fd, resources->connectors[i]);
+        crtc = find_crtc_for_connector(fd, resources, connector);
+        if (crtc->crtc_id != main_crtc->crtc_id)
+            drm_disable_crtc(fd, crtc);
+        drmModeFreeCrtc(crtc);
+    }
+}
+
+static GRSurface* drm_init(minui_backend* backend __unused) {
+    drmModeRes *res = NULL;
+    uint32_t selected_mode;
+    char *dev_name;
+    int width, height;
+    int ret, i;
+
+    /* Consider DRM devices in order. */
+    for (i = 0; i < DRM_MAX_MINOR; i++) {
+        uint64_t cap = 0;
+
+        ret = asprintf(&dev_name, DRM_DEV_NAME, DRM_DIR_NAME, i);
+        if (ret < 0)
+            continue;
+
+        drm_fd = open(dev_name, O_RDWR, 0);
+        free(dev_name);
+        if (drm_fd < 0)
+            continue;
+
+        /* We need dumb buffers. */
+        ret = drmGetCap(drm_fd, DRM_CAP_DUMB_BUFFER, &cap);
+        if (ret || cap == 0) {
+            close(drm_fd);
+            continue;
+        }
+
+        res = drmModeGetResources(drm_fd);
+        if (!res) {
+            close(drm_fd);
+            continue;
+        }
+
+        /* Use this device if it has at least one connected monitor. */
+        if (res->count_crtcs > 0 && res->count_connectors > 0)
+            if (find_first_connected_connector(drm_fd, res))
+                break;
+
+        drmModeFreeResources(res);
+        close(drm_fd);
+        res = NULL;
+    }
+
+    if (drm_fd < 0 || res == NULL) {
+        perror("cannot find/open a drm device");
+        return NULL;
+    }
+
+    main_monitor_connector = find_main_monitor(drm_fd,
+            res, &selected_mode);
+
+    if (!main_monitor_connector) {
+        printf("main_monitor_connector not found\n");
+        drmModeFreeResources(res);
+        close(drm_fd);
+        return NULL;
+    }
+
+    main_monitor_crtc = find_crtc_for_connector(drm_fd, res,
+                                                main_monitor_connector);
+
+    if (!main_monitor_crtc) {
+        printf("main_monitor_crtc not found\n");
+        drmModeFreeResources(res);
+        close(drm_fd);
+        return NULL;
+    }
+
+    disable_non_main_crtcs(drm_fd,
+                           res, main_monitor_crtc);
+
+    main_monitor_crtc->mode = main_monitor_connector->modes[selected_mode];
+
+    width = main_monitor_crtc->mode.hdisplay;
+    height = main_monitor_crtc->mode.vdisplay;
+
+    drmModeFreeResources(res);
+
+    drm_surfaces[0] = drm_create_surface(width, height);
+    drm_surfaces[1] = drm_create_surface(width, height);
+    if (!drm_surfaces[0] || !drm_surfaces[1]) {
+        drm_destroy_surface(drm_surfaces[0]);
+        drm_destroy_surface(drm_surfaces[1]);
+        drmModeFreeResources(res);
+        close(drm_fd);
+        return NULL;
+    }
+
+    current_buffer = 0;
+
+    drm_enable_crtc(drm_fd, main_monitor_crtc, drm_surfaces[1]);
+
+    return &(drm_surfaces[0]->base);
+}
+
+static GRSurface* drm_flip(minui_backend* backend __unused) {
+    int ret;
+
+    ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id,
+                          drm_surfaces[current_buffer]->fb_id, 0, NULL);
+    if (ret < 0) {
+        printf("drmModePageFlip failed ret=%d\n", ret);
+        return NULL;
+    }
+    current_buffer = 1 - current_buffer;
+    return &(drm_surfaces[current_buffer]->base);
+}
+
+static void drm_exit(minui_backend* backend __unused) {
+    drm_disable_crtc(drm_fd, main_monitor_crtc);
+    drm_destroy_surface(drm_surfaces[0]);
+    drm_destroy_surface(drm_surfaces[1]);
+    drmModeFreeCrtc(main_monitor_crtc);
+    drmModeFreeConnector(main_monitor_connector);
+    close(drm_fd);
+    drm_fd = -1;
+}
+
+static minui_backend drm_backend = {
+    .init = drm_init,
+    .flip = drm_flip,
+    .blank = drm_blank,
+    .exit = drm_exit,
+};
+
+minui_backend* open_drm() {
+    return &drm_backend;
+}
diff --git a/recovery/minui/graphics_fbdev.cpp b/recovery/minui/graphics_fbdev.cpp
new file mode 100644
index 0000000..0788f75
--- /dev/null
+++ b/recovery/minui/graphics_fbdev.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <stdio.h>
+
+#include <sys/cdefs.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <linux/fb.h>
+#include <linux/kd.h>
+
+#include "minui.h"
+#include "graphics.h"
+
+static GRSurface* fbdev_init(minui_backend*);
+static GRSurface* fbdev_flip(minui_backend*);
+static void fbdev_blank(minui_backend*, bool);
+static void fbdev_exit(minui_backend*);
+
+static GRSurface gr_framebuffer[2];
+static bool double_buffered;
+static GRSurface* gr_draw = NULL;
+static int displayed_buffer;
+
+static fb_var_screeninfo vi;
+static int fb_fd = -1;
+
+static minui_backend my_backend = {
+    .init = fbdev_init,
+    .flip = fbdev_flip,
+    .blank = fbdev_blank,
+    .exit = fbdev_exit,
+};
+
+minui_backend* open_fbdev() {
+    return &my_backend;
+}
+
+static void fbdev_blank(minui_backend* backend __unused, bool blank)
+{
+    int ret;
+
+    ret = ioctl(fb_fd, FBIOBLANK, blank ? FB_BLANK_POWERDOWN : FB_BLANK_UNBLANK);
+    if (ret < 0)
+        perror("ioctl(): blank");
+}
+
+static void set_displayed_framebuffer(unsigned n)
+{
+    if (n > 1 || !double_buffered) return;
+
+    vi.yres_virtual = gr_framebuffer[0].height * 2;
+    vi.yoffset = n * gr_framebuffer[0].height;
+    vi.bits_per_pixel = gr_framebuffer[0].pixel_bytes * 8;
+    if (ioctl(fb_fd, FBIOPUT_VSCREENINFO, &vi) < 0) {
+        perror("active fb swap failed");
+    }
+    displayed_buffer = n;
+}
+
+static GRSurface* fbdev_init(minui_backend* backend) {
+    int fd = open("/dev/graphics/fb0", O_RDWR);
+    if (fd == -1) {
+        perror("cannot open fb0");
+        return NULL;
+    }
+
+    fb_fix_screeninfo fi;
+    if (ioctl(fd, FBIOGET_FSCREENINFO, &fi) < 0) {
+        perror("failed to get fb0 info");
+        close(fd);
+        return NULL;
+    }
+
+    if (ioctl(fd, FBIOGET_VSCREENINFO, &vi) < 0) {
+        perror("failed to get fb0 info");
+        close(fd);
+        return NULL;
+    }
+
+    // We print this out for informational purposes only, but
+    // throughout we assume that the framebuffer device uses an RGBX
+    // pixel format.  This is the case for every development device I
+    // have access to.  For some of those devices (eg, hammerhead aka
+    // Nexus 5), FBIOGET_VSCREENINFO *reports* that it wants a
+    // different format (XBGR) but actually produces the correct
+    // results on the display when you write RGBX.
+    //
+    // If you have a device that actually *needs* another pixel format
+    // (ie, BGRX, or 565), patches welcome...
+
+    printf("fb0 reports (possibly inaccurate):\n"
+           "  vi.bits_per_pixel = %d\n"
+           "  vi.red.offset   = %3d   .length = %3d\n"
+           "  vi.green.offset = %3d   .length = %3d\n"
+           "  vi.blue.offset  = %3d   .length = %3d\n",
+           vi.bits_per_pixel,
+           vi.red.offset, vi.red.length,
+           vi.green.offset, vi.green.length,
+           vi.blue.offset, vi.blue.length);
+
+    void* bits = mmap(0, fi.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+    if (bits == MAP_FAILED) {
+        perror("failed to mmap framebuffer");
+        close(fd);
+        return NULL;
+    }
+
+    memset(bits, 0, fi.smem_len);
+
+    gr_framebuffer[0].width = vi.xres;
+    gr_framebuffer[0].height = vi.yres;
+    gr_framebuffer[0].row_bytes = fi.line_length;
+    gr_framebuffer[0].pixel_bytes = vi.bits_per_pixel / 8;
+    gr_framebuffer[0].data = reinterpret_cast<uint8_t*>(bits);
+    memset(gr_framebuffer[0].data, 0, gr_framebuffer[0].height * gr_framebuffer[0].row_bytes);
+
+    /* check if we can use double buffering */
+    if (vi.yres * fi.line_length * 2 <= fi.smem_len) {
+        double_buffered = true;
+
+        memcpy(gr_framebuffer+1, gr_framebuffer, sizeof(GRSurface));
+        gr_framebuffer[1].data = gr_framebuffer[0].data +
+            gr_framebuffer[0].height * gr_framebuffer[0].row_bytes;
+
+        gr_draw = gr_framebuffer+1;
+
+    } else {
+        double_buffered = false;
+
+        // Without double-buffering, we allocate RAM for a buffer to
+        // draw in, and then "flipping" the buffer consists of a
+        // memcpy from the buffer we allocated to the framebuffer.
+
+        gr_draw = (GRSurface*) malloc(sizeof(GRSurface));
+        memcpy(gr_draw, gr_framebuffer, sizeof(GRSurface));
+        gr_draw->data = (unsigned char*) malloc(gr_draw->height * gr_draw->row_bytes);
+        if (!gr_draw->data) {
+            perror("failed to allocate in-memory surface");
+            return NULL;
+        }
+    }
+
+    memset(gr_draw->data, 0, gr_draw->height * gr_draw->row_bytes);
+    fb_fd = fd;
+    set_displayed_framebuffer(0);
+
+    printf("framebuffer: %d (%d x %d)\n", fb_fd, gr_draw->width, gr_draw->height);
+
+    fbdev_blank(backend, true);
+    fbdev_blank(backend, false);
+
+    return gr_draw;
+}
+
+static GRSurface* fbdev_flip(minui_backend* backend __unused) {
+    if (double_buffered) {
+        // Change gr_draw to point to the buffer currently displayed,
+        // then flip the driver so we're displaying the other buffer
+        // instead.
+        gr_draw = gr_framebuffer + displayed_buffer;
+        set_displayed_framebuffer(1-displayed_buffer);
+    } else {
+        // Copy from the in-memory surface to the framebuffer.
+        memcpy(gr_framebuffer[0].data, gr_draw->data,
+               gr_draw->height * gr_draw->row_bytes);
+    }
+    return gr_draw;
+}
+
+static void fbdev_exit(minui_backend* backend __unused) {
+    close(fb_fd);
+    fb_fd = -1;
+
+    if (!double_buffered && gr_draw) {
+        free(gr_draw->data);
+        free(gr_draw);
+    }
+    gr_draw = NULL;
+}
diff --git a/recovery/minui/minui.h b/recovery/minui/minui.h
new file mode 100644
index 0000000..d572205
--- /dev/null
+++ b/recovery/minui/minui.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef _MINUI_H_
+#define _MINUI_H_
+
+#include <sys/types.h>
+
+#include <functional>
+
+//
+// Graphics.
+//
+
+struct GRSurface {
+    int width;
+    int height;
+    int row_bytes;
+    int pixel_bytes;
+    unsigned char* data;
+};
+
+int gr_init();
+void gr_exit();
+
+int gr_fb_width();
+int gr_fb_height();
+
+void gr_flip();
+void gr_fb_blank(bool blank);
+
+void gr_clear();  // clear entire surface to current color
+void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
+void gr_fill(int x1, int y1, int x2, int y2);
+void gr_text(int x, int y, const char *s, bool bold);
+void gr_texticon(int x, int y, GRSurface* icon);
+int gr_measure(const char *s);
+void gr_font_size(int *x, int *y);
+
+void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy);
+unsigned int gr_get_width(GRSurface* surface);
+unsigned int gr_get_height(GRSurface* surface);
+
+//
+// Input events.
+//
+
+struct input_event;
+
+// TODO: move these over to std::function.
+typedef int (*ev_callback)(int fd, uint32_t epevents, void* data);
+typedef int (*ev_set_key_callback)(int code, int value, void* data);
+
+int ev_init(ev_callback input_cb, void* data);
+void ev_exit();
+int ev_add_fd(int fd, ev_callback cb, void* data);
+void ev_iterate_available_keys(const std::function<void(int)>& f);
+int ev_sync_key_state(ev_set_key_callback set_key_cb, void* data);
+
+// 'timeout' has the same semantics as poll(2).
+//    0 : don't block
+//  < 0 : block forever
+//  > 0 : block for 'timeout' milliseconds
+int ev_wait(int timeout);
+
+int ev_get_input(int fd, uint32_t epevents, input_event* ev);
+void ev_dispatch();
+int ev_get_epollfd();
+
+//
+// Resources
+//
+
+// res_create_*_surface() functions return 0 if no error, else
+// negative.
+//
+// A "display" surface is one that is intended to be drawn to the
+// screen with gr_blit().  An "alpha" surface is a grayscale image
+// interpreted as an alpha mask used to render text in the current
+// color (with gr_text() or gr_texticon()).
+//
+// All these functions load PNG images from "/res/images/${name}.png".
+
+// Load a single display surface from a PNG image.
+int res_create_display_surface(const char* name, GRSurface** pSurface);
+
+// Load an array of display surfaces from a single PNG image.  The PNG
+// should have a 'Frames' text chunk whose value is the number of
+// frames this image represents.  The pixel data itself is interlaced
+// by row.
+int res_create_multi_display_surface(const char* name, int* frames,
+                                     int* fps, GRSurface*** pSurface);
+
+// Load a single alpha surface from a grayscale PNG image.
+int res_create_alpha_surface(const char* name, GRSurface** pSurface);
+
+// Load part of a grayscale PNG image that is the first match for the
+// given locale.  The image is expected to be a composite of multiple
+// translations of the same text, with special added rows that encode
+// the subimages' size and intended locale in the pixel data.  See
+// bootable/recovery/tools/recovery_l10n for an app that will generate
+// these specialized images from Android resources.
+int res_create_localized_alpha_surface(const char* name, const char* locale,
+                                       GRSurface** pSurface);
+
+// Free a surface allocated by any of the res_create_*_surface()
+// functions.
+void res_free_surface(GRSurface* surface);
+
+#endif
diff --git a/recovery/minui/mkfont.c b/recovery/minui/mkfont.c
new file mode 100644
index 0000000..61a5ede
--- /dev/null
+++ b/recovery/minui/mkfont.c
@@ -0,0 +1,54 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char *argv)
+{
+    unsigned n;
+    unsigned char *x;
+    unsigned m;
+    unsigned run_val;
+    unsigned run_count;
+ 
+    n = gimp_image.width * gimp_image.height;
+    m = 0;
+    x = gimp_image.pixel_data;
+
+    printf("struct {\n");
+    printf("  unsigned width;\n");
+    printf("  unsigned height;\n");
+    printf("  unsigned cwidth;\n");
+    printf("  unsigned cheight;\n");
+    printf("  unsigned char rundata[];\n");
+    printf("} font = {\n");
+    printf("  .width = %d,\n  .height = %d,\n  .cwidth = %d,\n  .cheight = %d,\n", gimp_image.width, gimp_image.height,
+           gimp_image.width / 96, gimp_image.height);
+    printf("  .rundata = {\n");
+   
+    run_val = (*x ? 0 : 255);
+    run_count = 1;
+    n--;
+    x+=3;
+
+    while(n-- > 0) {
+        unsigned val = (*x ? 0 : 255);
+        x+=3;
+        if((val == run_val) && (run_count < 127)) {
+            run_count++;
+        } else {
+eject:
+            printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
+            run_val = val;
+            run_count = 1;
+            m += 5;
+            if(m >= 75) {
+                printf("\n");
+                m = 0;
+            }
+        }
+    }
+    printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
+    printf("\n0x00,");
+    printf("\n");
+    printf("  }\n};\n");
+    return 0;
+}
diff --git a/recovery/minui/resources.cpp b/recovery/minui/resources.cpp
new file mode 100644
index 0000000..8489d60
--- /dev/null
+++ b/recovery/minui/resources.cpp
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <stdio.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <linux/fb.h>
+#include <linux/kd.h>
+
+#include <vector>
+#include <png.h>
+
+#include "minui.h"
+
+extern char* locale;
+
+#define SURFACE_DATA_ALIGNMENT 8
+
+static GRSurface* malloc_surface(size_t data_size) {
+    size_t size = sizeof(GRSurface) + data_size + SURFACE_DATA_ALIGNMENT;
+    unsigned char* temp = reinterpret_cast<unsigned char*>(malloc(size));
+    if (temp == NULL) return NULL;
+    GRSurface* surface = reinterpret_cast<GRSurface*>(temp);
+    surface->data = temp + sizeof(GRSurface) +
+        (SURFACE_DATA_ALIGNMENT - (sizeof(GRSurface) % SURFACE_DATA_ALIGNMENT));
+    return surface;
+}
+
+static int open_png(const char* name, png_structp* png_ptr, png_infop* info_ptr,
+                    png_uint_32* width, png_uint_32* height, png_byte* channels) {
+    char resPath[256];
+    unsigned char header[8];
+    int result = 0;
+    int color_type, bit_depth;
+    size_t bytesRead;
+
+    snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.png", name);
+    resPath[sizeof(resPath)-1] = '\0';
+    FILE* fp = fopen(resPath, "rb");
+    if (fp == NULL) {
+        result = -1;
+        goto exit;
+    }
+
+    bytesRead = fread(header, 1, sizeof(header), fp);
+    if (bytesRead != sizeof(header)) {
+        result = -2;
+        goto exit;
+    }
+
+    if (png_sig_cmp(header, 0, sizeof(header))) {
+        result = -3;
+        goto exit;
+    }
+
+    *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+    if (!*png_ptr) {
+        result = -4;
+        goto exit;
+    }
+
+    *info_ptr = png_create_info_struct(*png_ptr);
+    if (!*info_ptr) {
+        result = -5;
+        goto exit;
+    }
+
+    if (setjmp(png_jmpbuf(*png_ptr))) {
+        result = -6;
+        goto exit;
+    }
+
+    png_init_io(*png_ptr, fp);
+    png_set_sig_bytes(*png_ptr, sizeof(header));
+    png_read_info(*png_ptr, *info_ptr);
+
+    png_get_IHDR(*png_ptr, *info_ptr, width, height, &bit_depth,
+            &color_type, NULL, NULL, NULL);
+
+    *channels = png_get_channels(*png_ptr, *info_ptr);
+
+    if (bit_depth == 8 && *channels == 3 && color_type == PNG_COLOR_TYPE_RGB) {
+        // 8-bit RGB images: great, nothing to do.
+    } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_GRAY) {
+        // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
+        png_set_expand_gray_1_2_4_to_8(*png_ptr);
+    } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_PALETTE) {
+        // paletted images: expand to 8-bit RGB.  Note that we DON'T
+        // currently expand the tRNS chunk (if any) to an alpha
+        // channel, because minui doesn't support alpha channels in
+        // general.
+        png_set_palette_to_rgb(*png_ptr);
+        *channels = 3;
+    } else {
+        fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n",
+                bit_depth, *channels, color_type);
+        result = -7;
+        goto exit;
+    }
+
+    return result;
+
+  exit:
+    if (result < 0) {
+        png_destroy_read_struct(png_ptr, info_ptr, NULL);
+    }
+    if (fp != NULL) {
+        fclose(fp);
+    }
+
+    return result;
+}
+
+// "display" surfaces are transformed into the framebuffer's required
+// pixel format (currently only RGBX is supported) at load time, so
+// gr_blit() can be nothing more than a memcpy() for each row.  The
+// next two functions are the only ones that know anything about the
+// framebuffer pixel format; they need to be modified if the
+// framebuffer format changes (but nothing else should).
+
+// Allocate and return a GRSurface* sufficient for storing an image of
+// the indicated size in the framebuffer pixel format.
+static GRSurface* init_display_surface(png_uint_32 width, png_uint_32 height) {
+    GRSurface* surface = malloc_surface(width * height * 4);
+    if (surface == NULL) return NULL;
+
+    surface->width = width;
+    surface->height = height;
+    surface->row_bytes = width * 4;
+    surface->pixel_bytes = 4;
+
+    return surface;
+}
+
+// Copy 'input_row' to 'output_row', transforming it to the
+// framebuffer pixel format.  The input format depends on the value of
+// 'channels':
+//
+//   1 - input is 8-bit grayscale
+//   3 - input is 24-bit RGB
+//   4 - input is 32-bit RGBA/RGBX
+//
+// 'width' is the number of pixels in the row.
+static void transform_rgb_to_draw(unsigned char* input_row,
+                                  unsigned char* output_row,
+                                  int channels, int width) {
+    int x;
+    unsigned char* ip = input_row;
+    unsigned char* op = output_row;
+
+    switch (channels) {
+        case 1:
+            // expand gray level to RGBX
+            for (x = 0; x < width; ++x) {
+                *op++ = *ip;
+                *op++ = *ip;
+                *op++ = *ip;
+                *op++ = 0xff;
+                ip++;
+            }
+            break;
+
+        case 3:
+            // expand RGBA to RGBX
+            for (x = 0; x < width; ++x) {
+                *op++ = *ip++;
+                *op++ = *ip++;
+                *op++ = *ip++;
+                *op++ = 0xff;
+            }
+            break;
+
+        case 4:
+            // copy RGBA to RGBX
+            memcpy(output_row, input_row, width*4);
+            break;
+    }
+}
+
+int res_create_display_surface(const char* name, GRSurface** pSurface) {
+    GRSurface* surface = NULL;
+    int result = 0;
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    png_uint_32 width, height;
+    png_byte channels;
+    unsigned char* p_row;
+    unsigned int y;
+
+    *pSurface = NULL;
+
+    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
+    if (result < 0) return result;
+
+    surface = init_display_surface(width, height);
+    if (surface == NULL) {
+        result = -8;
+        goto exit;
+    }
+
+#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
+    png_set_bgr(png_ptr);
+#endif
+
+    p_row = reinterpret_cast<unsigned char*>(malloc(width * 4));
+    for (y = 0; y < height; ++y) {
+        png_read_row(png_ptr, p_row, NULL);
+        transform_rgb_to_draw(p_row, surface->data + y * surface->row_bytes, channels, width);
+    }
+    free(p_row);
+
+    *pSurface = surface;
+
+  exit:
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    if (result < 0 && surface != NULL) free(surface);
+    return result;
+}
+
+int res_create_multi_display_surface(const char* name, int* frames, int* fps,
+        GRSurface*** pSurface) {
+    GRSurface** surface = NULL;
+    int result = 0;
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    png_uint_32 width, height;
+    png_byte channels;
+    png_textp text;
+    int num_text;
+    unsigned char* p_row;
+    unsigned int y;
+
+    *pSurface = NULL;
+    *frames = -1;
+
+    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
+    if (result < 0) return result;
+
+    *frames = 1;
+    *fps = 20;
+    if (png_get_text(png_ptr, info_ptr, &text, &num_text)) {
+        for (int i = 0; i < num_text; ++i) {
+            if (text[i].key && strcmp(text[i].key, "Frames") == 0 && text[i].text) {
+                *frames = atoi(text[i].text);
+            } else if (text[i].key && strcmp(text[i].key, "FPS") == 0 && text[i].text) {
+                *fps = atoi(text[i].text);
+            }
+        }
+        printf("  found frames = %d\n", *frames);
+        printf("  found fps = %d\n", *fps);
+    }
+
+    if (frames <= 0 || fps <= 0) {
+        printf("bad number of frames (%d) and/or FPS (%d)\n", *frames, *fps);
+        result = -10;
+        goto exit;
+    }
+
+    if (height % *frames != 0) {
+        printf("bad height (%d) for frame count (%d)\n", height, *frames);
+        result = -9;
+        goto exit;
+    }
+
+    surface = reinterpret_cast<GRSurface**>(calloc(*frames, sizeof(GRSurface*)));
+    if (surface == NULL) {
+        result = -8;
+        goto exit;
+    }
+    for (int i = 0; i < *frames; ++i) {
+        surface[i] = init_display_surface(width, height / *frames);
+        if (surface[i] == NULL) {
+            result = -8;
+            goto exit;
+        }
+    }
+
+#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
+    png_set_bgr(png_ptr);
+#endif
+
+    p_row = reinterpret_cast<unsigned char*>(malloc(width * 4));
+    for (y = 0; y < height; ++y) {
+        png_read_row(png_ptr, p_row, NULL);
+        int frame = y % *frames;
+        unsigned char* out_row = surface[frame]->data +
+            (y / *frames) * surface[frame]->row_bytes;
+        transform_rgb_to_draw(p_row, out_row, channels, width);
+    }
+    free(p_row);
+
+    *pSurface = reinterpret_cast<GRSurface**>(surface);
+
+exit:
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+
+    if (result < 0) {
+        if (surface) {
+            for (int i = 0; i < *frames; ++i) {
+                free(surface[i]);
+            }
+            free(surface);
+        }
+    }
+    return result;
+}
+
+int res_create_alpha_surface(const char* name, GRSurface** pSurface) {
+    GRSurface* surface = NULL;
+    int result = 0;
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    png_uint_32 width, height;
+    png_byte channels;
+
+    *pSurface = NULL;
+
+    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
+    if (result < 0) return result;
+
+    if (channels != 1) {
+        result = -7;
+        goto exit;
+    }
+
+    surface = malloc_surface(width * height);
+    if (surface == NULL) {
+        result = -8;
+        goto exit;
+    }
+    surface->width = width;
+    surface->height = height;
+    surface->row_bytes = width;
+    surface->pixel_bytes = 1;
+
+#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
+    png_set_bgr(png_ptr);
+#endif
+
+    unsigned char* p_row;
+    unsigned int y;
+    for (y = 0; y < height; ++y) {
+        p_row = surface->data + y * surface->row_bytes;
+        png_read_row(png_ptr, p_row, NULL);
+    }
+
+    *pSurface = surface;
+
+  exit:
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    if (result < 0 && surface != NULL) free(surface);
+    return result;
+}
+
+static int matches_locale(const char* loc, const char* locale) {
+    if (locale == NULL) return 0;
+
+    if (strcmp(loc, locale) == 0) return 1;
+
+    // if loc does *not* have an underscore, and it matches the start
+    // of locale, and the next character in locale *is* an underscore,
+    // that's a match.  For instance, loc == "en" matches locale ==
+    // "en_US".
+
+    int i;
+    for (i = 0; loc[i] != 0 && loc[i] != '_'; ++i);
+    if (loc[i] == '_') return 0;
+
+    return (strncmp(locale, loc, i) == 0 && locale[i] == '_');
+}
+
+int res_create_localized_alpha_surface(const char* name,
+                                       const char* locale,
+                                       GRSurface** pSurface) {
+    GRSurface* surface = NULL;
+    int result = 0;
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    png_uint_32 width, height;
+    png_byte channels;
+    png_uint_32 y;
+    std::vector<unsigned char> row;
+
+    *pSurface = NULL;
+
+    if (locale == NULL) {
+        return result;
+    }
+
+    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
+    if (result < 0) return result;
+
+    if (channels != 1) {
+        result = -7;
+        goto exit;
+    }
+
+    row.resize(width);
+    for (y = 0; y < height; ++y) {
+        png_read_row(png_ptr, row.data(), NULL);
+        int w = (row[1] << 8) | row[0];
+        int h = (row[3] << 8) | row[2];
+        __unused int len = row[4];
+        char* loc = reinterpret_cast<char*>(&row[5]);
+
+        if (y+1+h >= height || matches_locale(loc, locale)) {
+            printf("  %20s: %s (%d x %d @ %d)\n", name, loc, w, h, y);
+
+            surface = malloc_surface(w*h);
+            if (surface == NULL) {
+                result = -8;
+                goto exit;
+            }
+            surface->width = w;
+            surface->height = h;
+            surface->row_bytes = w;
+            surface->pixel_bytes = 1;
+
+            int i;
+            for (i = 0; i < h; ++i, ++y) {
+                png_read_row(png_ptr, row.data(), NULL);
+                memcpy(surface->data + i*w, row.data(), w);
+            }
+
+            *pSurface = reinterpret_cast<GRSurface*>(surface);
+            break;
+        } else {
+            int i;
+            for (i = 0; i < h; ++i, ++y) {
+                png_read_row(png_ptr, row.data(), NULL);
+            }
+        }
+    }
+
+exit:
+    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+    if (result < 0 && surface != NULL) free(surface);
+    return result;
+}
+
+void res_free_surface(GRSurface* surface) {
+    free(surface);
+}
diff --git a/recovery/minzip/Android.mk b/recovery/minzip/Android.mk
new file mode 100644
index 0000000..3d36fd6
--- /dev/null
+++ b/recovery/minzip/Android.mk
@@ -0,0 +1,23 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+	Hash.c \
+	SysUtil.c \
+	DirUtil.cpp \
+	Inlines.c \
+	Zip.c
+
+LOCAL_C_INCLUDES := \
+	external/zlib \
+	external/safe-iop/include
+
+LOCAL_STATIC_LIBRARIES := libselinux
+
+LOCAL_MODULE := libminzip
+
+LOCAL_CLANG := true
+
+LOCAL_CFLAGS += -Werror -Wall
+
+include $(BUILD_STATIC_LIBRARY)
diff --git a/recovery/minzip/Bits.h b/recovery/minzip/Bits.h
new file mode 100644
index 0000000..f96e6c4
--- /dev/null
+++ b/recovery/minzip/Bits.h
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Some handy functions for manipulating bits and bytes.
+ */
+#ifndef _MINZIP_BITS
+#define _MINZIP_BITS
+
+#include "inline_magic.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * Get 1 byte.  (Included to make the code more legible.)
+ */
+INLINE unsigned char get1(unsigned const char* pSrc)
+{
+    return *pSrc;
+}
+
+/*
+ * Get 2 big-endian bytes.
+ */
+INLINE unsigned short get2BE(unsigned char const* pSrc)
+{
+    unsigned short result;
+
+    result = *pSrc++ << 8;
+    result |= *pSrc++;
+
+    return result;
+}
+
+/*
+ * Get 4 big-endian bytes.
+ */
+INLINE unsigned int get4BE(unsigned char const* pSrc)
+{
+    unsigned int result;
+
+    result = *pSrc++ << 24;
+    result |= *pSrc++ << 16;
+    result |= *pSrc++ << 8;
+    result |= *pSrc++;
+
+    return result;
+}
+
+/*
+ * Get 8 big-endian bytes.
+ */
+INLINE unsigned long long get8BE(unsigned char const* pSrc)
+{
+    unsigned long long result;
+
+    result = (unsigned long long) *pSrc++ << 56;
+    result |= (unsigned long long) *pSrc++ << 48;
+    result |= (unsigned long long) *pSrc++ << 40;
+    result |= (unsigned long long) *pSrc++ << 32;
+    result |= (unsigned long long) *pSrc++ << 24;
+    result |= (unsigned long long) *pSrc++ << 16;
+    result |= (unsigned long long) *pSrc++ << 8;
+    result |= (unsigned long long) *pSrc++;
+
+    return result;
+}
+
+/*
+ * Get 2 little-endian bytes.
+ */
+INLINE unsigned short get2LE(unsigned char const* pSrc)
+{
+    unsigned short result;
+
+    result = *pSrc++;
+    result |= *pSrc++ << 8;
+
+    return result;
+}
+
+/*
+ * Get 4 little-endian bytes.
+ */
+INLINE unsigned int get4LE(unsigned char const* pSrc)
+{
+    unsigned int result;
+
+    result = *pSrc++;
+    result |= *pSrc++ << 8;
+    result |= *pSrc++ << 16;
+    result |= *pSrc++ << 24;
+
+    return result;
+}
+
+/*
+ * Get 8 little-endian bytes.
+ */
+INLINE unsigned long long get8LE(unsigned char const* pSrc)
+{
+    unsigned long long result;
+
+    result = (unsigned long long) *pSrc++;
+    result |= (unsigned long long) *pSrc++ << 8;
+    result |= (unsigned long long) *pSrc++ << 16;
+    result |= (unsigned long long) *pSrc++ << 24;
+    result |= (unsigned long long) *pSrc++ << 32;
+    result |= (unsigned long long) *pSrc++ << 40;
+    result |= (unsigned long long) *pSrc++ << 48;
+    result |= (unsigned long long) *pSrc++ << 56;
+
+    return result;
+}
+
+/*
+ * Grab 1 byte and advance the data pointer.
+ */
+INLINE unsigned char read1(unsigned const char** ppSrc)
+{
+    return *(*ppSrc)++;
+}
+
+/*
+ * Grab 2 big-endian bytes and advance the data pointer.
+ */
+INLINE unsigned short read2BE(unsigned char const** ppSrc)
+{
+    unsigned short result;
+
+    result = *(*ppSrc)++ << 8;
+    result |= *(*ppSrc)++;
+
+    return result;
+}
+
+/*
+ * Grab 4 big-endian bytes and advance the data pointer.
+ */
+INLINE unsigned int read4BE(unsigned char const** ppSrc)
+{
+    unsigned int result;
+
+    result = *(*ppSrc)++ << 24;
+    result |= *(*ppSrc)++ << 16;
+    result |= *(*ppSrc)++ << 8;
+    result |= *(*ppSrc)++;
+
+    return result;
+}
+
+/*
+ * Get 8 big-endian bytes.
+ */
+INLINE unsigned long long read8BE(unsigned char const** ppSrc)
+{
+    unsigned long long result;
+
+    result = (unsigned long long) *(*ppSrc)++ << 56;
+    result |= (unsigned long long) *(*ppSrc)++ << 48;
+    result |= (unsigned long long) *(*ppSrc)++ << 40;
+    result |= (unsigned long long) *(*ppSrc)++ << 32;
+    result |= (unsigned long long) *(*ppSrc)++ << 24;
+    result |= (unsigned long long) *(*ppSrc)++ << 16;
+    result |= (unsigned long long) *(*ppSrc)++ << 8;
+    result |= (unsigned long long) *(*ppSrc)++;
+
+    return result;
+}
+
+/*
+ * Grab 2 little-endian bytes and advance the data pointer.
+ */
+INLINE unsigned short read2LE(unsigned char const** ppSrc)
+{
+    unsigned short result;
+
+    result = *(*ppSrc)++;
+    result |= *(*ppSrc)++ << 8;
+
+    return result;
+}
+
+/*
+ * Grab 4 little-endian bytes and advance the data pointer.
+ */
+INLINE unsigned int read4LE(unsigned char const** ppSrc)
+{
+    unsigned int result;
+
+    result = *(*ppSrc)++;
+    result |= *(*ppSrc)++ << 8;
+    result |= *(*ppSrc)++ << 16;
+    result |= *(*ppSrc)++ << 24;
+
+    return result;
+}
+
+/*
+ * Get 8 little-endian bytes.
+ */
+INLINE unsigned long long read8LE(unsigned char const** ppSrc)
+{
+    unsigned long long result;
+
+    result = (unsigned long long) *(*ppSrc)++;
+    result |= (unsigned long long) *(*ppSrc)++ << 8;
+    result |= (unsigned long long) *(*ppSrc)++ << 16;
+    result |= (unsigned long long) *(*ppSrc)++ << 24;
+    result |= (unsigned long long) *(*ppSrc)++ << 32;
+    result |= (unsigned long long) *(*ppSrc)++ << 40;
+    result |= (unsigned long long) *(*ppSrc)++ << 48;
+    result |= (unsigned long long) *(*ppSrc)++ << 56;
+
+    return result;
+}
+
+/*
+ * Skip over a UTF-8 string.
+ */
+INLINE void skipUtf8String(unsigned char const** ppSrc)
+{
+    unsigned int length = read4BE(ppSrc);
+
+    (*ppSrc) += length;
+}
+
+/*
+ * Read a UTF-8 string into a fixed-size buffer, and null-terminate it.
+ *
+ * Returns the length of the original string.
+ */
+INLINE int readUtf8String(unsigned char const** ppSrc, char* buf, size_t bufLen)
+{
+    unsigned int length = read4BE(ppSrc);
+    size_t copyLen = (length < bufLen) ? length : bufLen-1;
+
+    memcpy(buf, *ppSrc, copyLen);
+    buf[copyLen] = '\0';
+
+    (*ppSrc) += length;
+    return length;
+}
+
+/*
+ * Read a UTF-8 string into newly-allocated storage, and null-terminate it.
+ *
+ * Returns the string and its length.  (The latter is probably unnecessary
+ * for the way we're using UTF8.)
+ */
+INLINE char* readNewUtf8String(unsigned char const** ppSrc, size_t* pLength)
+{
+    unsigned int length = read4BE(ppSrc);
+    char* buf;
+
+    buf = (char*) malloc(length+1);
+
+    memcpy(buf, *ppSrc, length);
+    buf[length] = '\0';
+
+    (*ppSrc) += length;
+
+    *pLength = length;
+    return buf;
+}
+
+
+/*
+ * Set 1 byte.  (Included to make the code more legible.)
+ */
+INLINE void set1(unsigned char* buf, unsigned char val)
+{
+    *buf = (unsigned char)(val);
+}
+
+/*
+ * Set 2 big-endian bytes.
+ */
+INLINE void set2BE(unsigned char* buf, unsigned short val)
+{
+    *buf++ = (unsigned char)(val >> 8);
+    *buf = (unsigned char)(val);
+}
+
+/*
+ * Set 4 big-endian bytes.
+ */
+INLINE void set4BE(unsigned char* buf, unsigned int val)
+{
+    *buf++ = (unsigned char)(val >> 24);
+    *buf++ = (unsigned char)(val >> 16);
+    *buf++ = (unsigned char)(val >> 8);
+    *buf = (unsigned char)(val);
+}
+
+/*
+ * Set 8 big-endian bytes.
+ */
+INLINE void set8BE(unsigned char* buf, unsigned long long val)
+{
+    *buf++ = (unsigned char)(val >> 56);
+    *buf++ = (unsigned char)(val >> 48);
+    *buf++ = (unsigned char)(val >> 40);
+    *buf++ = (unsigned char)(val >> 32);
+    *buf++ = (unsigned char)(val >> 24);
+    *buf++ = (unsigned char)(val >> 16);
+    *buf++ = (unsigned char)(val >> 8);
+    *buf = (unsigned char)(val);
+}
+
+/*
+ * Set 2 little-endian bytes.
+ */
+INLINE void set2LE(unsigned char* buf, unsigned short val)
+{
+    *buf++ = (unsigned char)(val);
+    *buf = (unsigned char)(val >> 8);
+}
+
+/*
+ * Set 4 little-endian bytes.
+ */
+INLINE void set4LE(unsigned char* buf, unsigned int val)
+{
+    *buf++ = (unsigned char)(val);
+    *buf++ = (unsigned char)(val >> 8);
+    *buf++ = (unsigned char)(val >> 16);
+    *buf = (unsigned char)(val >> 24);
+}
+
+/*
+ * Set 8 little-endian bytes.
+ */
+INLINE void set8LE(unsigned char* buf, unsigned long long val)
+{
+    *buf++ = (unsigned char)(val);
+    *buf++ = (unsigned char)(val >> 8);
+    *buf++ = (unsigned char)(val >> 16);
+    *buf++ = (unsigned char)(val >> 24);
+    *buf++ = (unsigned char)(val >> 32);
+    *buf++ = (unsigned char)(val >> 40);
+    *buf++ = (unsigned char)(val >> 48);
+    *buf = (unsigned char)(val >> 56);
+}
+
+/*
+ * Stuff a UTF-8 string into the buffer.
+ */
+INLINE void setUtf8String(unsigned char* buf, const unsigned char* str)
+{
+    unsigned int strLen = strlen((const char*)str);
+
+    set4BE(buf, strLen);
+    memcpy(buf + sizeof(unsigned int), str, strLen);
+}
+
+#endif /*_MINZIP_BITS*/
diff --git a/recovery/minzip/DirUtil.cpp b/recovery/minzip/DirUtil.cpp
new file mode 100644
index 0000000..e08e360
--- /dev/null
+++ b/recovery/minzip/DirUtil.cpp
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include "DirUtil.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <dirent.h>
+#include <limits.h>
+
+#include <string>
+
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+typedef enum { DMISSING, DDIR, DILLEGAL } DirStatus;
+
+static DirStatus
+getPathDirStatus(const char *path)
+{
+    struct stat st;
+    int err;
+
+    err = stat(path, &st);
+    if (err == 0) {
+        /* Something's there; make sure it's a directory.
+         */
+        if (S_ISDIR(st.st_mode)) {
+            return DDIR;
+        }
+        errno = ENOTDIR;
+        return DILLEGAL;
+    } else if (errno != ENOENT) {
+        /* Something went wrong, or something in the path
+         * is bad.  Can't do anything in this situation.
+         */
+        return DILLEGAL;
+    }
+    return DMISSING;
+}
+
+int
+dirCreateHierarchy(const char *path, int mode,
+        const struct utimbuf *timestamp, bool stripFileName,
+        struct selabel_handle *sehnd)
+{
+    DirStatus ds;
+
+    /* Check for an empty string before we bother
+     * making any syscalls.
+     */
+    if (path[0] == '\0') {
+        errno = ENOENT;
+        return -1;
+    }
+    // Allocate a path that we can modify; stick a slash on
+    // the end to make things easier.
+    std::string cpath = path;
+    if (stripFileName) {
+        // Strip everything after the last slash.
+        size_t pos = cpath.rfind('/');
+        if (pos == std::string::npos) {
+            errno = ENOENT;
+            return -1;
+        }
+        cpath.resize(pos + 1);
+    } else {
+        // Make sure that the path ends in a slash.
+        cpath.push_back('/');
+    }
+
+    /* See if it already exists.
+     */
+    ds = getPathDirStatus(cpath.c_str());
+    if (ds == DDIR) {
+        return 0;
+    } else if (ds == DILLEGAL) {
+        return -1;
+    }
+
+    /* Walk up the path from the root and make each level.
+     * If a directory already exists, no big deal.
+     */
+    const char *path_start = &cpath[0];
+    char *p = &cpath[0];
+    while (*p != '\0') {
+        /* Skip any slashes, watching out for the end of the string.
+         */
+        while (*p != '\0' && *p == '/') {
+            p++;
+        }
+        if (*p == '\0') {
+            break;
+        }
+
+        /* Find the end of the next path component.
+         * We know that we'll see a slash before the NUL,
+         * because we added it, above.
+         */
+        while (*p != '/') {
+            p++;
+        }
+        *p = '\0';
+
+        /* Check this part of the path and make a new directory
+         * if necessary.
+         */
+        ds = getPathDirStatus(path_start);
+        if (ds == DILLEGAL) {
+            /* Could happen if some other process/thread is
+             * messing with the filesystem.
+             */
+            return -1;
+        } else if (ds == DMISSING) {
+            int err;
+
+            char *secontext = NULL;
+
+            if (sehnd) {
+                selabel_lookup(sehnd, &secontext, path_start, mode);
+                setfscreatecon(secontext);
+            }
+
+            err = mkdir(path_start, mode);
+
+            if (secontext) {
+                freecon(secontext);
+                setfscreatecon(NULL);
+            }
+
+            if (err != 0) {
+                return -1;
+            }
+            if (timestamp != NULL && utime(path_start, timestamp)) {
+                return -1;
+            }
+        }
+        // else, this directory already exists.
+
+        // Repair the path and continue.
+        *p = '/';
+    }
+    return 0;
+}
+
+int
+dirUnlinkHierarchy(const char *path)
+{
+    struct stat st;
+    DIR *dir;
+    struct dirent *de;
+    int fail = 0;
+
+    /* is it a file or directory? */
+    if (lstat(path, &st) < 0) {
+        return -1;
+    }
+
+    /* a file, so unlink it */
+    if (!S_ISDIR(st.st_mode)) {
+        return unlink(path);
+    }
+
+    /* a directory, so open handle */
+    dir = opendir(path);
+    if (dir == NULL) {
+        return -1;
+    }
+
+    /* recurse over components */
+    errno = 0;
+    while ((de = readdir(dir)) != NULL) {
+        //TODO: don't blow the stack
+        char dn[PATH_MAX];
+        if (!strcmp(de->d_name, "..") || !strcmp(de->d_name, ".")) {
+            continue;
+        }
+        snprintf(dn, sizeof(dn), "%s/%s", path, de->d_name);
+        if (dirUnlinkHierarchy(dn) < 0) {
+            fail = 1;
+            break;
+        }
+        errno = 0;
+    }
+    /* in case readdir or unlink_recursive failed */
+    if (fail || errno < 0) {
+        int save = errno;
+        closedir(dir);
+        errno = save;
+        return -1;
+    }
+
+    /* close directory handle */
+    if (closedir(dir) < 0) {
+        return -1;
+    }
+
+    /* delete target directory */
+    return rmdir(path);
+}
diff --git a/recovery/minzip/DirUtil.h b/recovery/minzip/DirUtil.h
new file mode 100644
index 0000000..85b83c3
--- /dev/null
+++ b/recovery/minzip/DirUtil.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef MINZIP_DIRUTIL_H_
+#define MINZIP_DIRUTIL_H_
+
+#include <stdbool.h>
+#include <utime.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct selabel_handle;
+
+/* Like "mkdir -p", try to guarantee that all directories
+ * specified in path are present, creating as many directories
+ * as necessary.  The specified mode is passed to all mkdir
+ * calls;  no modifications are made to umask.
+ *
+ * If stripFileName is set, everything after the final '/'
+ * is stripped before creating the directory hierarchy.
+ *
+ * If timestamp is non-NULL, new directories will be timestamped accordingly.
+ *
+ * Returns 0 on success; returns -1 (and sets errno) on failure
+ * (usually if some element of path is not a directory).
+ */
+int dirCreateHierarchy(const char *path, int mode,
+        const struct utimbuf *timestamp, bool stripFileName,
+        struct selabel_handle* sehnd);
+
+/* rm -rf <path>
+ */
+int dirUnlinkHierarchy(const char *path);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // MINZIP_DIRUTIL_H_
diff --git a/recovery/minzip/Hash.c b/recovery/minzip/Hash.c
new file mode 100644
index 0000000..49bcb31
--- /dev/null
+++ b/recovery/minzip/Hash.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Hash table.  The dominant calls are add and lookup, with removals
+ * happening very infrequently.  We use probing, and don't worry much
+ * about tombstone removal.
+ */
+#include <stdlib.h>
+#include <assert.h>
+
+#define LOG_TAG "minzip"
+#include "Log.h"
+#include "Hash.h"
+
+/* table load factor, i.e. how full can it get before we resize */
+//#define LOAD_NUMER  3       // 75%
+//#define LOAD_DENOM  4
+#define LOAD_NUMER  5       // 62.5%
+#define LOAD_DENOM  8
+//#define LOAD_NUMER  1       // 50%
+//#define LOAD_DENOM  2
+
+/*
+ * Compute the capacity needed for a table to hold "size" elements.
+ */
+size_t mzHashSize(size_t size) {
+    return (size * LOAD_DENOM) / LOAD_NUMER +1;
+}
+
+/*
+ * Round up to the next highest power of 2.
+ *
+ * Found on http://graphics.stanford.edu/~seander/bithacks.html.
+ */
+unsigned int roundUpPower2(unsigned int val)
+{
+    val--;
+    val |= val >> 1;
+    val |= val >> 2;
+    val |= val >> 4;
+    val |= val >> 8;
+    val |= val >> 16;
+    val++;
+
+    return val;
+}
+
+/*
+ * Create and initialize a hash table.
+ */
+HashTable* mzHashTableCreate(size_t initialSize, HashFreeFunc freeFunc)
+{
+    HashTable* pHashTable;
+
+    assert(initialSize > 0);
+
+    pHashTable = (HashTable*) malloc(sizeof(*pHashTable));
+    if (pHashTable == NULL)
+        return NULL;
+
+    pHashTable->tableSize = roundUpPower2(initialSize);
+    pHashTable->numEntries = pHashTable->numDeadEntries = 0;
+    pHashTable->freeFunc = freeFunc;
+    pHashTable->pEntries =
+        (HashEntry*) calloc((size_t)pHashTable->tableSize, sizeof(HashTable));
+    if (pHashTable->pEntries == NULL) {
+        free(pHashTable);
+        return NULL;
+    }
+
+    return pHashTable;
+}
+
+/*
+ * Clear out all entries.
+ */
+void mzHashTableClear(HashTable* pHashTable)
+{
+    HashEntry* pEnt;
+    int i;
+
+    pEnt = pHashTable->pEntries;
+    for (i = 0; i < pHashTable->tableSize; i++, pEnt++) {
+        if (pEnt->data == HASH_TOMBSTONE) {
+            // nuke entry
+            pEnt->data = NULL;
+        } else if (pEnt->data != NULL) {
+            // call free func then nuke entry
+            if (pHashTable->freeFunc != NULL)
+                (*pHashTable->freeFunc)(pEnt->data);
+            pEnt->data = NULL;
+        }
+    }
+
+    pHashTable->numEntries = 0;
+    pHashTable->numDeadEntries = 0;
+}
+
+/*
+ * Free the table.
+ */
+void mzHashTableFree(HashTable* pHashTable)
+{
+    if (pHashTable == NULL)
+        return;
+    mzHashTableClear(pHashTable);
+    free(pHashTable->pEntries);
+    free(pHashTable);
+}
+
+#ifndef NDEBUG
+/*
+ * Count up the number of tombstone entries in the hash table.
+ */
+static int countTombStones(HashTable* pHashTable)
+{
+    int i, count;
+
+    for (count = i = 0; i < pHashTable->tableSize; i++) {
+        if (pHashTable->pEntries[i].data == HASH_TOMBSTONE)
+            count++;
+    }
+    return count;
+}
+#endif
+
+/*
+ * Resize a hash table.  We do this when adding an entry increased the
+ * size of the table beyond its comfy limit.
+ *
+ * This essentially requires re-inserting all elements into the new storage.
+ *
+ * If multiple threads can access the hash table, the table's lock should
+ * have been grabbed before issuing the "lookup+add" call that led to the
+ * resize, so we don't have a synchronization problem here.
+ */
+static bool resizeHash(HashTable* pHashTable, int newSize)
+{
+    HashEntry* pNewEntries;
+    int i;
+
+    assert(countTombStones(pHashTable) == pHashTable->numDeadEntries);
+
+    pNewEntries = (HashEntry*) calloc(newSize, sizeof(HashTable));
+    if (pNewEntries == NULL)
+        return false;
+
+    for (i = 0; i < pHashTable->tableSize; i++) {
+        void* data = pHashTable->pEntries[i].data;
+        if (data != NULL && data != HASH_TOMBSTONE) {
+            int hashValue = pHashTable->pEntries[i].hashValue;
+            int newIdx;
+
+            /* probe for new spot, wrapping around */
+            newIdx = hashValue & (newSize-1);
+            while (pNewEntries[newIdx].data != NULL)
+                newIdx = (newIdx + 1) & (newSize-1);
+
+            pNewEntries[newIdx].hashValue = hashValue;
+            pNewEntries[newIdx].data = data;
+        }
+    }
+
+    free(pHashTable->pEntries);
+    pHashTable->pEntries = pNewEntries;
+    pHashTable->tableSize = newSize;
+    pHashTable->numDeadEntries = 0;
+
+    assert(countTombStones(pHashTable) == 0);
+    return true;
+}
+
+/*
+ * Look up an entry.
+ *
+ * We probe on collisions, wrapping around the table.
+ */
+void* mzHashTableLookup(HashTable* pHashTable, unsigned int itemHash, void* item,
+    HashCompareFunc cmpFunc, bool doAdd)
+{
+    HashEntry* pEntry;
+    HashEntry* pEnd;
+    void* result = NULL;
+
+    assert(pHashTable->tableSize > 0);
+    assert(item != HASH_TOMBSTONE);
+    assert(item != NULL);
+
+    /* jump to the first entry and probe for a match */
+    pEntry = &pHashTable->pEntries[itemHash & (pHashTable->tableSize-1)];
+    pEnd = &pHashTable->pEntries[pHashTable->tableSize];
+    while (pEntry->data != NULL) {
+        if (pEntry->data != HASH_TOMBSTONE &&
+            pEntry->hashValue == itemHash &&
+            (*cmpFunc)(pEntry->data, item) == 0)
+        {
+            /* match */
+            break;
+        }
+
+        pEntry++;
+        if (pEntry == pEnd) {     /* wrap around to start */
+            if (pHashTable->tableSize == 1)
+                break;      /* edge case - single-entry table */
+            pEntry = pHashTable->pEntries;
+        }
+    }
+
+    if (pEntry->data == NULL) {
+        if (doAdd) {
+            pEntry->hashValue = itemHash;
+            pEntry->data = item;
+            pHashTable->numEntries++;
+
+            /*
+             * We've added an entry.  See if this brings us too close to full.
+             */
+            if ((pHashTable->numEntries+pHashTable->numDeadEntries) * LOAD_DENOM
+                > pHashTable->tableSize * LOAD_NUMER)
+            {
+                if (!resizeHash(pHashTable, pHashTable->tableSize * 2)) {
+                    /* don't really have a way to indicate failure */
+                    LOGE("Dalvik hash resize failure\n");
+                    abort();
+                }
+                /* note "pEntry" is now invalid */
+            }
+
+            /* full table is bad -- search for nonexistent never halts */
+            assert(pHashTable->numEntries < pHashTable->tableSize);
+            result = item;
+        } else {
+            assert(result == NULL);
+        }
+    } else {
+        result = pEntry->data;
+    }
+
+    return result;
+}
+
+/*
+ * Remove an entry from the table.
+ *
+ * Does NOT invoke the "free" function on the item.
+ */
+bool mzHashTableRemove(HashTable* pHashTable, unsigned int itemHash, void* item)
+{
+    HashEntry* pEntry;
+    HashEntry* pEnd;
+
+    assert(pHashTable->tableSize > 0);
+
+    /* jump to the first entry and probe for a match */
+    pEntry = &pHashTable->pEntries[itemHash & (pHashTable->tableSize-1)];
+    pEnd = &pHashTable->pEntries[pHashTable->tableSize];
+    while (pEntry->data != NULL) {
+        if (pEntry->data == item) {
+            pEntry->data = HASH_TOMBSTONE;
+            pHashTable->numEntries--;
+            pHashTable->numDeadEntries++;
+            return true;
+        }
+
+        pEntry++;
+        if (pEntry == pEnd) {     /* wrap around to start */
+            if (pHashTable->tableSize == 1)
+                break;      /* edge case - single-entry table */
+            pEntry = pHashTable->pEntries;
+        }
+    }
+
+    return false;
+}
+
+/*
+ * Execute a function on every entry in the hash table.
+ *
+ * If "func" returns a nonzero value, terminate early and return the value.
+ */
+int mzHashForeach(HashTable* pHashTable, HashForeachFunc func, void* arg)
+{
+    int i, val;
+
+    for (i = 0; i < pHashTable->tableSize; i++) {
+        HashEntry* pEnt = &pHashTable->pEntries[i];
+
+        if (pEnt->data != NULL && pEnt->data != HASH_TOMBSTONE) {
+            val = (*func)(pEnt->data, arg);
+            if (val != 0)
+                return val;
+        }
+    }
+
+    return 0;
+}
+
+
+/*
+ * Look up an entry, counting the number of times we have to probe.
+ *
+ * Returns -1 if the entry wasn't found.
+ */
+int countProbes(HashTable* pHashTable, unsigned int itemHash, const void* item,
+    HashCompareFunc cmpFunc)
+{
+    HashEntry* pEntry;
+    HashEntry* pEnd;
+    int count = 0;
+
+    assert(pHashTable->tableSize > 0);
+    assert(item != HASH_TOMBSTONE);
+    assert(item != NULL);
+
+    /* jump to the first entry and probe for a match */
+    pEntry = &pHashTable->pEntries[itemHash & (pHashTable->tableSize-1)];
+    pEnd = &pHashTable->pEntries[pHashTable->tableSize];
+    while (pEntry->data != NULL) {
+        if (pEntry->data != HASH_TOMBSTONE &&
+            pEntry->hashValue == itemHash &&
+            (*cmpFunc)(pEntry->data, item) == 0)
+        {
+            /* match */
+            break;
+        }
+
+        pEntry++;
+        if (pEntry == pEnd) {     /* wrap around to start */
+            if (pHashTable->tableSize == 1)
+                break;      /* edge case - single-entry table */
+            pEntry = pHashTable->pEntries;
+        }
+
+        count++;
+    }
+    if (pEntry->data == NULL)
+        return -1;
+
+    return count;
+}
+
+/*
+ * Evaluate the amount of probing required for the specified hash table.
+ *
+ * We do this by running through all entries in the hash table, computing
+ * the hash value and then doing a lookup.
+ *
+ * The caller should lock the table before calling here.
+ */
+void mzHashTableProbeCount(HashTable* pHashTable, HashCalcFunc calcFunc,
+    HashCompareFunc cmpFunc)
+{
+    int numEntries, minProbe, maxProbe, totalProbe;
+    HashIter iter;
+
+    numEntries = maxProbe = totalProbe = 0;
+    minProbe = 65536*32767;
+
+    for (mzHashIterBegin(pHashTable, &iter); !mzHashIterDone(&iter);
+        mzHashIterNext(&iter))
+    {
+        const void* data = (const void*)mzHashIterData(&iter);
+        int count;
+
+        count = countProbes(pHashTable, (*calcFunc)(data), data, cmpFunc);
+
+        numEntries++;
+
+        if (count < minProbe)
+            minProbe = count;
+        if (count > maxProbe)
+            maxProbe = count;
+        totalProbe += count;
+    }
+
+    LOGV("Probe: min=%d max=%d, total=%d in %d (%d), avg=%.3f\n",
+        minProbe, maxProbe, totalProbe, numEntries, pHashTable->tableSize,
+        (float) totalProbe / (float) numEntries);
+}
diff --git a/recovery/minzip/Hash.h b/recovery/minzip/Hash.h
new file mode 100644
index 0000000..e83eac4
--- /dev/null
+++ b/recovery/minzip/Hash.h
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2007 The Android Open Source Project
+ *
+ * General purpose hash table, used for finding classes, methods, etc.
+ *
+ * When the number of elements reaches 3/4 of the table's capacity, the
+ * table will be resized.
+ */
+#ifndef _MINZIP_HASH
+#define _MINZIP_HASH
+
+#include "inline_magic.h"
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* compute the hash of an item with a specific type */
+typedef unsigned int (*HashCompute)(const void* item);
+
+/*
+ * Compare a hash entry with a "loose" item after their hash values match.
+ * Returns { <0, 0, >0 } depending on ordering of items (same semantics
+ * as strcmp()).
+ */
+typedef int (*HashCompareFunc)(const void* tableItem, const void* looseItem);
+
+/*
+ * This function will be used to free entries in the table.  This can be
+ * NULL if no free is required, free(), or a custom function.
+ */
+typedef void (*HashFreeFunc)(void* ptr);
+
+/*
+ * Used by mzHashForeach().
+ */
+typedef int (*HashForeachFunc)(void* data, void* arg);
+
+/*
+ * One entry in the hash table.  "data" values are expected to be (or have
+ * the same characteristics as) valid pointers.  In particular, a NULL
+ * value for "data" indicates an empty slot, and HASH_TOMBSTONE indicates
+ * a no-longer-used slot that must be stepped over during probing.
+ *
+ * Attempting to add a NULL or tombstone value is an error.
+ *
+ * When an entry is released, we will call (HashFreeFunc)(entry->data).
+ */
+typedef struct HashEntry {
+    unsigned int hashValue;
+    void* data;
+} HashEntry;
+
+#define HASH_TOMBSTONE ((void*) 0xcbcacccd)     // invalid ptr value
+
+/*
+ * Expandable hash table.
+ *
+ * This structure should be considered opaque.
+ */
+typedef struct HashTable {
+    int         tableSize;          /* must be power of 2 */
+    int         numEntries;         /* current #of "live" entries */
+    int         numDeadEntries;     /* current #of tombstone entries */
+    HashEntry*  pEntries;           /* array on heap */
+    HashFreeFunc freeFunc;
+} HashTable;
+
+/*
+ * Create and initialize a HashTable structure, using "initialSize" as
+ * a basis for the initial capacity of the table.  (The actual initial
+ * table size may be adjusted upward.)  If you know exactly how many
+ * elements the table will hold, pass the result from mzHashSize() in.)
+ *
+ * Returns "false" if unable to allocate the table.
+ */
+HashTable* mzHashTableCreate(size_t initialSize, HashFreeFunc freeFunc);
+
+/*
+ * Compute the capacity needed for a table to hold "size" elements.  Use
+ * this when you know ahead of time how many elements the table will hold.
+ * Pass this value into mzHashTableCreate() to ensure that you can add
+ * all elements without needing to reallocate the table.
+ */
+size_t mzHashSize(size_t size);
+
+/*
+ * Clear out a hash table, freeing the contents of any used entries.
+ */
+void mzHashTableClear(HashTable* pHashTable);
+
+/*
+ * Free a hash table.
+ */
+void mzHashTableFree(HashTable* pHashTable);
+
+/*
+ * Get #of entries in hash table.
+ */
+INLINE int mzHashTableNumEntries(HashTable* pHashTable) {
+    return pHashTable->numEntries;
+}
+
+/*
+ * Get total size of hash table (for memory usage calculations).
+ */
+INLINE int mzHashTableMemUsage(HashTable* pHashTable) {
+    return sizeof(HashTable) + pHashTable->tableSize * sizeof(HashEntry);
+}
+
+/*
+ * Look up an entry in the table, possibly adding it if it's not there.
+ *
+ * If "item" is not found, and "doAdd" is false, NULL is returned.
+ * Otherwise, a pointer to the found or added item is returned.  (You can
+ * tell the difference by seeing if return value == item.)
+ *
+ * An "add" operation may cause the entire table to be reallocated.
+ */
+void* mzHashTableLookup(HashTable* pHashTable, unsigned int itemHash, void* item,
+    HashCompareFunc cmpFunc, bool doAdd);
+
+/*
+ * Remove an item from the hash table, given its "data" pointer.  Does not
+ * invoke the "free" function; just detaches it from the table.
+ */
+bool mzHashTableRemove(HashTable* pHashTable, unsigned int hash, void* item);
+
+/*
+ * Execute "func" on every entry in the hash table.
+ *
+ * If "func" returns a nonzero value, terminate early and return the value.
+ */
+int mzHashForeach(HashTable* pHashTable, HashForeachFunc func, void* arg);
+
+/*
+ * An alternative to mzHashForeach(), using an iterator.
+ *
+ * Use like this:
+ *   HashIter iter;
+ *   for (mzHashIterBegin(hashTable, &iter); !mzHashIterDone(&iter);
+ *       mzHashIterNext(&iter))
+ *   {
+ *       MyData* data = (MyData*)mzHashIterData(&iter);
+ *   }
+ */
+typedef struct HashIter {
+    void*       data;
+    HashTable*  pHashTable;
+    int         idx;
+} HashIter;
+INLINE void mzHashIterNext(HashIter* pIter) {
+    int i = pIter->idx +1;
+    int lim = pIter->pHashTable->tableSize;
+    for ( ; i < lim; i++) {
+        void* data = pIter->pHashTable->pEntries[i].data;
+        if (data != NULL && data != HASH_TOMBSTONE)
+            break;
+    }
+    pIter->idx = i;
+}
+INLINE void mzHashIterBegin(HashTable* pHashTable, HashIter* pIter) {
+    pIter->pHashTable = pHashTable;
+    pIter->idx = -1;
+    mzHashIterNext(pIter);
+}
+INLINE bool mzHashIterDone(HashIter* pIter) {
+    return (pIter->idx >= pIter->pHashTable->tableSize);
+}
+INLINE void* mzHashIterData(HashIter* pIter) {
+    assert(pIter->idx >= 0 && pIter->idx < pIter->pHashTable->tableSize);
+    return pIter->pHashTable->pEntries[pIter->idx].data;
+}
+
+
+/*
+ * Evaluate hash table performance by examining the number of times we
+ * have to probe for an entry.
+ *
+ * The caller should lock the table beforehand.
+ */
+typedef unsigned int (*HashCalcFunc)(const void* item);
+void mzHashTableProbeCount(HashTable* pHashTable, HashCalcFunc calcFunc,
+    HashCompareFunc cmpFunc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*_MINZIP_HASH*/
diff --git a/recovery/minzip/Inlines.c b/recovery/minzip/Inlines.c
new file mode 100644
index 0000000..91f8775
--- /dev/null
+++ b/recovery/minzip/Inlines.c
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+/* Make sure that non-inlined versions of INLINED-marked functions
+ * exist so that debug builds (which don't generally do inlining)
+ * don't break.
+ */
+#define MINZIP_GENERATE_INLINES 1
+#include "Bits.h"
+#include "Hash.h"
+#include "SysUtil.h"
+#include "Zip.h"
diff --git a/recovery/minzip/Log.h b/recovery/minzip/Log.h
new file mode 100644
index 0000000..36e62f5
--- /dev/null
+++ b/recovery/minzip/Log.h
@@ -0,0 +1,207 @@
+//
+// Copyright 2005 The Android Open Source Project
+//
+// C/C++ logging functions.  See the logging documentation for API details.
+//
+// We'd like these to be available from C code (in case we import some from
+// somewhere), so this has a C interface.
+//
+// The output will be correct when the log file is shared between multiple
+// threads and/or multiple processes so long as the operating system
+// supports O_APPEND.  These calls have mutex-protected data structures
+// and so are NOT reentrant.  Do not use LOG in a signal handler.
+//
+#ifndef _MINZIP_LOG_H
+#define _MINZIP_LOG_H
+
+#include <stdio.h>
+
+// ---------------------------------------------------------------------
+
+/*
+ * Normally we strip LOGV (VERBOSE messages) from release builds.
+ * You can modify this (for example with "#define LOG_NDEBUG 0"
+ * at the top of your source file) to change that behavior.
+ */
+#ifndef LOG_NDEBUG
+#ifdef NDEBUG
+#define LOG_NDEBUG 1
+#else
+#define LOG_NDEBUG 0
+#endif
+#endif
+
+/*
+ * This is the local tag used for the following simplified
+ * logging macros.  You can change this preprocessor definition
+ * before using the other macros to change the tag.
+ */
+#ifndef LOG_TAG
+#define LOG_TAG NULL
+#endif
+
+// ---------------------------------------------------------------------
+
+/*
+ * Simplified macro to send a verbose log message using the current LOG_TAG.
+ */
+#ifndef LOGV
+#if LOG_NDEBUG
+#define LOGV(...)   ((void)0)
+#else
+#define LOGV(...) ((void)LOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
+#endif
+#endif
+
+#define CONDITION(cond)     (__builtin_expect((cond)!=0, 0))
+
+#ifndef LOGV_IF
+#if LOG_NDEBUG
+#define LOGV_IF(cond, ...)   ((void)0)
+#else
+#define LOGV_IF(cond, ...) \
+    ( (CONDITION(cond)) \
+    ? ((void)LOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \
+    : (void)0 )
+#endif
+#endif
+
+#define LOGVV LOGV
+#define LOGVV_IF LOGV_IF
+
+/*
+ * Simplified macro to send a debug log message using the current LOG_TAG.
+ */
+#ifndef LOGD
+#define LOGD(...) ((void)LOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef LOGD_IF
+#define LOGD_IF(cond, ...) \
+    ( (CONDITION(cond)) \
+    ? ((void)LOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \
+    : (void)0 )
+#endif
+
+/*
+ * Simplified macro to send an info log message using the current LOG_TAG.
+ */
+#ifndef LOGI
+#define LOGI(...) ((void)LOG(LOG_INFO, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef LOGI_IF
+#define LOGI_IF(cond, ...) \
+    ( (CONDITION(cond)) \
+    ? ((void)LOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \
+    : (void)0 )
+#endif
+
+/*
+ * Simplified macro to send a warning log message using the current LOG_TAG.
+ */
+#ifndef LOGW
+#define LOGW(...) ((void)LOG(LOG_WARN, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef LOGW_IF
+#define LOGW_IF(cond, ...) \
+    ( (CONDITION(cond)) \
+    ? ((void)LOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \
+    : (void)0 )
+#endif
+
+/*
+ * Simplified macro to send an error log message using the current LOG_TAG.
+ */
+#ifndef LOGE
+#define LOGE(...) ((void)LOG(LOG_ERROR, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef LOGE_IF
+#define LOGE_IF(cond, ...) \
+    ( (CONDITION(cond)) \
+    ? ((void)LOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \
+    : (void)0 )
+#endif
+
+
+/*
+ * Conditional based on whether the current LOG_TAG is enabled at
+ * verbose priority.
+ */
+#ifndef IF_LOGV
+#if LOG_NDEBUG
+#define IF_LOGV() if (false)
+#else
+#define IF_LOGV() IF_LOG(LOG_VERBOSE, LOG_TAG)
+#endif
+#endif
+
+/*
+ * Conditional based on whether the current LOG_TAG is enabled at
+ * debug priority.
+ */
+#ifndef IF_LOGD
+#define IF_LOGD() IF_LOG(LOG_DEBUG, LOG_TAG)
+#endif
+
+/*
+ * Conditional based on whether the current LOG_TAG is enabled at
+ * info priority.
+ */
+#ifndef IF_LOGI
+#define IF_LOGI() IF_LOG(LOG_INFO, LOG_TAG)
+#endif
+
+/*
+ * Conditional based on whether the current LOG_TAG is enabled at
+ * warn priority.
+ */
+#ifndef IF_LOGW
+#define IF_LOGW() IF_LOG(LOG_WARN, LOG_TAG)
+#endif
+
+/*
+ * Conditional based on whether the current LOG_TAG is enabled at
+ * error priority.
+ */
+#ifndef IF_LOGE
+#define IF_LOGE() IF_LOG(LOG_ERROR, LOG_TAG)
+#endif
+
+// ---------------------------------------------------------------------
+
+/*
+ * Basic log message macro.
+ *
+ * Example:
+ *  LOG(LOG_WARN, NULL, "Failed with error %d", errno);
+ *
+ * The second argument may be NULL or "" to indicate the "global" tag.
+ *
+ * Non-gcc probably won't have __FUNCTION__.  It's not vital.  gcc also
+ * offers __PRETTY_FUNCTION__, which is rather more than we need.
+ */
+#ifndef LOG
+#define LOG(priority, tag, ...) \
+    LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)
+#endif
+
+/*
+ * Log macro that allows you to specify a number for the priority.
+ */
+#ifndef LOG_PRI
+#define LOG_PRI(priority, tag, ...) \
+    printf(tag ": " __VA_ARGS__)
+#endif
+
+/*
+ * Conditional given a desired logging priority and tag.
+ */
+#ifndef IF_LOG
+#define IF_LOG(priority, tag) \
+    if (1)
+#endif
+
+#endif // _MINZIP_LOG_H
diff --git a/recovery/minzip/SysUtil.c b/recovery/minzip/SysUtil.c
new file mode 100644
index 0000000..3f1fce1
--- /dev/null
+++ b/recovery/minzip/SysUtil.c
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * System utilities.
+ */
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define LOG_TAG "sysutil"
+#include "Log.h"
+#include "SysUtil.h"
+
+static bool sysMapFD(int fd, MemMapping* pMap) {
+    assert(pMap != NULL);
+
+    struct stat sb;
+    if (fstat(fd, &sb) == -1) {
+        LOGE("fstat(%d) failed: %s\n", fd, strerror(errno));
+        return false;
+    }
+
+    void* memPtr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+    if (memPtr == MAP_FAILED) {
+        LOGE("mmap(%d, R, PRIVATE, %d, 0) failed: %s\n", (int) sb.st_size, fd, strerror(errno));
+        return false;
+    }
+
+    pMap->addr = memPtr;
+    pMap->length = sb.st_size;
+    pMap->range_count = 1;
+    pMap->ranges = malloc(sizeof(MappedRange));
+    if (pMap->ranges == NULL) {
+        LOGE("malloc failed: %s\n", strerror(errno));
+        munmap(memPtr, length);
+        return -1;
+    }
+    pMap->ranges[0].addr = memPtr;
+    pMap->ranges[0].length = sb.st_size;
+
+    return true;
+}
+
+static int sysMapBlockFile(FILE* mapf, MemMapping* pMap)
+{
+    char block_dev[PATH_MAX+1];
+    size_t size;
+    unsigned int blksize;
+    size_t blocks;
+    unsigned int range_count;
+    unsigned int i;
+
+    if (fgets(block_dev, sizeof(block_dev), mapf) == NULL) {
+        LOGE("failed to read block device from header\n");
+        return -1;
+    }
+    for (i = 0; i < sizeof(block_dev); ++i) {
+        if (block_dev[i] == '\n') {
+            block_dev[i] = 0;
+            break;
+        }
+    }
+
+    if (fscanf(mapf, "%zu %u\n%u\n", &size, &blksize, &range_count) != 3) {
+        LOGE("failed to parse block map header\n");
+        return -1;
+    }
+    if (blksize != 0) {
+        blocks = ((size-1) / blksize) + 1;
+    }
+    if (size == 0 || blksize == 0 || blocks > SIZE_MAX / blksize || range_count == 0) {
+        LOGE("invalid data in block map file: size %zu, blksize %u, range_count %u\n",
+             size, blksize, range_count);
+        return -1;
+    }
+
+    pMap->range_count = range_count;
+    pMap->ranges = calloc(range_count, sizeof(MappedRange));
+    if (pMap->ranges == NULL) {
+        LOGE("calloc(%u, %zu) failed: %s\n", range_count, sizeof(MappedRange), strerror(errno));
+        return -1;
+    }
+
+    // Reserve enough contiguous address space for the whole file.
+    unsigned char* reserve;
+    reserve = mmap64(NULL, blocks * blksize, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
+    if (reserve == MAP_FAILED) {
+        LOGW("failed to reserve address space: %s\n", strerror(errno));
+        free(pMap->ranges);
+        return -1;
+    }
+
+    int fd = open(block_dev, O_RDONLY);
+    if (fd < 0) {
+        LOGW("failed to open block device %s: %s\n", block_dev, strerror(errno));
+        munmap(reserve, blocks * blksize);
+        free(pMap->ranges);
+        return -1;
+    }
+
+    unsigned char* next = reserve;
+    size_t remaining_size = blocks * blksize;
+    bool success = true;
+    for (i = 0; i < range_count; ++i) {
+        size_t start, end;
+        if (fscanf(mapf, "%zu %zu\n", &start, &end) != 2) {
+            LOGW("failed to parse range %d in block map\n", i);
+            success = false;
+            break;
+        }
+        size_t length = (end - start) * blksize;
+        if (end <= start || (end - start) > SIZE_MAX / blksize || length > remaining_size) {
+          LOGE("unexpected range in block map: %zu %zu\n", start, end);
+          success = false;
+          break;
+        }
+
+        void* addr = mmap64(next, length, PROT_READ, MAP_PRIVATE | MAP_FIXED, fd, ((off64_t)start)*blksize);
+        if (addr == MAP_FAILED) {
+            LOGW("failed to map block %d: %s\n", i, strerror(errno));
+            success = false;
+            break;
+        }
+        pMap->ranges[i].addr = addr;
+        pMap->ranges[i].length = length;
+
+        next += length;
+        remaining_size -= length;
+    }
+    if (success && remaining_size != 0) {
+      LOGE("ranges in block map are invalid: remaining_size = %zu\n", remaining_size);
+      success = false;
+    }
+    if (!success) {
+      close(fd);
+      munmap(reserve, blocks * blksize);
+      free(pMap->ranges);
+      return -1;
+    }
+
+    close(fd);
+    pMap->addr = reserve;
+    pMap->length = size;
+
+    LOGI("mmapped %d ranges\n", range_count);
+
+    return 0;
+}
+
+int sysMapFile(const char* fn, MemMapping* pMap)
+{
+    memset(pMap, 0, sizeof(*pMap));
+
+    if (fn && fn[0] == '@') {
+        // A map of blocks
+        FILE* mapf = fopen(fn+1, "r");
+        if (mapf == NULL) {
+            LOGE("Unable to open '%s': %s\n", fn+1, strerror(errno));
+            return -1;
+        }
+
+        if (sysMapBlockFile(mapf, pMap) != 0) {
+            LOGW("Map of '%s' failed\n", fn);
+            fclose(mapf);
+            return -1;
+        }
+
+        fclose(mapf);
+    } else {
+        // This is a regular file.
+        int fd = open(fn, O_RDONLY);
+        if (fd == -1) {
+            LOGE("Unable to open '%s': %s\n", fn, strerror(errno));
+            return -1;
+        }
+
+        if (!sysMapFD(fd, pMap)) {
+            LOGE("Map of '%s' failed\n", fn);
+            close(fd);
+            return -1;
+        }
+
+        close(fd);
+    }
+    return 0;
+}
+
+/*
+ * Release a memory mapping.
+ */
+void sysReleaseMap(MemMapping* pMap)
+{
+    int i;
+    for (i = 0; i < pMap->range_count; ++i) {
+        if (munmap(pMap->ranges[i].addr, pMap->ranges[i].length) < 0) {
+            LOGE("munmap(%p, %d) failed: %s\n",
+                 pMap->ranges[i].addr, (int)pMap->ranges[i].length, strerror(errno));
+        }
+    }
+    free(pMap->ranges);
+    pMap->ranges = NULL;
+    pMap->range_count = 0;
+}
diff --git a/recovery/minzip/SysUtil.h b/recovery/minzip/SysUtil.h
new file mode 100644
index 0000000..7adff1e
--- /dev/null
+++ b/recovery/minzip/SysUtil.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * System utilities.
+ */
+#ifndef _MINZIP_SYSUTIL
+#define _MINZIP_SYSUTIL
+
+#include <stdio.h>
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct MappedRange {
+    void* addr;
+    size_t length;
+} MappedRange;
+
+/*
+ * Use this to keep track of mapped segments.
+ */
+typedef struct MemMapping {
+    unsigned char* addr;           /* start of data */
+    size_t         length;         /* length of data */
+
+    int            range_count;
+    MappedRange*   ranges;
+} MemMapping;
+
+/*
+ * Map a file into a private, read-only memory segment.  If 'fn'
+ * begins with an '@' character, it is a map of blocks to be mapped,
+ * otherwise it is treated as an ordinary file.
+ *
+ * On success, "pMap" is filled in, and zero is returned.
+ */
+int sysMapFile(const char* fn, MemMapping* pMap);
+
+/*
+ * Release the pages associated with a shared memory segment.
+ *
+ * This does not free "pMap"; it just releases the memory.
+ */
+void sysReleaseMap(MemMapping* pMap);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*_MINZIP_SYSUTIL*/
diff --git a/recovery/minzip/Zip.c b/recovery/minzip/Zip.c
new file mode 100644
index 0000000..d557daa
--- /dev/null
+++ b/recovery/minzip/Zip.c
@@ -0,0 +1,1018 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Simple Zip file support.
+ */
+#include "safe_iop.h"
+#include "zlib.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>     // for uintptr_t
+#include <stdlib.h>
+#include <sys/stat.h>   // for S_ISLNK()
+#include <unistd.h>
+
+#define LOG_TAG "minzip"
+#include "Zip.h"
+#include "Bits.h"
+#include "Log.h"
+#include "DirUtil.h"
+
+#undef NDEBUG   // do this after including Log.h
+#include <assert.h>
+
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#define SORT_ENTRIES 1
+
+/*
+ * Offset and length constants (java.util.zip naming convention).
+ */
+enum {
+    CENSIG = 0x02014b50,      // PK12
+    CENHDR = 46,
+
+    CENVEM =  4,
+    CENVER =  6,
+    CENFLG =  8,
+    CENHOW = 10,
+    CENTIM = 12,
+    CENCRC = 16,
+    CENSIZ = 20,
+    CENLEN = 24,
+    CENNAM = 28,
+    CENEXT = 30,
+    CENCOM = 32,
+    CENDSK = 34,
+    CENATT = 36,
+    CENATX = 38,
+    CENOFF = 42,
+
+    ENDSIG = 0x06054b50,     // PK56
+    ENDHDR = 22,
+
+    ENDSUB =  8,
+    ENDTOT = 10,
+    ENDSIZ = 12,
+    ENDOFF = 16,
+    ENDCOM = 20,
+
+    EXTSIG = 0x08074b50,     // PK78
+    EXTHDR = 16,
+
+    EXTCRC =  4,
+    EXTSIZ =  8,
+    EXTLEN = 12,
+
+    LOCSIG = 0x04034b50,      // PK34
+    LOCHDR = 30,
+
+    LOCVER =  4,
+    LOCFLG =  6,
+    LOCHOW =  8,
+    LOCTIM = 10,
+    LOCCRC = 14,
+    LOCSIZ = 18,
+    LOCLEN = 22,
+    LOCNAM = 26,
+    LOCEXT = 28,
+
+    STORED = 0,
+    DEFLATED = 8,
+
+    CENVEM_UNIX = 3 << 8,   // the high byte of CENVEM
+};
+
+
+/*
+ * For debugging, dump the contents of a ZipEntry.
+ */
+#if 0
+static void dumpEntry(const ZipEntry* pEntry)
+{
+    LOGI(" %p '%.*s'\n", pEntry->fileName,pEntry->fileNameLen,pEntry->fileName);
+    LOGI("   off=%u comp=%u uncomp=%u how=%d\n", pEntry->offset,
+        pEntry->compLen, pEntry->uncompLen, pEntry->compression);
+}
+#endif
+
+/*
+ * (This is a mzHashTableLookup callback.)
+ *
+ * Compare two ZipEntry structs, by name.
+ */
+static int hashcmpZipEntry(const void* ventry1, const void* ventry2)
+{
+    const ZipEntry* entry1 = (const ZipEntry*) ventry1;
+    const ZipEntry* entry2 = (const ZipEntry*) ventry2;
+
+    if (entry1->fileNameLen != entry2->fileNameLen)
+        return entry1->fileNameLen - entry2->fileNameLen;
+    return memcmp(entry1->fileName, entry2->fileName, entry1->fileNameLen);
+}
+
+/*
+ * (This is a mzHashTableLookup callback.)
+ *
+ * find a ZipEntry struct by name.
+ */
+static int hashcmpZipName(const void* ventry, const void* vname)
+{
+    const ZipEntry* entry = (const ZipEntry*) ventry;
+    const char* name = (const char*) vname;
+    unsigned int nameLen = strlen(name);
+
+    if (entry->fileNameLen != nameLen)
+        return entry->fileNameLen - nameLen;
+    return memcmp(entry->fileName, name, nameLen);
+}
+
+/*
+ * Compute the hash code for a ZipEntry filename.
+ *
+ * Not expected to be compatible with any other hash function, so we init
+ * to 2 to ensure it doesn't happen to match.
+ */
+static unsigned int computeHash(const char* name, int nameLen)
+{
+    unsigned int hash = 2;
+
+    while (nameLen--)
+        hash = hash * 31 + *name++;
+
+    return hash;
+}
+
+static void addEntryToHashTable(HashTable* pHash, ZipEntry* pEntry)
+{
+    unsigned int itemHash = computeHash(pEntry->fileName, pEntry->fileNameLen);
+    const ZipEntry* found;
+
+    found = (const ZipEntry*)mzHashTableLookup(pHash,
+                itemHash, pEntry, hashcmpZipEntry, true);
+    if (found != pEntry) {
+        LOGW("WARNING: duplicate entry '%.*s' in Zip\n",
+            found->fileNameLen, found->fileName);
+        /* keep going */
+    }
+}
+
+static int validFilename(const char *fileName, unsigned int fileNameLen)
+{
+    // Forbid super long filenames.
+    if (fileNameLen >= PATH_MAX) {
+        LOGW("Filename too long (%d chatacters)\n", fileNameLen);
+        return 0;
+    }
+
+    // Require all characters to be printable ASCII (no NUL, no UTF-8, etc).
+    unsigned int i;
+    for (i = 0; i < fileNameLen; ++i) {
+        if (fileName[i] < 32 || fileName[i] >= 127) {
+            LOGW("Filename contains invalid character '\%03o'\n", fileName[i]);
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+/*
+ * Parse the contents of a Zip archive.  After confirming that the file
+ * is in fact a Zip, we scan out the contents of the central directory and
+ * store it in a hash table.
+ *
+ * Returns "true" on success.
+ */
+static bool parseZipArchive(ZipArchive* pArchive)
+{
+    bool result = false;
+    const unsigned char* ptr;
+    unsigned int i, numEntries, cdOffset;
+    unsigned int val;
+
+    /*
+     * The first 4 bytes of the file will either be the local header
+     * signature for the first file (LOCSIG) or, if the archive doesn't
+     * have any files in it, the end-of-central-directory signature (ENDSIG).
+     */
+    val = get4LE(pArchive->addr);
+    if (val == ENDSIG) {
+        LOGW("Found Zip archive, but it looks empty\n");
+        goto bail;
+    } else if (val != LOCSIG) {
+        LOGW("Not a Zip archive (found 0x%08x)\n", val);
+        goto bail;
+    }
+
+    /*
+     * Find the EOCD.  We'll find it immediately unless they have a file
+     * comment.
+     */
+    ptr = pArchive->addr + pArchive->length - ENDHDR;
+
+    while (ptr >= (const unsigned char*) pArchive->addr) {
+        if (*ptr == (ENDSIG & 0xff) && get4LE(ptr) == ENDSIG)
+            break;
+        ptr--;
+    }
+    if (ptr < (const unsigned char*) pArchive->addr) {
+        LOGW("Could not find end-of-central-directory in Zip\n");
+        goto bail;
+    }
+
+    /*
+     * There are two interesting items in the EOCD block: the number of
+     * entries in the file, and the file offset of the start of the
+     * central directory.
+     */
+    numEntries = get2LE(ptr + ENDSUB);
+    cdOffset = get4LE(ptr + ENDOFF);
+
+    LOGVV("numEntries=%d cdOffset=%d\n", numEntries, cdOffset);
+    if (numEntries == 0 || cdOffset >= pArchive->length) {
+        LOGW("Invalid entries=%d offset=%d (len=%zd)\n",
+            numEntries, cdOffset, pArchive->length);
+        goto bail;
+    }
+
+    /*
+     * Create data structures to hold entries.
+     */
+    pArchive->numEntries = numEntries;
+    pArchive->pEntries = (ZipEntry*) calloc(numEntries, sizeof(ZipEntry));
+    pArchive->pHash = mzHashTableCreate(mzHashSize(numEntries), NULL);
+    if (pArchive->pEntries == NULL || pArchive->pHash == NULL)
+        goto bail;
+
+    ptr = pArchive->addr + cdOffset;
+    for (i = 0; i < numEntries; i++) {
+        ZipEntry* pEntry;
+        unsigned int fileNameLen, extraLen, commentLen, localHdrOffset;
+        const unsigned char* localHdr;
+        const char *fileName;
+
+        if (ptr + CENHDR > (const unsigned char*)pArchive->addr + pArchive->length) {
+            LOGW("Ran off the end (at %d)\n", i);
+            goto bail;
+        }
+        if (get4LE(ptr) != CENSIG) {
+            LOGW("Missed a central dir sig (at %d)\n", i);
+            goto bail;
+        }
+
+        localHdrOffset = get4LE(ptr + CENOFF);
+        fileNameLen = get2LE(ptr + CENNAM);
+        extraLen = get2LE(ptr + CENEXT);
+        commentLen = get2LE(ptr + CENCOM);
+        fileName = (const char*)ptr + CENHDR;
+        if (fileName + fileNameLen > (const char*)pArchive->addr + pArchive->length) {
+            LOGW("Filename ran off the end (at %d)\n", i);
+            goto bail;
+        }
+        if (!validFilename(fileName, fileNameLen)) {
+            LOGW("Invalid filename (at %d)\n", i);
+            goto bail;
+        }
+
+#if SORT_ENTRIES
+        /* Figure out where this entry should go (binary search).
+         */
+        if (i > 0) {
+            int low, high;
+
+            low = 0;
+            high = i - 1;
+            while (low <= high) {
+                int mid;
+                int diff;
+                int diffLen;
+
+                mid = low + ((high - low) / 2); // avoid overflow
+
+                if (pArchive->pEntries[mid].fileNameLen < fileNameLen) {
+                    diffLen = pArchive->pEntries[mid].fileNameLen;
+                } else {
+                    diffLen = fileNameLen;
+                }
+                diff = strncmp(pArchive->pEntries[mid].fileName, fileName,
+                        diffLen);
+                if (diff == 0) {
+                    diff = pArchive->pEntries[mid].fileNameLen - fileNameLen;
+                }
+                if (diff < 0) {
+                    low = mid + 1;
+                } else if (diff > 0) {
+                    high = mid - 1;
+                } else {
+                    high = mid;
+                    break;
+                }
+            }
+
+            unsigned int target = high + 1;
+            assert(target <= i);
+            if (target != i) {
+                /* It belongs somewhere other than at the end of
+                 * the list.  Make some room at [target].
+                 */
+                memmove(pArchive->pEntries + target + 1,
+                        pArchive->pEntries + target,
+                        (i - target) * sizeof(ZipEntry));
+            }
+            pEntry = &pArchive->pEntries[target];
+        } else {
+            pEntry = &pArchive->pEntries[0];
+        }
+#else
+        pEntry = &pArchive->pEntries[i];
+#endif
+        pEntry->fileNameLen = fileNameLen;
+        pEntry->fileName = fileName;
+
+        pEntry->compLen = get4LE(ptr + CENSIZ);
+        pEntry->uncompLen = get4LE(ptr + CENLEN);
+        pEntry->compression = get2LE(ptr + CENHOW);
+        pEntry->modTime = get4LE(ptr + CENTIM);
+        pEntry->crc32 = get4LE(ptr + CENCRC);
+
+        /* These two are necessary for finding the mode of the file.
+         */
+        pEntry->versionMadeBy = get2LE(ptr + CENVEM);
+        if ((pEntry->versionMadeBy & 0xff00) != 0 &&
+                (pEntry->versionMadeBy & 0xff00) != CENVEM_UNIX)
+        {
+            LOGW("Incompatible \"version made by\": 0x%02x (at %d)\n",
+                    pEntry->versionMadeBy >> 8, i);
+            goto bail;
+        }
+        pEntry->externalFileAttributes = get4LE(ptr + CENATX);
+
+        // Perform pArchive->addr + localHdrOffset, ensuring that it won't
+        // overflow. This is needed because localHdrOffset is untrusted.
+        if (!safe_add((uintptr_t *)&localHdr, (uintptr_t)pArchive->addr,
+            (uintptr_t)localHdrOffset)) {
+            LOGW("Integer overflow adding in parseZipArchive\n");
+            goto bail;
+        }
+        if ((uintptr_t)localHdr + LOCHDR >
+            (uintptr_t)pArchive->addr + pArchive->length) {
+            LOGW("Bad offset to local header: %d (at %d)\n", localHdrOffset, i);
+            goto bail;
+        }
+        if (get4LE(localHdr) != LOCSIG) {
+            LOGW("Missed a local header sig (at %d)\n", i);
+            goto bail;
+        }
+        pEntry->offset = localHdrOffset + LOCHDR
+            + get2LE(localHdr + LOCNAM) + get2LE(localHdr + LOCEXT);
+        if (!safe_add(NULL, pEntry->offset, pEntry->compLen)) {
+            LOGW("Integer overflow adding in parseZipArchive\n");
+            goto bail;
+        }
+        if ((size_t)pEntry->offset + pEntry->compLen > pArchive->length) {
+            LOGW("Data ran off the end (at %d)\n", i);
+            goto bail;
+        }
+
+#if !SORT_ENTRIES
+        /* Add to hash table; no need to lock here.
+         * Can't do this now if we're sorting, because entries
+         * will move around.
+         */
+        addEntryToHashTable(pArchive->pHash, pEntry);
+#endif
+
+        //dumpEntry(pEntry);
+        ptr += CENHDR + fileNameLen + extraLen + commentLen;
+    }
+
+#if SORT_ENTRIES
+    /* If we're sorting, we have to wait until all entries
+     * are in their final places, otherwise the pointers will
+     * probably point to the wrong things.
+     */
+    for (i = 0; i < numEntries; i++) {
+        /* Add to hash table; no need to lock here.
+         */
+        addEntryToHashTable(pArchive->pHash, &pArchive->pEntries[i]);
+    }
+#endif
+
+    result = true;
+
+bail:
+    if (!result) {
+        mzHashTableFree(pArchive->pHash);
+        pArchive->pHash = NULL;
+    }
+    return result;
+}
+
+/*
+ * Open a Zip archive and scan out the contents.
+ *
+ * The easiest way to do this is to mmap() the whole thing and do the
+ * traditional backward scan for central directory.  Since the EOCD is
+ * a relatively small bit at the end, we should end up only touching a
+ * small set of pages.
+ *
+ * This will be called on non-Zip files, especially during startup, so
+ * we don't want to be too noisy about failures.  (Do we want a "quiet"
+ * flag?)
+ *
+ * On success, we fill out the contents of "pArchive".
+ */
+int mzOpenZipArchive(unsigned char* addr, size_t length, ZipArchive* pArchive)
+{
+    int err;
+
+    if (length < ENDHDR) {
+        err = -1;
+        LOGW("Archive %p is too small to be zip (%zd)\n", pArchive, length);
+        goto bail;
+    }
+
+    pArchive->addr = addr;
+    pArchive->length = length;
+
+    if (!parseZipArchive(pArchive)) {
+        err = -1;
+        LOGW("Parsing archive %p failed\n", pArchive);
+        goto bail;
+    }
+
+    err = 0;
+
+bail:
+    if (err != 0)
+        mzCloseZipArchive(pArchive);
+    return err;
+}
+
+/*
+ * Close a ZipArchive, closing the file and freeing the contents.
+ *
+ * NOTE: the ZipArchive may not have been fully created.
+ */
+void mzCloseZipArchive(ZipArchive* pArchive)
+{
+    LOGV("Closing archive %p\n", pArchive);
+
+    free(pArchive->pEntries);
+
+    mzHashTableFree(pArchive->pHash);
+
+    pArchive->pHash = NULL;
+    pArchive->pEntries = NULL;
+}
+
+/*
+ * Find a matching entry.
+ *
+ * Returns NULL if no matching entry found.
+ */
+const ZipEntry* mzFindZipEntry(const ZipArchive* pArchive,
+        const char* entryName)
+{
+    unsigned int itemHash = computeHash(entryName, strlen(entryName));
+
+    return (const ZipEntry*)mzHashTableLookup(pArchive->pHash,
+                itemHash, (char*) entryName, hashcmpZipName, false);
+}
+
+/*
+ * Return true if the entry is a symbolic link.
+ */
+static bool mzIsZipEntrySymlink(const ZipEntry* pEntry)
+{
+    if ((pEntry->versionMadeBy & 0xff00) == CENVEM_UNIX) {
+        return S_ISLNK(pEntry->externalFileAttributes >> 16);
+    }
+    return false;
+}
+
+/* Call processFunction on the uncompressed data of a STORED entry.
+ */
+static bool processStoredEntry(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, ProcessZipEntryContentsFunction processFunction,
+    void *cookie)
+{
+    return processFunction(pArchive->addr + pEntry->offset, pEntry->uncompLen, cookie);
+}
+
+static bool processDeflatedEntry(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, ProcessZipEntryContentsFunction processFunction,
+    void *cookie)
+{
+    bool success = false;
+    unsigned long totalOut = 0;
+    unsigned char procBuf[32 * 1024];
+    z_stream zstream;
+    int zerr;
+
+    /*
+     * Initialize the zlib stream.
+     */
+    memset(&zstream, 0, sizeof(zstream));
+    zstream.zalloc = Z_NULL;
+    zstream.zfree = Z_NULL;
+    zstream.opaque = Z_NULL;
+    zstream.next_in = pArchive->addr + pEntry->offset;
+    zstream.avail_in = pEntry->compLen;
+    zstream.next_out = (Bytef*) procBuf;
+    zstream.avail_out = sizeof(procBuf);
+    zstream.data_type = Z_UNKNOWN;
+
+    /*
+     * Use the undocumented "negative window bits" feature to tell zlib
+     * that there's no zlib header waiting for it.
+     */
+    zerr = inflateInit2(&zstream, -MAX_WBITS);
+    if (zerr != Z_OK) {
+        if (zerr == Z_VERSION_ERROR) {
+            LOGE("Installed zlib is not compatible with linked version (%s)\n",
+                ZLIB_VERSION);
+        } else {
+            LOGE("Call to inflateInit2 failed (zerr=%d)\n", zerr);
+        }
+        goto bail;
+    }
+
+    /*
+     * Loop while we have data.
+     */
+    do {
+        /* uncompress the data */
+        zerr = inflate(&zstream, Z_NO_FLUSH);
+        if (zerr != Z_OK && zerr != Z_STREAM_END) {
+            LOGW("zlib inflate call failed (zerr=%d)\n", zerr);
+            goto z_bail;
+        }
+
+        /* write when we're full or when we're done */
+        if (zstream.avail_out == 0 ||
+            (zerr == Z_STREAM_END && zstream.avail_out != sizeof(procBuf)))
+        {
+            long procSize = zstream.next_out - procBuf;
+            LOGVV("+++ processing %d bytes\n", (int) procSize);
+            bool ret = processFunction(procBuf, procSize, cookie);
+            if (!ret) {
+                LOGW("Process function elected to fail (in inflate)\n");
+                goto z_bail;
+            }
+
+            zstream.next_out = procBuf;
+            zstream.avail_out = sizeof(procBuf);
+        }
+    } while (zerr == Z_OK);
+
+    assert(zerr == Z_STREAM_END);       /* other errors should've been caught */
+
+    // success!
+    totalOut = zstream.total_out;
+    success = true;
+
+z_bail:
+    inflateEnd(&zstream);        /* free up any allocated structures */
+
+bail:
+    if (totalOut != pEntry->uncompLen) {
+        if (success) {       // error already shown?
+            LOGW("Size mismatch on inflated file (%lu vs %u)\n", totalOut, pEntry->uncompLen);
+        }
+        return false;
+    }
+    return true;
+}
+
+/*
+ * Stream the uncompressed data through the supplied function,
+ * passing cookie to it each time it gets called.  processFunction
+ * may be called more than once.
+ *
+ * If processFunction returns false, the operation is abandoned and
+ * mzProcessZipEntryContents() immediately returns false.
+ *
+ * This is useful for calculating the hash of an entry's uncompressed contents.
+ */
+bool mzProcessZipEntryContents(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, ProcessZipEntryContentsFunction processFunction,
+    void *cookie)
+{
+    bool ret = false;
+
+    switch (pEntry->compression) {
+    case STORED:
+        ret = processStoredEntry(pArchive, pEntry, processFunction, cookie);
+        break;
+    case DEFLATED:
+        ret = processDeflatedEntry(pArchive, pEntry, processFunction, cookie);
+        break;
+    default:
+        LOGE("Unsupported compression type %d for entry '%s'\n",
+                pEntry->compression, pEntry->fileName);
+        break;
+    }
+
+    return ret;
+}
+
+typedef struct {
+    char *buf;
+    int bufLen;
+} CopyProcessArgs;
+
+static bool copyProcessFunction(const unsigned char *data, int dataLen,
+        void *cookie)
+{
+    CopyProcessArgs *args = (CopyProcessArgs *)cookie;
+    if (dataLen <= args->bufLen) {
+        memcpy(args->buf, data, dataLen);
+        args->buf += dataLen;
+        args->bufLen -= dataLen;
+        return true;
+    }
+    return false;
+}
+
+/*
+ * Read an entry into a buffer allocated by the caller.
+ */
+bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry,
+        char *buf, int bufLen)
+{
+    CopyProcessArgs args;
+    bool ret;
+
+    args.buf = buf;
+    args.bufLen = bufLen;
+    ret = mzProcessZipEntryContents(pArchive, pEntry, copyProcessFunction,
+            (void *)&args);
+    if (!ret) {
+        LOGE("Can't extract entry to buffer.\n");
+        return false;
+    }
+    return true;
+}
+
+static bool writeProcessFunction(const unsigned char *data, int dataLen,
+                                 void *cookie)
+{
+    int fd = (int)(intptr_t)cookie;
+    if (dataLen == 0) {
+        return true;
+    }
+    ssize_t soFar = 0;
+    while (true) {
+        ssize_t n = TEMP_FAILURE_RETRY(write(fd, data+soFar, dataLen-soFar));
+        if (n <= 0) {
+            LOGE("Error writing %zd bytes from zip file from %p: %s\n",
+                 dataLen-soFar, data+soFar, strerror(errno));
+            return false;
+        } else if (n > 0) {
+            soFar += n;
+            if (soFar == dataLen) return true;
+            if (soFar > dataLen) {
+                LOGE("write overrun?  (%zd bytes instead of %d)\n",
+                     soFar, dataLen);
+                return false;
+            }
+        }
+    }
+}
+
+/*
+ * Uncompress "pEntry" in "pArchive" to "fd" at the current offset.
+ */
+bool mzExtractZipEntryToFile(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, int fd)
+{
+    bool ret = mzProcessZipEntryContents(pArchive, pEntry, writeProcessFunction,
+                                         (void*)(intptr_t)fd);
+    if (!ret) {
+        LOGE("Can't extract entry to file.\n");
+        return false;
+    }
+    return true;
+}
+
+typedef struct {
+    unsigned char* buffer;
+    long len;
+} BufferExtractCookie;
+
+static bool bufferProcessFunction(const unsigned char *data, int dataLen,
+    void *cookie) {
+    BufferExtractCookie *bec = (BufferExtractCookie*)cookie;
+
+    memmove(bec->buffer, data, dataLen);
+    bec->buffer += dataLen;
+    bec->len -= dataLen;
+
+    return true;
+}
+
+/*
+ * Uncompress "pEntry" in "pArchive" to buffer, which must be large
+ * enough to hold mzGetZipEntryUncomplen(pEntry) bytes.
+ */
+bool mzExtractZipEntryToBuffer(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, unsigned char *buffer)
+{
+    BufferExtractCookie bec;
+    bec.buffer = buffer;
+    bec.len = mzGetZipEntryUncompLen(pEntry);
+
+    bool ret = mzProcessZipEntryContents(pArchive, pEntry,
+        bufferProcessFunction, (void*)&bec);
+    if (!ret || bec.len != 0) {
+        LOGE("Can't extract entry to memory buffer.\n");
+        return false;
+    }
+    return true;
+}
+
+
+/* Helper state to make path translation easier and less malloc-happy.
+ */
+typedef struct {
+    const char *targetDir;
+    const char *zipDir;
+    char *buf;
+    int targetDirLen;
+    int zipDirLen;
+    int bufLen;
+} MzPathHelper;
+
+/* Given the values of targetDir and zipDir in the helper,
+ * return the target filename of the provided entry.
+ * The helper must be initialized first.
+ */
+static const char *targetEntryPath(MzPathHelper *helper, ZipEntry *pEntry)
+{
+    int needLen;
+    bool firstTime = (helper->buf == NULL);
+
+    /* target file <-- targetDir + / + entry[zipDirLen:]
+     */
+    needLen = helper->targetDirLen + 1 +
+            pEntry->fileNameLen - helper->zipDirLen + 1;
+    if (firstTime || needLen > helper->bufLen) {
+        char *newBuf;
+
+        needLen *= 2;
+        newBuf = (char *)realloc(helper->buf, needLen);
+        if (newBuf == NULL) {
+            return NULL;
+        }
+        helper->buf = newBuf;
+        helper->bufLen = needLen;
+    }
+
+    /* Every path will start with the target path and a slash.
+     */
+    if (firstTime) {
+        char *p = helper->buf;
+        memcpy(p, helper->targetDir, helper->targetDirLen);
+        p += helper->targetDirLen;
+        if (p == helper->buf || p[-1] != '/') {
+            helper->targetDirLen += 1;
+            *p++ = '/';
+        }
+    }
+
+    /* Replace the custom part of the path with the appropriate
+     * part of the entry's path.
+     */
+    char *epath = helper->buf + helper->targetDirLen;
+    memcpy(epath, pEntry->fileName + helper->zipDirLen,
+            pEntry->fileNameLen - helper->zipDirLen);
+    epath += pEntry->fileNameLen - helper->zipDirLen;
+    *epath = '\0';
+
+    return helper->buf;
+}
+
+/*
+ * Inflate all entries under zipDir to the directory specified by
+ * targetDir, which must exist and be a writable directory.
+ *
+ * The immediate children of zipDir will become the immediate
+ * children of targetDir; e.g., if the archive contains the entries
+ *
+ *     a/b/c/one
+ *     a/b/c/two
+ *     a/b/c/d/three
+ *
+ * and mzExtractRecursive(a, "a/b/c", "/tmp") is called, the resulting
+ * files will be
+ *
+ *     /tmp/one
+ *     /tmp/two
+ *     /tmp/d/three
+ *
+ * Returns true on success, false on failure.
+ */
+bool mzExtractRecursive(const ZipArchive *pArchive,
+                        const char *zipDir, const char *targetDir,
+                        const struct utimbuf *timestamp,
+                        void (*callback)(const char *fn, void *), void *cookie,
+                        struct selabel_handle *sehnd)
+{
+    if (zipDir[0] == '/') {
+        LOGE("mzExtractRecursive(): zipDir must be a relative path.\n");
+        return false;
+    }
+    if (targetDir[0] != '/') {
+        LOGE("mzExtractRecursive(): targetDir must be an absolute path.\n");
+        return false;
+    }
+
+    unsigned int zipDirLen;
+    char *zpath;
+
+    zipDirLen = strlen(zipDir);
+    zpath = (char *)malloc(zipDirLen + 2);
+    if (zpath == NULL) {
+        LOGE("Can't allocate %d bytes for zip path\n", zipDirLen + 2);
+        return false;
+    }
+    /* If zipDir is empty, we'll extract the entire zip file.
+     * Otherwise, canonicalize the path.
+     */
+    if (zipDirLen > 0) {
+        /* Make sure there's (hopefully, exactly one) slash at the
+         * end of the path.  This way we don't need to worry about
+         * accidentally extracting "one/twothree" when a path like
+         * "one/two" is specified.
+         */
+        memcpy(zpath, zipDir, zipDirLen);
+        if (zpath[zipDirLen-1] != '/') {
+            zpath[zipDirLen++] = '/';
+        }
+    }
+    zpath[zipDirLen] = '\0';
+
+    /* Set up the helper structure that we'll use to assemble paths.
+     */
+    MzPathHelper helper;
+    helper.targetDir = targetDir;
+    helper.targetDirLen = strlen(helper.targetDir);
+    helper.zipDir = zpath;
+    helper.zipDirLen = strlen(helper.zipDir);
+    helper.buf = NULL;
+    helper.bufLen = 0;
+
+    /* Walk through the entries and extract anything whose path begins
+     * with zpath.
+    //TODO: since the entries are sorted, binary search for the first match
+    //      and stop after the first non-match.
+     */
+    unsigned int i;
+    bool seenMatch = false;
+    int ok = true;
+    int extractCount = 0;
+    for (i = 0; i < pArchive->numEntries; i++) {
+        ZipEntry *pEntry = pArchive->pEntries + i;
+        if (pEntry->fileNameLen < zipDirLen) {
+       //TODO: look out for a single empty directory entry that matches zpath, but
+       //      missing the trailing slash.  Most zip files seem to include
+       //      the trailing slash, but I think it's legal to leave it off.
+       //      e.g., zpath "a/b/", entry "a/b", with no children of the entry.
+            /* No chance of matching.
+             */
+#if SORT_ENTRIES
+            if (seenMatch) {
+                /* Since the entries are sorted, we can give up
+                 * on the first mismatch after the first match.
+                 */
+                break;
+            }
+#endif
+            continue;
+        }
+        /* If zpath is empty, this strncmp() will match everything,
+         * which is what we want.
+         */
+        if (strncmp(pEntry->fileName, zpath, zipDirLen) != 0) {
+#if SORT_ENTRIES
+            if (seenMatch) {
+                /* Since the entries are sorted, we can give up
+                 * on the first mismatch after the first match.
+                 */
+                break;
+            }
+#endif
+            continue;
+        }
+        /* This entry begins with zipDir, so we'll extract it.
+         */
+        seenMatch = true;
+
+        /* Find the target location of the entry.
+         */
+        const char *targetFile = targetEntryPath(&helper, pEntry);
+        if (targetFile == NULL) {
+            LOGE("Can't assemble target path for \"%.*s\"\n",
+                    pEntry->fileNameLen, pEntry->fileName);
+            ok = false;
+            break;
+        }
+
+#define UNZIP_DIRMODE 0755
+#define UNZIP_FILEMODE 0644
+        /*
+         * Create the file or directory. We ignore directory entries
+         * because we recursively create paths to each file entry we encounter
+         * in the zip archive anyway.
+         *
+         * NOTE: A "directory entry" in a zip archive is just a zero length
+         * entry that ends in a "/". They're not mandatory and many tools get
+         * rid of them. We need to process them only if we want to preserve
+         * empty directories from the archive.
+         */
+        if (pEntry->fileName[pEntry->fileNameLen-1] != '/') {
+            /* This is not a directory.  First, make sure that
+             * the containing directory exists.
+             */
+            int ret = dirCreateHierarchy(
+                    targetFile, UNZIP_DIRMODE, timestamp, true, sehnd);
+            if (ret != 0) {
+                LOGE("Can't create containing directory for \"%s\": %s\n",
+                        targetFile, strerror(errno));
+                ok = false;
+                break;
+            }
+
+            /*
+             * The entry is a regular file or a symlink. Open the target for writing.
+             *
+             * TODO: This behavior for symlinks seems rather bizarre. For a
+             * symlink foo/bar/baz -> foo/tar/taz, we will create a file called
+             * "foo/bar/baz" whose contents are the literal "foo/tar/taz". We
+             * warn about this for now and preserve older behavior.
+             */
+            if (mzIsZipEntrySymlink(pEntry)) {
+                LOGE("Symlink entry \"%.*s\" will be output as a regular file.",
+                     pEntry->fileNameLen, pEntry->fileName);
+            }
+
+            char *secontext = NULL;
+
+            if (sehnd) {
+                selabel_lookup(sehnd, &secontext, targetFile, UNZIP_FILEMODE);
+                setfscreatecon(secontext);
+            }
+
+            int fd = open(targetFile, O_CREAT|O_WRONLY|O_TRUNC,
+                UNZIP_FILEMODE);
+
+            if (secontext) {
+                freecon(secontext);
+                setfscreatecon(NULL);
+            }
+
+            if (fd < 0) {
+                LOGE("Can't create target file \"%s\": %s\n",
+                        targetFile, strerror(errno));
+                ok = false;
+                break;
+            }
+
+            bool ok = mzExtractZipEntryToFile(pArchive, pEntry, fd);
+            if (ok) {
+                ok = (fsync(fd) == 0);
+            }
+            if (close(fd) != 0) {
+                ok = false;
+            }
+            if (!ok) {
+                LOGE("Error extracting \"%s\"\n", targetFile);
+                ok = false;
+                break;
+            }
+
+            if (timestamp != NULL && utime(targetFile, timestamp)) {
+                LOGE("Error touching \"%s\"\n", targetFile);
+                ok = false;
+                break;
+            }
+
+            LOGV("Extracted file \"%s\"\n", targetFile);
+            ++extractCount;
+        }
+
+        if (callback != NULL) callback(targetFile, cookie);
+    }
+
+    LOGV("Extracted %d file(s)\n", extractCount);
+
+    free(helper.buf);
+    free(zpath);
+
+    return ok;
+}
diff --git a/recovery/minzip/Zip.h b/recovery/minzip/Zip.h
new file mode 100644
index 0000000..c932c11
--- /dev/null
+++ b/recovery/minzip/Zip.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Simple Zip archive support.
+ */
+#ifndef _MINZIP_ZIP
+#define _MINZIP_ZIP
+
+#include "inline_magic.h"
+
+#include <stdlib.h>
+#include <utime.h>
+
+#include "Hash.h"
+#include "SysUtil.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct selabel_handle;
+
+/*
+ * One entry in the Zip archive.  Treat this as opaque -- use accessors below.
+ *
+ * TODO: we're now keeping the pages mapped so we don't have to copy the
+ * filename.  We can change the accessors to retrieve the various pieces
+ * directly from the source file instead of copying them out, for a very
+ * slight speed hit and a modest reduction in memory usage.
+ */
+typedef struct ZipEntry {
+    unsigned int fileNameLen;
+    const char*  fileName;       // not null-terminated
+    uint32_t     offset;
+    uint32_t     compLen;
+    uint32_t     uncompLen;
+    int          compression;
+    long         modTime;
+    long         crc32;
+    int          versionMadeBy;
+    long         externalFileAttributes;
+} ZipEntry;
+
+/*
+ * One Zip archive.  Treat as opaque.
+ */
+typedef struct ZipArchive {
+    unsigned int   numEntries;
+    ZipEntry*      pEntries;
+    HashTable*     pHash;          // maps file name to ZipEntry
+    unsigned char* addr;
+    size_t         length;
+} ZipArchive;
+
+/*
+ * Represents a non-NUL-terminated string,
+ * which is how entry names are stored.
+ */
+typedef struct {
+    const char *str;
+    size_t len;
+} UnterminatedString;
+
+/*
+ * Open a Zip archive.
+ *
+ * On success, returns 0 and populates "pArchive".  Returns nonzero errno
+ * value on failure.
+ */
+int mzOpenZipArchive(unsigned char* addr, size_t length, ZipArchive* pArchive);
+
+/*
+ * Close archive, releasing resources associated with it.
+ *
+ * Depending on the implementation this could unmap pages used by classes
+ * stored in a Jar.  This should only be done after unloading classes.
+ */
+void mzCloseZipArchive(ZipArchive* pArchive);
+
+
+/*
+ * Find an entry in the Zip archive, by name.
+ */
+const ZipEntry* mzFindZipEntry(const ZipArchive* pArchive,
+        const char* entryName);
+
+INLINE uint32_t mzGetZipEntryOffset(const ZipEntry* pEntry) {
+    return pEntry->offset;
+}
+INLINE uint32_t mzGetZipEntryUncompLen(const ZipEntry* pEntry) {
+    return pEntry->uncompLen;
+}
+
+/*
+ * Type definition for the callback function used by
+ * mzProcessZipEntryContents().
+ */
+typedef bool (*ProcessZipEntryContentsFunction)(const unsigned char *data,
+    int dataLen, void *cookie);
+
+/*
+ * Stream the uncompressed data through the supplied function,
+ * passing cookie to it each time it gets called.  processFunction
+ * may be called more than once.
+ *
+ * If processFunction returns false, the operation is abandoned and
+ * mzProcessZipEntryContents() immediately returns false.
+ *
+ * This is useful for calculating the hash of an entry's uncompressed contents.
+ */
+bool mzProcessZipEntryContents(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, ProcessZipEntryContentsFunction processFunction,
+    void *cookie);
+
+/*
+ * Read an entry into a buffer allocated by the caller.
+ */
+bool mzReadZipEntry(const ZipArchive* pArchive, const ZipEntry* pEntry,
+        char* buf, int bufLen);
+
+/*
+ * Inflate and write an entry to a file.
+ */
+bool mzExtractZipEntryToFile(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, int fd);
+
+/*
+ * Inflate and write an entry to a memory buffer, which must be long
+ * enough to hold mzGetZipEntryUncomplen(pEntry) bytes.
+ */
+bool mzExtractZipEntryToBuffer(const ZipArchive *pArchive,
+    const ZipEntry *pEntry, unsigned char* buffer);
+
+/*
+ * Inflate all files under zipDir to the directory specified by
+ * targetDir, which must exist and be a writable directory.
+ *
+ * Directory entries and symlinks are not extracted.
+ *
+ *
+ * The immediate children of zipDir will become the immediate
+ * children of targetDir; e.g., if the archive contains the entries
+ *
+ *     a/b/c/one
+ *     a/b/c/two
+ *     a/b/c/d/three
+ *
+ * and mzExtractRecursive(a, "a/b/c", "/tmp", ...) is called, the resulting
+ * files will be
+ *
+ *     /tmp/one
+ *     /tmp/two
+ *     /tmp/d/three
+ *
+ * If timestamp is non-NULL, file timestamps will be set accordingly.
+ *
+ * If callback is non-NULL, it will be invoked with each unpacked file.
+ *
+ * Returns true on success, false on failure.
+ */
+bool mzExtractRecursive(const ZipArchive *pArchive,
+        const char *zipDir, const char *targetDir,
+        const struct utimbuf *timestamp,
+        void (*callback)(const char *fn, void*), void *cookie,
+        struct selabel_handle *sehnd);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*_MINZIP_ZIP*/
diff --git a/recovery/minzip/inline_magic.h b/recovery/minzip/inline_magic.h
new file mode 100644
index 0000000..59c659f
--- /dev/null
+++ b/recovery/minzip/inline_magic.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef MINZIP_INLINE_MAGIC_H_
+#define MINZIP_INLINE_MAGIC_H_
+
+#ifndef MINZIP_GENERATE_INLINES
+#define INLINE extern inline __attribute((__gnu_inline__))
+#else
+#define INLINE
+#endif
+
+#endif  // MINZIP_INLINE_MAGIC_H_
diff --git a/recovery/mounts.cpp b/recovery/mounts.cpp
new file mode 100644
index 0000000..f23376b
--- /dev/null
+++ b/recovery/mounts.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include "mounts.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <mntent.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+
+#include <string>
+#include <vector>
+
+struct MountedVolume {
+    std::string device;
+    std::string mount_point;
+    std::string filesystem;
+    std::string flags;
+};
+
+std::vector<MountedVolume*> g_mounts_state;
+
+bool scan_mounted_volumes() {
+    for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+        delete g_mounts_state[i];
+    }
+    g_mounts_state.clear();
+
+    // Open and read mount table entries.
+    FILE* fp = setmntent("/proc/mounts", "re");
+    if (fp == NULL) {
+        return false;
+    }
+    mntent* e;
+    while ((e = getmntent(fp)) != NULL) {
+        MountedVolume* v = new MountedVolume;
+        v->device = e->mnt_fsname;
+        v->mount_point = e->mnt_dir;
+        v->filesystem = e->mnt_type;
+        v->flags = e->mnt_opts;
+        g_mounts_state.push_back(v);
+    }
+    endmntent(fp);
+    return true;
+}
+
+MountedVolume* find_mounted_volume_by_device(const char* device) {
+    for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+        if (g_mounts_state[i]->device == device) return g_mounts_state[i];
+    }
+    return nullptr;
+}
+
+MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point) {
+    for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+        if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i];
+    }
+    return nullptr;
+}
+
+int unmount_mounted_volume(MountedVolume* volume) {
+    // Intentionally pass the empty string to umount if the caller tries
+    // to unmount a volume they already unmounted using this
+    // function.
+    std::string mount_point = volume->mount_point;
+    volume->mount_point.clear();
+    return umount(mount_point.c_str());
+}
+
+int remount_read_only(MountedVolume* volume) {
+    return mount(volume->device.c_str(), volume->mount_point.c_str(), volume->filesystem.c_str(),
+                 MS_NOATIME | MS_NODEV | MS_NODIRATIME | MS_RDONLY | MS_REMOUNT, 0);
+}
diff --git a/recovery/mounts.h b/recovery/mounts.h
new file mode 100644
index 0000000..1b76703
--- /dev/null
+++ b/recovery/mounts.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef MOUNTS_H_
+#define MOUNTS_H_
+
+struct MountedVolume;
+
+bool scan_mounted_volumes();
+
+MountedVolume* find_mounted_volume_by_device(const char* device);
+
+MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
+
+int unmount_mounted_volume(MountedVolume* volume);
+
+int remount_read_only(MountedVolume* volume);
+
+#endif
diff --git a/recovery/otafault/Android.mk b/recovery/otafault/Android.mk
new file mode 100644
index 0000000..50e385e
--- /dev/null
+++ b/recovery/otafault/Android.mk
@@ -0,0 +1,49 @@
+# Copyright 2015 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 languae governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# otafault (static library)
+# ===============================
+include $(CLEAR_VARS)
+
+otafault_static_libs := \
+    libbase \
+    libminzip \
+    libz \
+    libselinux
+
+LOCAL_SRC_FILES := config.cpp ota_io.cpp
+LOCAL_MODULE := libotafault
+LOCAL_CLANG := true
+LOCAL_C_INCLUDES := bootable/recovery
+LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)
+LOCAL_STATIC_LIBRARIES := $(otafault_static_libs)
+
+include $(BUILD_STATIC_LIBRARY)
+
+# otafault_test (static executable)
+# ===============================
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := config.cpp ota_io.cpp test.cpp
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := otafault_test
+LOCAL_STATIC_LIBRARIES := \
+    libotafault \
+    $(otafault_static_libs)
+LOCAL_C_INCLUDES := bootable/recovery
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+
+include $(BUILD_EXECUTABLE)
diff --git a/recovery/otafault/config.cpp b/recovery/otafault/config.cpp
new file mode 100644
index 0000000..b456739
--- /dev/null
+++ b/recovery/otafault/config.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <map>
+#include <string>
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <android-base/stringprintf.h>
+
+#include "minzip/Zip.h"
+#include "config.h"
+#include "ota_io.h"
+
+#define OTAIO_MAX_FNAME_SIZE 128
+
+static ZipArchive* archive;
+static std::map<std::string, bool> should_inject_cache;
+
+static std::string get_type_path(const char* io_type) {
+    return android::base::StringPrintf("%s/%s", OTAIO_BASE_DIR, io_type);
+}
+
+void ota_io_init(ZipArchive* za) {
+    archive = za;
+    ota_set_fault_files();
+}
+
+bool should_fault_inject(const char* io_type) {
+    // archive will be NULL if we used an entry point other
+    // than updater/updater.cpp:main
+    if (archive == NULL) {
+        return false;
+    }
+    const std::string type_path = get_type_path(io_type);
+    if (should_inject_cache.find(type_path) != should_inject_cache.end()) {
+        return should_inject_cache[type_path];
+    }
+    const ZipEntry* entry = mzFindZipEntry(archive, type_path.c_str());
+    should_inject_cache[type_path] = entry != nullptr;
+    return entry != NULL;
+}
+
+bool should_hit_cache() {
+    return should_fault_inject(OTAIO_CACHE);
+}
+
+std::string fault_fname(const char* io_type) {
+    std::string type_path = get_type_path(io_type);
+    std::string fname;
+    fname.resize(OTAIO_MAX_FNAME_SIZE);
+    const ZipEntry* entry = mzFindZipEntry(archive, type_path.c_str());
+    mzReadZipEntry(archive, entry, &fname[0], OTAIO_MAX_FNAME_SIZE);
+    return fname;
+}
diff --git a/recovery/otafault/config.h b/recovery/otafault/config.h
new file mode 100644
index 0000000..4430be3
--- /dev/null
+++ b/recovery/otafault/config.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+/*
+ * Read configuration files in the OTA package to determine which files, if any, will trigger errors.
+ *
+ * OTA packages can be modified to trigger errors by adding a top-level
+ * directory called .libotafault, which may optionally contain up to three
+ * files called READ, WRITE, and FSYNC. Each one of these optional files
+ * contains the name of a single file on the device disk which will cause
+ * an IO error on the first call of the appropriate I/O action to that file.
+ *
+ * Example:
+ * ota.zip
+ *   <normal package contents>
+ *   .libotafault
+ *     WRITE
+ *
+ * If the contents of the file WRITE were /system/build.prop, the first write
+ * action to /system/build.prop would fail with EIO. Note that READ and
+ * FSYNC files are absent, so these actions will not cause an error.
+ */
+
+#ifndef _UPDATER_OTA_IO_CFG_H_
+#define _UPDATER_OTA_IO_CFG_H_
+
+#include <string>
+
+#include <stdbool.h>
+
+#include "minzip/Zip.h"
+
+#define OTAIO_BASE_DIR ".libotafault"
+#define OTAIO_READ "READ"
+#define OTAIO_WRITE "WRITE"
+#define OTAIO_FSYNC "FSYNC"
+#define OTAIO_CACHE "CACHE"
+
+/*
+ * Initialize libotafault by providing a reference to the OTA package.
+ */
+void ota_io_init(ZipArchive* za);
+
+/*
+ * Return true if a config file is present for the given IO type.
+ */
+bool should_fault_inject(const char* io_type);
+
+/*
+ * Return true if an EIO should occur on the next hit to /cache/saved.file
+ * instead of the next hit to the specified file.
+ */
+bool should_hit_cache();
+
+/*
+ * Return the name of the file that should cause an error for the
+ * given IO type.
+ */
+std::string fault_fname(const char* io_type);
+
+#endif
diff --git a/recovery/otafault/ota_io.cpp b/recovery/otafault/ota_io.cpp
new file mode 100644
index 0000000..cb37d51
--- /dev/null
+++ b/recovery/otafault/ota_io.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <map>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "ota_io.h"
+
+static std::map<intptr_t, const char*> filename_cache;
+static std::string read_fault_file_name = "";
+static std::string write_fault_file_name = "";
+static std::string fsync_fault_file_name = "";
+bool have_eio_error = false;
+
+static bool get_hit_file(const char* cached_path, const std::string& ffn) {
+    return should_hit_cache()
+        ? !strncmp(cached_path, OTAIO_CACHE_FNAME, strlen(cached_path))
+        : !strncmp(cached_path, ffn.c_str(), strlen(cached_path));
+}
+
+void ota_set_fault_files() {
+    if (should_fault_inject(OTAIO_READ)) {
+        read_fault_file_name = fault_fname(OTAIO_READ);
+    }
+    if (should_fault_inject(OTAIO_WRITE)) {
+        write_fault_file_name = fault_fname(OTAIO_WRITE);
+    }
+    if (should_fault_inject(OTAIO_FSYNC)) {
+        fsync_fault_file_name = fault_fname(OTAIO_FSYNC);
+    }
+}
+
+int ota_open(const char* path, int oflags) {
+    // Let the caller handle errors; we do not care if open succeeds or fails
+    int fd = open(path, oflags);
+    filename_cache[fd] = path;
+    return fd;
+}
+
+int ota_open(const char* path, int oflags, mode_t mode) {
+    int fd = open(path, oflags, mode);
+    filename_cache[fd] = path;
+    return fd; }
+
+FILE* ota_fopen(const char* path, const char* mode) {
+    FILE* fh = fopen(path, mode);
+    filename_cache[(intptr_t)fh] = path;
+    return fh;
+}
+
+int ota_close(int fd) {
+    // descriptors can be reused, so make sure not to leave them in the cache
+    filename_cache.erase(fd);
+    return close(fd);
+}
+
+int ota_fclose(FILE* fh) {
+    filename_cache.erase((intptr_t)fh);
+    return fclose(fh);
+}
+
+size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream) {
+    if (should_fault_inject(OTAIO_READ)) {
+        auto cached = filename_cache.find((intptr_t)stream);
+        const char* cached_path = cached->second;
+        if (cached != filename_cache.end() &&
+                get_hit_file(cached_path, read_fault_file_name)) {
+            read_fault_file_name = "";
+            errno = EIO;
+            have_eio_error = true;
+            return 0;
+        }
+    }
+    size_t status = fread(ptr, size, nitems, stream);
+    // If I/O error occurs, set the retry-update flag.
+    if (status != nitems && errno == EIO) {
+        have_eio_error = true;
+    }
+    return status;
+}
+
+ssize_t ota_read(int fd, void* buf, size_t nbyte) {
+    if (should_fault_inject(OTAIO_READ)) {
+        auto cached = filename_cache.find(fd);
+        const char* cached_path = cached->second;
+        if (cached != filename_cache.end()
+                && get_hit_file(cached_path, read_fault_file_name)) {
+            read_fault_file_name = "";
+            errno = EIO;
+            have_eio_error = true;
+            return -1;
+        }
+    }
+    ssize_t status = read(fd, buf, nbyte);
+    if (status == -1 && errno == EIO) {
+        have_eio_error = true;
+    }
+    return status;
+}
+
+size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream) {
+    if (should_fault_inject(OTAIO_WRITE)) {
+        auto cached = filename_cache.find((intptr_t)stream);
+        const char* cached_path = cached->second;
+        if (cached != filename_cache.end() &&
+                get_hit_file(cached_path, write_fault_file_name)) {
+            write_fault_file_name = "";
+            errno = EIO;
+            have_eio_error = true;
+            return 0;
+        }
+    }
+    size_t status = fwrite(ptr, size, count, stream);
+    if (status != count && errno == EIO) {
+        have_eio_error = true;
+    }
+    return status;
+}
+
+ssize_t ota_write(int fd, const void* buf, size_t nbyte) {
+    if (should_fault_inject(OTAIO_WRITE)) {
+        auto cached = filename_cache.find(fd);
+        const char* cached_path = cached->second;
+        if (cached != filename_cache.end() &&
+                get_hit_file(cached_path, write_fault_file_name)) {
+            write_fault_file_name = "";
+            errno = EIO;
+            have_eio_error = true;
+            return -1;
+        }
+    }
+    ssize_t status = write(fd, buf, nbyte);
+    if (status == -1 && errno == EIO) {
+        have_eio_error = true;
+    }
+    return status;
+}
+
+int ota_fsync(int fd) {
+    if (should_fault_inject(OTAIO_FSYNC)) {
+        auto cached = filename_cache.find(fd);
+        const char* cached_path = cached->second;
+        if (cached != filename_cache.end() &&
+                get_hit_file(cached_path, fsync_fault_file_name)) {
+            fsync_fault_file_name = "";
+            errno = EIO;
+            have_eio_error = true;
+            return -1;
+        }
+    }
+    int status = fsync(fd);
+    if (status == -1 && errno == EIO) {
+        have_eio_error = true;
+    }
+    return status;
+}
+
diff --git a/recovery/otafault/ota_io.h b/recovery/otafault/ota_io.h
new file mode 100644
index 0000000..84187a7
--- /dev/null
+++ b/recovery/otafault/ota_io.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+/*
+ * Provide a series of proxy functions for basic file accessors.
+ * The behavior of these functions can be changed to return different
+ * errors under a variety of conditions.
+ */
+
+#ifndef _UPDATER_OTA_IO_H_
+#define _UPDATER_OTA_IO_H_
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#define OTAIO_CACHE_FNAME "/cache/saved.file"
+
+void ota_set_fault_files();
+
+int ota_open(const char* path, int oflags);
+
+int ota_open(const char* path, int oflags, mode_t mode);
+
+FILE* ota_fopen(const char* filename, const char* mode);
+
+int ota_close(int fd);
+
+int ota_fclose(FILE* fh);
+
+size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream);
+
+ssize_t ota_read(int fd, void* buf, size_t nbyte);
+
+size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
+
+ssize_t ota_write(int fd, const void* buf, size_t nbyte);
+
+int ota_fsync(int fd);
+
+#endif
diff --git a/recovery/otafault/test.cpp b/recovery/otafault/test.cpp
new file mode 100644
index 0000000..6514782
--- /dev/null
+++ b/recovery/otafault/test.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "ota_io.h"
+
+int main(int /* argc */, char** /* argv */) {
+    int fd = open("testdata/test.file", O_RDWR);
+    char buf[8];
+    const char* out = "321";
+    int readv = ota_read(fd, buf, 4);
+    printf("Read returned %d\n", readv);
+    int writev = ota_write(fd, out, 4);
+    printf("Write returned %d\n", writev);
+    close(fd);
+    return 0;
+}
diff --git a/recovery/print_sha1.h b/recovery/print_sha1.h
new file mode 100644
index 0000000..fa3d7e0
--- /dev/null
+++ b/recovery/print_sha1.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef RECOVERY_PRINT_SHA1_H
+#define RECOVERY_PRINT_SHA1_H
+
+#include <stdint.h>
+#include <string>
+
+#include "openssl/sha.h"
+
+static std::string print_sha1(const uint8_t sha1[SHA_DIGEST_LENGTH], size_t len) {
+    const char* hex = "0123456789abcdef";
+    std::string result = "";
+    for (size_t i = 0; i < len; ++i) {
+        result.push_back(hex[(sha1[i]>>4) & 0xf]);
+        result.push_back(hex[sha1[i] & 0xf]);
+    }
+    return result;
+}
+
+static std::string print_sha1(const uint8_t sha1[SHA_DIGEST_LENGTH]) {
+    return print_sha1(sha1, SHA_DIGEST_LENGTH);
+}
+
+static std::string short_sha1(const uint8_t sha1[SHA_DIGEST_LENGTH]) {
+    return print_sha1(sha1, 4);
+}
+
+#endif  // RECOVERY_PRINT_SHA1_H
diff --git a/recovery/recovery-persist.cpp b/recovery/recovery-persist.cpp
new file mode 100644
index 0000000..25df03f
--- /dev/null
+++ b/recovery/recovery-persist.cpp
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#define LOG_TAG "recovery-persist"
+
+//
+// Strictly to deal with reboot into system after OTA after /data
+// mounts to pull the last pmsg file data and place it
+// into /data/misc/recovery/ directory, rotating it in.
+//
+// Usage: recovery-persist [--force-persist]
+//
+//    On systems without /cache mount, all file content representing in the
+//    recovery/ directory stored in /sys/fs/pstore/pmsg-ramoops-0 in logger
+//    format that reside in the LOG_ID_SYSTEM buffer at ANDROID_LOG_INFO
+//    priority or higher is transfered to the /data/misc/recovery/ directory.
+//    The content is matched and rotated in as need be.
+//
+//    --force-persist  ignore /cache mount, always rotate in the contents.
+//
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <string>
+
+#include <android/log.h> /* Android Log Priority Tags */
+#include <android-base/file.h>
+#include <log/log.h>
+#include <log/logger.h> /* Android Log packet format */
+#include <private/android_logger.h> /* private pmsg functions */
+
+static const char *LAST_LOG_FILE = "/data/misc/recovery/last_log";
+static const char *LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
+static const char *LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
+static const char *LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
+static const char *ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
+
+static const int KEEP_LOG_COUNT = 10;
+
+// close a file, log an error if the error indicator is set
+static void check_and_fclose(FILE *fp, const char *name) {
+    fflush(fp);
+    if (ferror(fp)) SLOGE("%s %s", name, strerror(errno));
+    fclose(fp);
+}
+
+static void copy_file(const char* source, const char* destination) {
+    FILE* dest_fp = fopen(destination, "w");
+    if (dest_fp == nullptr) {
+        SLOGE("%s %s", destination, strerror(errno));
+    } else {
+        FILE* source_fp = fopen(source, "r");
+        if (source_fp != nullptr) {
+            char buf[4096];
+            size_t bytes;
+            while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
+                fwrite(buf, 1, bytes, dest_fp);
+            }
+            check_and_fclose(source_fp, source);
+        }
+        check_and_fclose(dest_fp, destination);
+    }
+}
+
+static bool rotated = false;
+
+// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max.
+// Similarly rename last_kmsg -> last_kmsg.1 -> ... -> last_kmsg.$max.
+// Overwrite any existing last_log.$max and last_kmsg.$max.
+static void rotate_logs(int max) {
+    // Logs should only be rotated once.
+
+    if (rotated) {
+        return;
+    }
+    rotated = true;
+
+    for (int i = max-1; i >= 0; --i) {
+        std::string old_log(LAST_LOG_FILE);
+        if (i > 0) {
+          old_log += "." + std::to_string(i);
+        }
+        std::string new_log(LAST_LOG_FILE);
+        new_log += "." + std::to_string(i+1);
+
+        // Ignore errors if old_log doesn't exist.
+        rename(old_log.c_str(), new_log.c_str());
+
+        std::string old_kmsg(LAST_KMSG_FILE);
+        if (i > 0) {
+          old_kmsg += "." + std::to_string(i);
+        }
+        std::string new_kmsg(LAST_KMSG_FILE);
+        new_kmsg += "." + std::to_string(i+1);
+
+        rename(old_kmsg.c_str(), new_kmsg.c_str());
+    }
+}
+
+ssize_t logsave(
+        log_id_t /* logId */,
+        char /* prio */,
+        const char *filename,
+        const char *buf, size_t len,
+        void * /* arg */) {
+
+    std::string destination("/data/misc/");
+    destination += filename;
+
+    std::string buffer(buf, len);
+
+    {
+        std::string content;
+        android::base::ReadFileToString(destination, &content);
+
+        if (buffer.compare(content) == 0) {
+            return len;
+        }
+    }
+
+    // ToDo: Any others that match? Are we pulling in multiple
+    // already-rotated files? Algorithm thus far is KISS: one file,
+    // one rotation allowed.
+
+    rotate_logs(KEEP_LOG_COUNT);
+
+    return android::base::WriteStringToFile(buffer, destination.c_str());
+}
+
+int main(int argc, char **argv) {
+
+    /* Is /cache a mount?, we have been delivered where we are not wanted */
+    /*
+     * Following code halves the size of the executable as compared to:
+     *
+     *    load_volume_table();
+     *    has_cache = volume_for_path(CACHE_ROOT) != nullptr;
+     */
+    bool has_cache = false;
+    static const char mounts_file[] = "/proc/mounts";
+    FILE *fp = fopen(mounts_file, "r");
+    if (!fp) {
+        SLOGV("%s %s", mounts_file, strerror(errno));
+    } else {
+        char *line = NULL;
+        size_t len = 0;
+        ssize_t read;
+        while ((read = getline(&line, &len, fp)) != -1) {
+            if (strstr(line, " /cache ")) {
+                has_cache = true;
+                break;
+            }
+        }
+        free(line);
+        fclose(fp);
+    }
+
+    if (has_cache) {
+        /*
+         * TBD: Future location to move content from
+         * /cache/recovery to /data/misc/recovery/
+         */
+        /* if --force-persist flag, then transfer pmsg data anyways */
+        if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
+            return 0;
+        }
+    }
+
+    /* Is there something in pmsg? */
+    if (access(LAST_PMSG_FILE, R_OK)) {
+        return 0;
+    }
+
+    // Take last pmsg file contents and send it off to the logsave
+    __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", logsave, NULL);
+
+    /* Is there a last console log too? */
+    if (rotated) {
+        if (!access(LAST_CONSOLE_FILE, R_OK)) {
+            copy_file(LAST_CONSOLE_FILE, LAST_KMSG_FILE);
+        } else if (!access(ALT_LAST_CONSOLE_FILE, R_OK)) {
+            copy_file(ALT_LAST_CONSOLE_FILE, LAST_KMSG_FILE);
+        }
+    }
+
+    return 0;
+}
diff --git a/recovery/recovery-persist.rc b/recovery/recovery-persist.rc
new file mode 100644
index 0000000..6761627
--- /dev/null
+++ b/recovery/recovery-persist.rc
@@ -0,0 +1,3 @@
+on post-fs-data
+    mkdir /data/misc/recovery 0770 system log
+    exec - system log -- /system/bin/recovery-persist
diff --git a/recovery/recovery-refresh.cpp b/recovery/recovery-refresh.cpp
new file mode 100644
index 0000000..b75c915
--- /dev/null
+++ b/recovery/recovery-refresh.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#define LOG_TAG "recovery-refresh"
+
+//
+// Strictly to deal with reboot into system after OTA, then
+// reboot while in system before boot complete landing us back
+// into recovery to continue with any mitigations with retained
+// log history. This simply refreshes the pmsg files from
+// the last pmsg file contents.
+//
+// Usage:
+//    recovery-refresh [--force-rotate|--rotate]
+//
+//    All file content representing in the recovery/ directory stored in
+//    /sys/fs/pstore/pmsg-ramoops-0 in logger format that reside in the
+//    LOG_ID_SYSTEM buffer at ANDROID_LOG_INFO priority or higher is
+//    refreshed into /dev/pmsg0. This ensures that an unexpected reboot
+//    before recovery-persist is run will still contain the associated
+//    pmsg Android Logger content.
+//
+//    --force-rotate  recovery/last_kmsg and recovery.last_log files are
+//                    rotated with .<number> suffixes upwards.
+//    --rotate        rotated only if rocovery/last_msg or recovery/last_log
+//                    exist, otherwise perform 1:1 refresh.
+//
+
+#include <string.h>
+
+#include <string>
+
+#include <android/log.h> /* Android Log Priority Tags */
+#include <log/logger.h> /* Android Log packet format */
+#include <private/android_logger.h> /* private pmsg functions */
+
+static const char LAST_KMSG_FILE[] = "recovery/last_kmsg";
+static const char LAST_LOG_FILE[] = "recovery/last_log";
+
+static ssize_t logbasename(
+        log_id_t /* logId */,
+        char /* prio */,
+        const char *filename,
+        const char * /* buf */, size_t len,
+        void *arg) {
+    if (strstr(LAST_KMSG_FILE, filename) ||
+            strstr(LAST_LOG_FILE, filename)) {
+        bool *doRotate = reinterpret_cast<bool *>(arg);
+        *doRotate = true;
+    }
+    return len;
+}
+
+static ssize_t logrotate(
+        log_id_t logId,
+        char prio,
+        const char *filename,
+        const char *buf, size_t len,
+        void *arg) {
+    bool *doRotate = reinterpret_cast<bool *>(arg);
+    if (!*doRotate) {
+        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
+    }
+
+    std::string name(filename);
+    size_t dot = name.find_last_of('.');
+    std::string sub = name.substr(0, dot);
+
+    if (!strstr(LAST_KMSG_FILE, sub.c_str()) &&
+                !strstr(LAST_LOG_FILE, sub.c_str())) {
+        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
+    }
+
+    // filename rotation
+    if (dot == std::string::npos) {
+        name += ".1";
+    } else {
+        std::string number = name.substr(dot + 1);
+        if (!isdigit(number.data()[0])) {
+            name += ".1";
+        } else {
+            auto i = std::stoull(number);
+            name = sub + "." + std::to_string(i + 1);
+        }
+    }
+
+    return __android_log_pmsg_file_write(logId, prio, name.c_str(), buf, len);
+}
+
+int main(int argc, char **argv) {
+    static const char filter[] = "recovery/";
+    static const char force_rotate_flag[] = "--force-rotate";
+    static const char rotate_flag[] = "--rotate";
+    ssize_t ret;
+    bool doRotate = false;
+
+    // Take last pmsg contents and rewrite it to the current pmsg session.
+    if ((argc <= 1) || !argv[1] ||
+            (((doRotate = strcmp(argv[1], rotate_flag))) &&
+                strcmp(argv[1], force_rotate_flag))) {
+        doRotate = false;
+    } else if (!doRotate) {
+        // Do we need to rotate?
+        __android_log_pmsg_file_read(
+            LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
+            logbasename, &doRotate);
+    }
+
+    // Take action to refresh pmsg contents
+    ret = __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
+        logrotate, &doRotate);
+
+    return (ret < 0) ? ret : 0;
+}
diff --git a/recovery/recovery-refresh.rc b/recovery/recovery-refresh.rc
new file mode 100644
index 0000000..14b05cc
--- /dev/null
+++ b/recovery/recovery-refresh.rc
@@ -0,0 +1,2 @@
+on post-fs
+    exec - system log -- /system/bin/recovery-refresh
diff --git a/recovery/recovery.cpp b/recovery/recovery.cpp
new file mode 100644
index 0000000..0dfcce0
--- /dev/null
+++ b/recovery/recovery.cpp
@@ -0,0 +1,1585 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/input.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/klog.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <chrono>
+#include <string>
+#include <vector>
+
+#include <adb.h>
+#include <android/log.h> /* Android Log Priority Tags */
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <cutils/android_reboot.h>
+#include <cutils/properties.h>
+#include <healthd/BatteryMonitor.h>
+#include <log/logger.h> /* Android Log packet format */
+#include <private/android_logger.h> /* private pmsg functions */
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#include "adb_install.h"
+#include "bootloader.h"
+#include "common.h"
+#include "device.h"
+#include "fuse_sdcard_provider.h"
+#include "fuse_sideload.h"
+#include "install.h"
+#include "minui/minui.h"
+#include "minzip/DirUtil.h"
+#include "roots.h"
+#include "ui.h"
+#include "screen_ui.h"
+
+struct selabel_handle *sehandle;
+
+static const struct option OPTIONS[] = {
+  { "update_package", required_argument, NULL, 'u' },
+  { "retry_count", required_argument, NULL, 'n' },
+  { "wipe_data", no_argument, NULL, 'w' },
+  { "wipe_cache", no_argument, NULL, 'c' },
+  { "show_text", no_argument, NULL, 't' },
+  { "sideload", no_argument, NULL, 's' },
+  { "sideload_auto_reboot", no_argument, NULL, 'a' },
+  { "just_exit", no_argument, NULL, 'x' },
+  { "locale", required_argument, NULL, 'l' },
+  { "stages", required_argument, NULL, 'g' },
+  { "shutdown_after", no_argument, NULL, 'p' },
+  { "reason", required_argument, NULL, 'r' },
+  { "brick", no_argument, NULL, 0 },
+  { NULL, 0, NULL, 0 },
+};
+
+static const char *CACHE_LOG_DIR = "/cache/recovery";
+static const char *COMMAND_FILE = "/cache/recovery/command";
+static const char *LOG_FILE = "/cache/recovery/log";
+static const char *LAST_INSTALL_FILE = "/cache/recovery/last_install";
+static const char *LOCALE_FILE = "/cache/recovery/last_locale";
+static const char *CACHE_ROOT = "/cache";
+static const char *SDCARD_ROOT = "/sdcard";
+static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
+static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
+static const char *LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+static const char *LAST_LOG_FILE = "/cache/recovery/last_log";
+static const int KEEP_LOG_COUNT = 10;
+static const int EIO_RETRY_COUNT = 2;
+static const int BATTERY_READ_TIMEOUT_IN_SEC = 10;
+// GmsCore enters recovery mode to install package when having enough battery
+// percentage. Normally, the threshold is 40% without charger and 20% with charger.
+// So we should check battery with a slightly lower limitation.
+static const int BATTERY_OK_PERCENTAGE = 20;
+static const int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
+constexpr const char* RECOVERY_BRICK = "/etc/recovery.brick";
+
+RecoveryUI* ui = NULL;
+char* locale = NULL;
+char* stage = NULL;
+char* reason = NULL;
+bool modified_flash = false;
+static bool has_cache = false;
+
+/*
+ * The recovery tool communicates with the main system through /cache files.
+ *   /cache/recovery/command - INPUT - command line for tool, one arg per line
+ *   /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
+ *
+ * The arguments which may be supplied in the recovery.command file:
+ *   --update_package=path - verify install an OTA package file
+ *   --wipe_data - erase user data (and cache), then reboot
+ *   --wipe_cache - wipe cache (but not user data), then reboot
+ *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
+ *   --just_exit - do nothing; exit and reboot
+ *
+ * After completing, we remove /cache/recovery/command and reboot.
+ * Arguments may also be supplied in the bootloader control block (BCB).
+ * These important scenarios must be safely restartable at any point:
+ *
+ * FACTORY RESET
+ * 1. user selects "factory reset"
+ * 2. main system writes "--wipe_data" to /cache/recovery/command
+ * 3. main system reboots into recovery
+ * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
+ *    -- after this, rebooting will restart the erase --
+ * 5. erase_volume() reformats /data
+ * 6. erase_volume() reformats /cache
+ * 7. finish_recovery() erases BCB
+ *    -- after this, rebooting will restart the main system --
+ * 8. main() calls reboot() to boot main system
+ *
+ * OTA INSTALL
+ * 1. main system downloads OTA package to /cache/some-filename.zip
+ * 2. main system writes "--update_package=/cache/some-filename.zip"
+ * 3. main system reboots into recovery
+ * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
+ *    -- after this, rebooting will attempt to reinstall the update --
+ * 5. install_package() attempts to install the update
+ *    NOTE: the package install must itself be restartable from any point
+ * 6. finish_recovery() erases BCB
+ *    -- after this, rebooting will (try to) restart the main system --
+ * 7. ** if install failed **
+ *    7a. prompt_and_wait() shows an error icon and waits for the user
+ *    7b; the user reboots (pulling the battery, etc) into the main system
+ * 8. main() calls maybe_install_firmware_update()
+ *    ** if the update contained radio/hboot firmware **:
+ *    8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
+ *        -- after this, rebooting will reformat cache & restart main system --
+ *    8b. m_i_f_u() writes firmware image into raw cache partition
+ *    8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
+ *        -- after this, rebooting will attempt to reinstall firmware --
+ *    8d. bootloader tries to flash firmware
+ *    8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
+ *        -- after this, rebooting will reformat cache & restart main system --
+ *    8f. erase_volume() reformats /cache
+ *    8g. finish_recovery() erases BCB
+ *        -- after this, rebooting will (try to) restart the main system --
+ * 9. main() calls reboot() to boot main system
+ */
+
+static const int MAX_ARG_LENGTH = 4096;
+static const int MAX_ARGS = 100;
+
+// open a given path, mounting partitions as necessary
+FILE* fopen_path(const char *path, const char *mode) {
+    if (ensure_path_mounted(path) != 0) {
+        LOGE("Can't mount %s\n", path);
+        return NULL;
+    }
+
+    // When writing, try to create the containing directory, if necessary.
+    // Use generous permissions, the system (init.rc) will reset them.
+    if (strchr("wa", mode[0])) dirCreateHierarchy(path, 0777, NULL, 1, sehandle);
+
+    FILE *fp = fopen(path, mode);
+    return fp;
+}
+
+// close a file, log an error if the error indicator is set
+static void check_and_fclose(FILE *fp, const char *name) {
+    fflush(fp);
+    if (ferror(fp)) LOGE("Error in %s\n(%s)\n", name, strerror(errno));
+    fclose(fp);
+}
+
+bool is_ro_debuggable() {
+    char value[PROPERTY_VALUE_MAX+1];
+    return (property_get("ro.debuggable", value, NULL) == 1 && value[0] == '1');
+}
+
+static void redirect_stdio(const char* filename) {
+    int pipefd[2];
+    if (pipe(pipefd) == -1) {
+        LOGE("pipe failed: %s\n", strerror(errno));
+
+        // Fall back to traditional logging mode without timestamps.
+        // If these fail, there's not really anywhere to complain...
+        freopen(filename, "a", stdout); setbuf(stdout, NULL);
+        freopen(filename, "a", stderr); setbuf(stderr, NULL);
+
+        return;
+    }
+
+    pid_t pid = fork();
+    if (pid == -1) {
+        LOGE("fork failed: %s\n", strerror(errno));
+
+        // Fall back to traditional logging mode without timestamps.
+        // If these fail, there's not really anywhere to complain...
+        freopen(filename, "a", stdout); setbuf(stdout, NULL);
+        freopen(filename, "a", stderr); setbuf(stderr, NULL);
+
+        return;
+    }
+
+    if (pid == 0) {
+        /// Close the unused write end.
+        close(pipefd[1]);
+
+        auto start = std::chrono::steady_clock::now();
+
+        // Child logger to actually write to the log file.
+        FILE* log_fp = fopen(filename, "a");
+        if (log_fp == nullptr) {
+            LOGE("fopen \"%s\" failed: %s\n", filename, strerror(errno));
+            close(pipefd[0]);
+            _exit(1);
+        }
+
+        FILE* pipe_fp = fdopen(pipefd[0], "r");
+        if (pipe_fp == nullptr) {
+            LOGE("fdopen failed: %s\n", strerror(errno));
+            check_and_fclose(log_fp, filename);
+            close(pipefd[0]);
+            _exit(1);
+        }
+
+        char* line = nullptr;
+        size_t len = 0;
+        while (getline(&line, &len, pipe_fp) != -1) {
+            auto now = std::chrono::steady_clock::now();
+            double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
+                    now - start).count();
+            if (line[0] == '\n') {
+                fprintf(log_fp, "[%12.6lf]\n", duration);
+            } else {
+                fprintf(log_fp, "[%12.6lf] %s", duration, line);
+            }
+            fflush(log_fp);
+        }
+
+        LOGE("getline failed: %s\n", strerror(errno));
+
+        free(line);
+        check_and_fclose(log_fp, filename);
+        close(pipefd[0]);
+        _exit(1);
+    } else {
+        // Redirect stdout/stderr to the logger process.
+        // Close the unused read end.
+        close(pipefd[0]);
+
+        setbuf(stdout, nullptr);
+        setbuf(stderr, nullptr);
+
+        if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
+            LOGE("dup2 stdout failed: %s\n", strerror(errno));
+        }
+        if (dup2(pipefd[1], STDERR_FILENO) == -1) {
+            LOGE("dup2 stderr failed: %s\n", strerror(errno));
+        }
+
+        close(pipefd[1]);
+    }
+}
+
+// command line args come from, in decreasing precedence:
+//   - the actual command line
+//   - the bootloader control block (one per line, after "recovery")
+//   - the contents of COMMAND_FILE (one per line)
+static void
+get_args(int *argc, char ***argv) {
+    struct bootloader_message boot;
+    memset(&boot, 0, sizeof(boot));
+    get_bootloader_message(&boot);  // this may fail, leaving a zeroed structure
+    stage = strndup(boot.stage, sizeof(boot.stage));
+
+    if (boot.command[0] != 0 && boot.command[0] != 255) {
+        LOGI("Boot command: %.*s\n", (int)sizeof(boot.command), boot.command);
+    }
+
+    if (boot.status[0] != 0 && boot.status[0] != 255) {
+        LOGI("Boot status: %.*s\n", (int)sizeof(boot.status), boot.status);
+    }
+
+    // --- if arguments weren't supplied, look in the bootloader control block
+    if (*argc <= 1) {
+        boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
+        const char *arg = strtok(boot.recovery, "\n");
+        if (arg != NULL && !strcmp(arg, "recovery")) {
+            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
+            (*argv)[0] = strdup(arg);
+            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
+                if ((arg = strtok(NULL, "\n")) == NULL) break;
+                (*argv)[*argc] = strdup(arg);
+            }
+            LOGI("Got arguments from boot message\n");
+        } else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
+            LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery);
+        }
+    }
+
+    // --- if that doesn't work, try the command file (if we have /cache).
+    if (*argc <= 1 && has_cache) {
+        FILE *fp = fopen_path(COMMAND_FILE, "r");
+        if (fp != NULL) {
+            char *token;
+            char *argv0 = (*argv)[0];
+            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
+            (*argv)[0] = argv0;  // use the same program name
+
+            char buf[MAX_ARG_LENGTH];
+            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
+                if (!fgets(buf, sizeof(buf), fp)) break;
+                token = strtok(buf, "\r\n");
+                if (token != NULL) {
+                    (*argv)[*argc] = strdup(token);  // Strip newline.
+                } else {
+                    --*argc;
+                }
+            }
+
+            check_and_fclose(fp, COMMAND_FILE);
+            LOGI("Got arguments from %s\n", COMMAND_FILE);
+        }
+    }
+
+    // --> write the arguments we have back into the bootloader control block
+    // always boot into recovery after this (until finish_recovery() is called)
+    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
+    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
+    int i;
+    for (i = 1; i < *argc; ++i) {
+        strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
+        strlcat(boot.recovery, "\n", sizeof(boot.recovery));
+    }
+    set_bootloader_message(&boot);
+}
+
+static void
+set_sdcard_update_bootloader_message() {
+    struct bootloader_message boot;
+    memset(&boot, 0, sizeof(boot));
+    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
+    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
+    set_bootloader_message(&boot);
+}
+
+// Read from kernel log into buffer and write out to file.
+static void save_kernel_log(const char* destination) {
+    int klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
+    if (klog_buf_len <= 0) {
+        LOGE("Error getting klog size: %s\n", strerror(errno));
+        return;
+    }
+
+    std::string buffer(klog_buf_len, 0);
+    int n = klogctl(KLOG_READ_ALL, &buffer[0], klog_buf_len);
+    if (n == -1) {
+        LOGE("Error in reading klog: %s\n", strerror(errno));
+        return;
+    }
+    buffer.resize(n);
+    android::base::WriteStringToFile(buffer, destination);
+}
+
+// write content to the current pmsg session.
+static ssize_t __pmsg_write(const char *filename, const char *buf, size_t len) {
+    return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
+                                         filename, buf, len);
+}
+
+static void copy_log_file_to_pmsg(const char* source, const char* destination) {
+    std::string content;
+    android::base::ReadFileToString(source, &content);
+    __pmsg_write(destination, content.c_str(), content.length());
+}
+
+// How much of the temp log we have copied to the copy in cache.
+static off_t tmplog_offset = 0;
+
+static void copy_log_file(const char* source, const char* destination, bool append) {
+    FILE* dest_fp = fopen_path(destination, append ? "a" : "w");
+    if (dest_fp == nullptr) {
+        LOGE("Can't open %s\n", destination);
+    } else {
+        FILE* source_fp = fopen(source, "r");
+        if (source_fp != nullptr) {
+            if (append) {
+                fseeko(source_fp, tmplog_offset, SEEK_SET);  // Since last write
+            }
+            char buf[4096];
+            size_t bytes;
+            while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
+                fwrite(buf, 1, bytes, dest_fp);
+            }
+            if (append) {
+                tmplog_offset = ftello(source_fp);
+            }
+            check_and_fclose(source_fp, source);
+        }
+        check_and_fclose(dest_fp, destination);
+    }
+}
+
+// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max.
+// Similarly rename last_kmsg -> last_kmsg.1 -> ... -> last_kmsg.$max.
+// Overwrite any existing last_log.$max and last_kmsg.$max.
+static void rotate_logs(int max) {
+    // Logs should only be rotated once.
+    static bool rotated = false;
+    if (rotated) {
+        return;
+    }
+    rotated = true;
+    ensure_path_mounted(LAST_LOG_FILE);
+    ensure_path_mounted(LAST_KMSG_FILE);
+
+    for (int i = max-1; i >= 0; --i) {
+        std::string old_log = android::base::StringPrintf("%s", LAST_LOG_FILE);
+        if (i > 0) {
+          old_log += "." + std::to_string(i);
+        }
+        std::string new_log = android::base::StringPrintf("%s.%d", LAST_LOG_FILE, i+1);
+        // Ignore errors if old_log doesn't exist.
+        rename(old_log.c_str(), new_log.c_str());
+
+        std::string old_kmsg = android::base::StringPrintf("%s", LAST_KMSG_FILE);
+        if (i > 0) {
+          old_kmsg += "." + std::to_string(i);
+        }
+        std::string new_kmsg = android::base::StringPrintf("%s.%d", LAST_KMSG_FILE, i+1);
+        rename(old_kmsg.c_str(), new_kmsg.c_str());
+    }
+}
+
+static void copy_logs() {
+    // We only rotate and record the log of the current session if there are
+    // actual attempts to modify the flash, such as wipes, installs from BCB
+    // or menu selections. This is to avoid unnecessary rotation (and
+    // possible deletion) of log files, if it does not do anything loggable.
+    if (!modified_flash) {
+        return;
+    }
+
+    // Always write to pmsg, this allows the OTA logs to be caught in logcat -L
+    copy_log_file_to_pmsg(TEMPORARY_LOG_FILE, LAST_LOG_FILE);
+    copy_log_file_to_pmsg(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE);
+
+    // We can do nothing for now if there's no /cache partition.
+    if (!has_cache) {
+        return;
+    }
+
+    rotate_logs(KEEP_LOG_COUNT);
+
+    // Copy logs to cache so the system can find out what happened.
+    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
+    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
+    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
+    save_kernel_log(LAST_KMSG_FILE);
+    chmod(LOG_FILE, 0600);
+    chown(LOG_FILE, 1000, 1000);   // system user
+    chmod(LAST_KMSG_FILE, 0600);
+    chown(LAST_KMSG_FILE, 1000, 1000);   // system user
+    chmod(LAST_LOG_FILE, 0640);
+    chmod(LAST_INSTALL_FILE, 0644);
+    sync();
+}
+
+// clear the recovery command and prepare to boot a (hopefully working) system,
+// copy our log file to cache as well (for the system to read). This function is
+// idempotent: call it as many times as you like.
+static void
+finish_recovery() {
+    // Save the locale to cache, so if recovery is next started up
+    // without a --locale argument (eg, directly from the bootloader)
+    // it will use the last-known locale.
+    if (locale != NULL) {
+        size_t len = strlen(locale);
+        __pmsg_write(LOCALE_FILE, locale, len);
+        if (has_cache) {
+            LOGI("Saving locale \"%s\"\n", locale);
+            FILE* fp = fopen_path(LOCALE_FILE, "w");
+            if (fp != NULL) {
+                fwrite(locale, 1, len, fp);
+                fflush(fp);
+                fsync(fileno(fp));
+                check_and_fclose(fp, LOCALE_FILE);
+            }
+        }
+    }
+
+    copy_logs();
+
+    // Reset to normal system boot so recovery won't cycle indefinitely.
+    struct bootloader_message boot;
+    memset(&boot, 0, sizeof(boot));
+    set_bootloader_message(&boot);
+
+    // Remove the command file, so recovery won't repeat indefinitely.
+    if (has_cache) {
+        if (ensure_path_mounted(COMMAND_FILE) != 0 || (unlink(COMMAND_FILE) && errno != ENOENT)) {
+            LOGW("Can't unlink %s\n", COMMAND_FILE);
+        }
+        ensure_path_unmounted(CACHE_ROOT);
+    }
+
+    sync();  // For good measure.
+}
+
+typedef struct _saved_log_file {
+    char* name;
+    struct stat st;
+    unsigned char* data;
+    struct _saved_log_file* next;
+} saved_log_file;
+
+static bool erase_volume(const char* volume) {
+    bool is_cache = (strcmp(volume, CACHE_ROOT) == 0);
+
+    ui->SetBackground(RecoveryUI::ERASING);
+    ui->SetProgressType(RecoveryUI::INDETERMINATE);
+
+    saved_log_file* head = NULL;
+
+    if (is_cache) {
+        // If we're reformatting /cache, we load any past logs
+        // (i.e. "/cache/recovery/last_*") and the current log
+        // ("/cache/recovery/log") into memory, so we can restore them after
+        // the reformat.
+
+        ensure_path_mounted(volume);
+
+        DIR* d;
+        struct dirent* de;
+        d = opendir(CACHE_LOG_DIR);
+        if (d) {
+            char path[PATH_MAX];
+            strcpy(path, CACHE_LOG_DIR);
+            strcat(path, "/");
+            int path_len = strlen(path);
+            while ((de = readdir(d)) != NULL) {
+                if (strncmp(de->d_name, "last_", 5) == 0 || strcmp(de->d_name, "log") == 0) {
+                    saved_log_file* p = (saved_log_file*) malloc(sizeof(saved_log_file));
+                    strcpy(path+path_len, de->d_name);
+                    p->name = strdup(path);
+                    if (stat(path, &(p->st)) == 0) {
+                        // truncate files to 512kb
+                        if (p->st.st_size > (1 << 19)) {
+                            p->st.st_size = 1 << 19;
+                        }
+                        p->data = (unsigned char*) malloc(p->st.st_size);
+                        FILE* f = fopen(path, "rb");
+                        fread(p->data, 1, p->st.st_size, f);
+                        fclose(f);
+                        p->next = head;
+                        head = p;
+                    } else {
+                        free(p);
+                    }
+                }
+            }
+            closedir(d);
+        } else {
+            if (errno != ENOENT) {
+                printf("opendir failed: %s\n", strerror(errno));
+            }
+        }
+    }
+
+    ui->Print("Formatting %s...\n", volume);
+
+    ensure_path_unmounted(volume);
+    int result = format_volume(volume);
+
+    if (is_cache) {
+        while (head) {
+            FILE* f = fopen_path(head->name, "wb");
+            if (f) {
+                fwrite(head->data, 1, head->st.st_size, f);
+                fclose(f);
+                chmod(head->name, head->st.st_mode);
+                chown(head->name, head->st.st_uid, head->st.st_gid);
+            }
+            free(head->name);
+            free(head->data);
+            saved_log_file* temp = head->next;
+            free(head);
+            head = temp;
+        }
+
+        // Any part of the log we'd copied to cache is now gone.
+        // Reset the pointer so we copy from the beginning of the temp
+        // log.
+        tmplog_offset = 0;
+        copy_logs();
+    }
+
+    return (result == 0);
+}
+
+static int
+get_menu_selection(const char* const * headers, const char* const * items,
+                   int menu_only, int initial_selection, Device* device) {
+    // throw away keys pressed previously, so user doesn't
+    // accidentally trigger menu items.
+    ui->FlushKeys();
+
+    ui->StartMenu(headers, items, initial_selection);
+    int selected = initial_selection;
+    int chosen_item = -1;
+
+    while (chosen_item < 0) {
+        int key = ui->WaitKey();
+        int visible = ui->IsTextVisible();
+
+        if (key == -1) {   // ui_wait_key() timed out
+            if (ui->WasTextEverVisible()) {
+                continue;
+            } else {
+                LOGI("timed out waiting for key input; rebooting.\n");
+                ui->EndMenu();
+                return 0; // XXX fixme
+            }
+        }
+
+        int action = device->HandleMenuKey(key, visible);
+
+        if (action < 0) {
+            switch (action) {
+                case Device::kHighlightUp:
+                    selected = ui->SelectMenu(--selected);
+                    break;
+                case Device::kHighlightDown:
+                    selected = ui->SelectMenu(++selected);
+                    break;
+                case Device::kInvokeItem:
+                    chosen_item = selected;
+                    break;
+                case Device::kNoAction:
+                    break;
+            }
+        } else if (!menu_only) {
+            chosen_item = action;
+        }
+    }
+
+    ui->EndMenu();
+    return chosen_item;
+}
+
+static int compare_string(const void* a, const void* b) {
+    return strcmp(*(const char**)a, *(const char**)b);
+}
+
+// Returns a malloc'd path, or NULL.
+static char* browse_directory(const char* path, Device* device) {
+    ensure_path_mounted(path);
+
+    DIR* d = opendir(path);
+    if (d == NULL) {
+        LOGE("error opening %s: %s\n", path, strerror(errno));
+        return NULL;
+    }
+
+    int d_size = 0;
+    int d_alloc = 10;
+    char** dirs = (char**)malloc(d_alloc * sizeof(char*));
+    int z_size = 1;
+    int z_alloc = 10;
+    char** zips = (char**)malloc(z_alloc * sizeof(char*));
+    zips[0] = strdup("../");
+
+    struct dirent* de;
+    while ((de = readdir(d)) != NULL) {
+        int name_len = strlen(de->d_name);
+
+        if (de->d_type == DT_DIR) {
+            // skip "." and ".." entries
+            if (name_len == 1 && de->d_name[0] == '.') continue;
+            if (name_len == 2 && de->d_name[0] == '.' &&
+                de->d_name[1] == '.') continue;
+
+            if (d_size >= d_alloc) {
+                d_alloc *= 2;
+                dirs = (char**)realloc(dirs, d_alloc * sizeof(char*));
+            }
+            dirs[d_size] = (char*)malloc(name_len + 2);
+            strcpy(dirs[d_size], de->d_name);
+            dirs[d_size][name_len] = '/';
+            dirs[d_size][name_len+1] = '\0';
+            ++d_size;
+        } else if (de->d_type == DT_REG &&
+                   name_len >= 4 &&
+                   strncasecmp(de->d_name + (name_len-4), ".zip", 4) == 0) {
+            if (z_size >= z_alloc) {
+                z_alloc *= 2;
+                zips = (char**)realloc(zips, z_alloc * sizeof(char*));
+            }
+            zips[z_size++] = strdup(de->d_name);
+        }
+    }
+    closedir(d);
+
+    qsort(dirs, d_size, sizeof(char*), compare_string);
+    qsort(zips, z_size, sizeof(char*), compare_string);
+
+    // append dirs to the zips list
+    if (d_size + z_size + 1 > z_alloc) {
+        z_alloc = d_size + z_size + 1;
+        zips = (char**)realloc(zips, z_alloc * sizeof(char*));
+    }
+    memcpy(zips + z_size, dirs, d_size * sizeof(char*));
+    free(dirs);
+    z_size += d_size;
+    zips[z_size] = NULL;
+
+    const char* headers[] = { "Choose a package to install:", path, NULL };
+
+    char* result;
+    int chosen_item = 0;
+    while (true) {
+        chosen_item = get_menu_selection(headers, zips, 1, chosen_item, device);
+
+        char* item = zips[chosen_item];
+        int item_len = strlen(item);
+        if (chosen_item == 0) {          // item 0 is always "../"
+            // go up but continue browsing (if the caller is update_directory)
+            result = NULL;
+            break;
+        }
+
+        char new_path[PATH_MAX];
+        strlcpy(new_path, path, PATH_MAX);
+        strlcat(new_path, "/", PATH_MAX);
+        strlcat(new_path, item, PATH_MAX);
+
+        if (item[item_len-1] == '/') {
+            // recurse down into a subdirectory
+            new_path[strlen(new_path)-1] = '\0';  // truncate the trailing '/'
+            result = browse_directory(new_path, device);
+            if (result) break;
+        } else {
+            // selected a zip file: return the malloc'd path to the caller.
+            result = strdup(new_path);
+            break;
+        }
+    }
+
+    for (int i = 0; i < z_size; ++i) free(zips[i]);
+    free(zips);
+
+    return result;
+}
+
+static bool yes_no(Device* device, const char* question1, const char* question2) {
+    const char* headers[] = { question1, question2, NULL };
+    const char* items[] = { " No", " Yes", NULL };
+
+    int chosen_item = get_menu_selection(headers, items, 1, 0, device);
+    return (chosen_item == 1);
+}
+
+// Return true on success.
+static bool wipe_data(int should_confirm, Device* device) {
+    if (should_confirm && !yes_no(device, "Wipe all user data?", "  THIS CAN NOT BE UNDONE!")) {
+        return false;
+    }
+
+    modified_flash = true;
+
+    ui->Print("\n-- Wiping data...\n");
+    bool success =
+        device->PreWipeData() &&
+        erase_volume("/data") &&
+        (has_cache ? erase_volume("/cache") : true) &&
+        device->PostWipeData();
+    ui->Print("Data wipe %s.\n", success ? "complete" : "failed");
+    return success;
+}
+
+// Return true on success.
+static bool wipe_cache(bool should_confirm, Device* device) {
+    if (!has_cache) {
+        ui->Print("No /cache partition found.\n");
+        return false;
+    }
+
+    if (should_confirm && !yes_no(device, "Wipe cache?", "  THIS CAN NOT BE UNDONE!")) {
+        return false;
+    }
+
+    modified_flash = true;
+
+    ui->Print("\n-- Wiping cache...\n");
+    bool success = erase_volume("/cache");
+    ui->Print("Cache wipe %s.\n", success ? "complete" : "failed");
+    return success;
+}
+
+// Secure-wipe a given partition. It uses BLKSECDISCARD, if supported.
+// Otherwise, it goes with BLKDISCARD (if device supports BLKDISCARDZEROES) or
+// BLKZEROOUT.
+static bool secure_wipe_partition(const std::string& partition) {
+    android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(partition.c_str(), O_WRONLY)));
+    if (fd == -1) {
+        LOGE("failed to open \"%s\": %s\n", partition.c_str(), strerror(errno));
+        return false;
+    }
+
+    uint64_t range[2] = {0, 0};
+    if (ioctl(fd, BLKGETSIZE64, &range[1]) == -1 || range[1] == 0) {
+        LOGE("failed to get partition size: %s\n", strerror(errno));
+        return false;
+    }
+    printf("Secure-wiping \"%s\" from %" PRIu64 " to %" PRIu64 ".\n",
+           partition.c_str(), range[0], range[1]);
+
+    printf("Trying BLKSECDISCARD...\t");
+    if (ioctl(fd, BLKSECDISCARD, &range) == -1) {
+        printf("failed: %s\n", strerror(errno));
+
+        // Use BLKDISCARD if it zeroes out blocks, otherwise use BLKZEROOUT.
+        unsigned int zeroes;
+        if (ioctl(fd, BLKDISCARDZEROES, &zeroes) == 0 && zeroes != 0) {
+            printf("Trying BLKDISCARD...\t");
+            if (ioctl(fd, BLKDISCARD, &range) == -1) {
+                printf("failed: %s\n", strerror(errno));
+                return false;
+            }
+        } else {
+            printf("Trying BLKZEROOUT...\t");
+            if (ioctl(fd, BLKZEROOUT, &range) == -1) {
+                printf("failed: %s\n", strerror(errno));
+                return false;
+            }
+        }
+    }
+
+    printf("done\n");
+    return true;
+}
+
+// Brick the current device, with a secure wipe of all the partitions in
+// RECOVERY_BRICK.
+static bool brick_device() {
+    ui->SetBackground(RecoveryUI::ERASING);
+    ui->SetProgressType(RecoveryUI::INDETERMINATE);
+
+    std::string partition_list;
+    if (!android::base::ReadFileToString(RECOVERY_BRICK, &partition_list)) {
+        LOGE("failed to read \"%s\".\n", RECOVERY_BRICK);
+        return false;
+    }
+
+    std::vector<std::string> lines = android::base::Split(partition_list, "\n");
+    for (const std::string& line : lines) {
+        std::string partition = android::base::Trim(line);
+        // Ignore '#' comment or empty lines.
+        if (android::base::StartsWith(partition, "#") || partition.empty()) {
+            continue;
+        }
+
+        // Proceed anyway even if it fails to wipe some partition.
+        secure_wipe_partition(partition);
+    }
+    return true;
+}
+
+static void choose_recovery_file(Device* device) {
+    // "Back" + KEEP_LOG_COUNT * 2 + terminating nullptr entry
+    char* entries[1 + KEEP_LOG_COUNT * 2 + 1];
+    memset(entries, 0, sizeof(entries));
+
+    unsigned int n = 0;
+
+    if (has_cache) {
+        // Add LAST_LOG_FILE + LAST_LOG_FILE.x
+        // Add LAST_KMSG_FILE + LAST_KMSG_FILE.x
+        for (int i = 0; i < KEEP_LOG_COUNT; i++) {
+            char* log_file;
+            int ret;
+            ret = (i == 0) ? asprintf(&log_file, "%s", LAST_LOG_FILE) :
+                    asprintf(&log_file, "%s.%d", LAST_LOG_FILE, i);
+            if (ret == -1) {
+                // memory allocation failure - return early. Should never happen.
+                return;
+            }
+            if ((ensure_path_mounted(log_file) != 0) || (access(log_file, R_OK) == -1)) {
+                free(log_file);
+            } else {
+                entries[n++] = log_file;
+            }
+
+            char* kmsg_file;
+            ret = (i == 0) ? asprintf(&kmsg_file, "%s", LAST_KMSG_FILE) :
+                    asprintf(&kmsg_file, "%s.%d", LAST_KMSG_FILE, i);
+            if (ret == -1) {
+                // memory allocation failure - return early. Should never happen.
+                return;
+            }
+            if ((ensure_path_mounted(kmsg_file) != 0) || (access(kmsg_file, R_OK) == -1)) {
+                free(kmsg_file);
+            } else {
+                entries[n++] = kmsg_file;
+            }
+        }
+    } else {
+        // If cache partition is not found, view /tmp/recovery.log instead.
+        ui->Print("No /cache partition found.\n");
+        if (access(TEMPORARY_LOG_FILE, R_OK) == -1) {
+            return;
+        } else{
+            entries[n++] = strdup(TEMPORARY_LOG_FILE);
+        }
+    }
+
+    entries[n++] = strdup("Back");
+
+    const char* headers[] = { "Select file to view", nullptr };
+
+    while (true) {
+        int chosen_item = get_menu_selection(headers, entries, 1, 0, device);
+        if (strcmp(entries[chosen_item], "Back") == 0) break;
+
+        ui->ShowFile(entries[chosen_item]);
+    }
+
+    for (size_t i = 0; i < (sizeof(entries) / sizeof(*entries)); i++) {
+        free(entries[i]);
+    }
+}
+
+// How long (in seconds) we wait for the fuse-provided package file to
+// appear, before timing out.
+#define SDCARD_INSTALL_TIMEOUT 10
+
+static int apply_from_sdcard(Device* device, bool* wipe_cache) {
+    modified_flash = true;
+
+    if (ensure_path_mounted(SDCARD_ROOT) != 0) {
+        ui->Print("\n-- Couldn't mount %s.\n", SDCARD_ROOT);
+        return INSTALL_ERROR;
+    }
+
+    char* path = browse_directory(SDCARD_ROOT, device);
+    if (path == NULL) {
+        ui->Print("\n-- No package file selected.\n");
+        ensure_path_unmounted(SDCARD_ROOT);
+        return INSTALL_ERROR;
+    }
+
+    ui->Print("\n-- Install %s ...\n", path);
+    set_sdcard_update_bootloader_message();
+
+    // We used to use fuse in a thread as opposed to a process. Since accessing
+    // through fuse involves going from kernel to userspace to kernel, it leads
+    // to deadlock when a page fault occurs. (Bug: 26313124)
+    pid_t child;
+    if ((child = fork()) == 0) {
+        bool status = start_sdcard_fuse(path);
+
+        _exit(status ? EXIT_SUCCESS : EXIT_FAILURE);
+    }
+
+    // FUSE_SIDELOAD_HOST_PATHNAME will start to exist once the fuse in child
+    // process is ready.
+    int result = INSTALL_ERROR;
+    int status;
+    bool waited = false;
+    for (int i = 0; i < SDCARD_INSTALL_TIMEOUT; ++i) {
+        if (waitpid(child, &status, WNOHANG) == -1) {
+            result = INSTALL_ERROR;
+            waited = true;
+            break;
+        }
+
+        struct stat sb;
+        if (stat(FUSE_SIDELOAD_HOST_PATHNAME, &sb) == -1) {
+            if (errno == ENOENT && i < SDCARD_INSTALL_TIMEOUT-1) {
+                sleep(1);
+                continue;
+            } else {
+                LOGE("Timed out waiting for the fuse-provided package.\n");
+                result = INSTALL_ERROR;
+                kill(child, SIGKILL);
+                break;
+            }
+        }
+
+        result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache,
+                                 TEMPORARY_INSTALL_FILE, false);
+        break;
+    }
+
+    if (!waited) {
+        // Calling stat() on this magic filename signals the fuse
+        // filesystem to shut down.
+        struct stat sb;
+        stat(FUSE_SIDELOAD_HOST_EXIT_PATHNAME, &sb);
+
+        waitpid(child, &status, 0);
+    }
+
+    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+        LOGE("Error exit from the fuse process: %d\n", WEXITSTATUS(status));
+    }
+
+    ensure_path_unmounted(SDCARD_ROOT);
+    return result;
+}
+
+// Return REBOOT, SHUTDOWN, or REBOOT_BOOTLOADER.  Returning NO_ACTION
+// means to take the default, which is to reboot or shutdown depending
+// on if the --shutdown_after flag was passed to recovery.
+static Device::BuiltinAction
+prompt_and_wait(Device* device, int status) {
+    for (;;) {
+        finish_recovery();
+        switch (status) {
+            case INSTALL_SUCCESS:
+            case INSTALL_NONE:
+                ui->SetBackground(RecoveryUI::NO_COMMAND);
+                break;
+
+            case INSTALL_ERROR:
+            case INSTALL_CORRUPT:
+                ui->SetBackground(RecoveryUI::ERROR);
+                break;
+        }
+        ui->SetProgressType(RecoveryUI::EMPTY);
+
+        int chosen_item = get_menu_selection(nullptr, device->GetMenuItems(), 0, 0, device);
+
+        // device-specific code may take some action here.  It may
+        // return one of the core actions handled in the switch
+        // statement below.
+        Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);
+
+        bool should_wipe_cache = false;
+        switch (chosen_action) {
+            case Device::NO_ACTION:
+                break;
+
+            case Device::REBOOT:
+            case Device::SHUTDOWN:
+            case Device::REBOOT_BOOTLOADER:
+                return chosen_action;
+
+            case Device::WIPE_DATA:
+                wipe_data(ui->IsTextVisible(), device);
+                if (!ui->IsTextVisible()) return Device::NO_ACTION;
+                break;
+
+            case Device::WIPE_CACHE:
+                wipe_cache(ui->IsTextVisible(), device);
+                if (!ui->IsTextVisible()) return Device::NO_ACTION;
+                break;
+
+            case Device::APPLY_ADB_SIDELOAD:
+            case Device::APPLY_SDCARD:
+                {
+                    bool adb = (chosen_action == Device::APPLY_ADB_SIDELOAD);
+                    if (adb) {
+                        status = apply_from_adb(ui, &should_wipe_cache, TEMPORARY_INSTALL_FILE);
+                    } else {
+                        status = apply_from_sdcard(device, &should_wipe_cache);
+                    }
+
+                    if (status == INSTALL_SUCCESS && should_wipe_cache) {
+                        if (!wipe_cache(false, device)) {
+                            status = INSTALL_ERROR;
+                        }
+                    }
+
+                    if (status != INSTALL_SUCCESS) {
+                        ui->SetBackground(RecoveryUI::ERROR);
+                        ui->Print("Installation aborted.\n");
+                        copy_logs();
+                    } else if (!ui->IsTextVisible()) {
+                        return Device::NO_ACTION;  // reboot if logs aren't visible
+                    } else {
+                        ui->Print("\nInstall from %s complete.\n", adb ? "ADB" : "SD card");
+                    }
+                }
+                break;
+
+            case Device::VIEW_RECOVERY_LOGS:
+                choose_recovery_file(device);
+                break;
+
+            case Device::MOUNT_SYSTEM:
+                char system_root_image[PROPERTY_VALUE_MAX];
+                property_get("ro.build.system_root_image", system_root_image, "");
+
+                // For a system image built with the root directory (i.e.
+                // system_root_image == "true"), we mount it to /system_root, and symlink /system
+                // to /system_root/system to make adb shell work (the symlink is created through
+                // the build system).
+                // Bug: 22855115
+                if (strcmp(system_root_image, "true") == 0) {
+                    if (ensure_path_mounted_at("/", "/system_root") != -1) {
+                        ui->Print("Mounted /system.\n");
+                    }
+                } else {
+                    if (ensure_path_mounted("/system") != -1) {
+                        ui->Print("Mounted /system.\n");
+                    }
+                }
+
+                break;
+        }
+    }
+}
+
+static void
+print_property(const char *key, const char *name, void *cookie) {
+    printf("%s=%s\n", key, name);
+}
+
+static void
+load_locale_from_cache() {
+    FILE* fp = fopen_path(LOCALE_FILE, "r");
+    char buffer[80];
+    if (fp != NULL) {
+        fgets(buffer, sizeof(buffer), fp);
+        int j = 0;
+        unsigned int i;
+        for (i = 0; i < sizeof(buffer) && buffer[i]; ++i) {
+            if (!isspace(buffer[i])) {
+                buffer[j++] = buffer[i];
+            }
+        }
+        buffer[j] = 0;
+        locale = strdup(buffer);
+        check_and_fclose(fp, LOCALE_FILE);
+    }
+}
+
+static RecoveryUI* gCurrentUI = NULL;
+
+void
+ui_print(const char* format, ...) {
+    char buffer[256];
+
+    va_list ap;
+    va_start(ap, format);
+    vsnprintf(buffer, sizeof(buffer), format, ap);
+    va_end(ap);
+
+    if (gCurrentUI != NULL) {
+        gCurrentUI->Print("%s", buffer);
+    } else {
+        fputs(buffer, stdout);
+    }
+}
+
+static bool is_battery_ok() {
+    struct healthd_config healthd_config = {
+            .batteryStatusPath = android::String8(android::String8::kEmptyString),
+            .batteryHealthPath = android::String8(android::String8::kEmptyString),
+            .batteryPresentPath = android::String8(android::String8::kEmptyString),
+            .batteryCapacityPath = android::String8(android::String8::kEmptyString),
+            .batteryVoltagePath = android::String8(android::String8::kEmptyString),
+            .batteryTemperaturePath = android::String8(android::String8::kEmptyString),
+            .batteryTechnologyPath = android::String8(android::String8::kEmptyString),
+            .batteryCurrentNowPath = android::String8(android::String8::kEmptyString),
+            .batteryCurrentAvgPath = android::String8(android::String8::kEmptyString),
+            .batteryChargeCounterPath = android::String8(android::String8::kEmptyString),
+            .batteryFullChargePath = android::String8(android::String8::kEmptyString),
+            .batteryCycleCountPath = android::String8(android::String8::kEmptyString),
+            .energyCounter = NULL,
+            .boot_min_cap = 0,
+            .screen_on = NULL
+    };
+    healthd_board_init(&healthd_config);
+
+    android::BatteryMonitor monitor;
+    monitor.init(&healthd_config);
+
+    int wait_second = 0;
+    while (true) {
+        int charge_status = monitor.getChargeStatus();
+        // Treat unknown status as charged.
+        bool charged = (charge_status != android::BATTERY_STATUS_DISCHARGING &&
+                        charge_status != android::BATTERY_STATUS_NOT_CHARGING);
+        android::BatteryProperty capacity;
+        android::status_t status = monitor.getProperty(android::BATTERY_PROP_CAPACITY, &capacity);
+        ui_print("charge_status %d, charged %d, status %d, capacity %lld\n", charge_status,
+                 charged, status, capacity.valueInt64);
+        // At startup, the battery drivers in devices like N5X/N6P take some time to load
+        // the battery profile. Before the load finishes, it reports value 50 as a fake
+        // capacity. BATTERY_READ_TIMEOUT_IN_SEC is set that the battery drivers are expected
+        // to finish loading the battery profile earlier than 10 seconds after kernel startup.
+        if (status == 0 && capacity.valueInt64 == 50) {
+            if (wait_second < BATTERY_READ_TIMEOUT_IN_SEC) {
+                sleep(1);
+                wait_second++;
+                continue;
+            }
+        }
+        // If we can't read battery percentage, it may be a device without battery. In this
+        // situation, use 100 as a fake battery percentage.
+        if (status != 0) {
+            capacity.valueInt64 = 100;
+        }
+        return (charged && capacity.valueInt64 >= BATTERY_WITH_CHARGER_OK_PERCENTAGE) ||
+                (!charged && capacity.valueInt64 >= BATTERY_OK_PERCENTAGE);
+    }
+}
+
+static void set_retry_bootloader_message(int retry_count, int argc, char** argv) {
+    struct bootloader_message boot {};
+    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
+    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
+
+    for (int i = 1; i < argc; ++i) {
+        if (strstr(argv[i], "retry_count") == nullptr) {
+            strlcat(boot.recovery, argv[i], sizeof(boot.recovery));
+            strlcat(boot.recovery, "\n", sizeof(boot.recovery));
+        }
+    }
+
+    // Initialize counter to 1 if it's not in BCB, otherwise increment it by 1.
+    if (retry_count == 0) {
+        strlcat(boot.recovery, "--retry_count=1\n", sizeof(boot.recovery));
+    } else {
+        char buffer[20];
+        snprintf(buffer, sizeof(buffer), "--retry_count=%d\n", retry_count+1);
+        strlcat(boot.recovery, buffer, sizeof(boot.recovery));
+    }
+    set_bootloader_message(&boot);
+}
+
+static ssize_t logbasename(
+        log_id_t /* logId */,
+        char /* prio */,
+        const char *filename,
+        const char * /* buf */, size_t len,
+        void *arg) {
+    if (strstr(LAST_KMSG_FILE, filename) ||
+            strstr(LAST_LOG_FILE, filename)) {
+        bool *doRotate = reinterpret_cast<bool *>(arg);
+        *doRotate = true;
+    }
+    return len;
+}
+
+static ssize_t logrotate(
+        log_id_t logId,
+        char prio,
+        const char *filename,
+        const char *buf, size_t len,
+        void *arg) {
+    bool *doRotate = reinterpret_cast<bool *>(arg);
+    if (!*doRotate) {
+        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
+    }
+
+    std::string name(filename);
+    size_t dot = name.find_last_of('.');
+    std::string sub = name.substr(0, dot);
+
+    if (!strstr(LAST_KMSG_FILE, sub.c_str()) &&
+                !strstr(LAST_LOG_FILE, sub.c_str())) {
+        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
+    }
+
+    // filename rotation
+    if (dot == std::string::npos) {
+        name += ".1";
+    } else {
+        std::string number = name.substr(dot + 1);
+        if (!isdigit(number.data()[0])) {
+            name += ".1";
+        } else {
+            auto i = std::stoull(number);
+            name = sub + "." + std::to_string(i + 1);
+        }
+    }
+
+    return __android_log_pmsg_file_write(logId, prio, name.c_str(), buf, len);
+}
+
+int main(int argc, char **argv) {
+    // Take last pmsg contents and rewrite it to the current pmsg session.
+    static const char filter[] = "recovery/";
+    // Do we need to rotate?
+    bool doRotate = false;
+    __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
+        logbasename, &doRotate);
+    // Take action to refresh pmsg contents
+    __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter,
+        logrotate, &doRotate);
+
+    // If this binary is started with the single argument "--adbd",
+    // instead of being the normal recovery binary, it turns into kind
+    // of a stripped-down version of adbd that only supports the
+    // 'sideload' command.  Note this must be a real argument, not
+    // anything in the command file or bootloader control block; the
+    // only way recovery should be run with this argument is when it
+    // starts a copy of itself from the apply_from_adb() function.
+    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
+        adb_server_main(0, DEFAULT_ADB_PORT, -1);
+        return 0;
+    }
+
+    time_t start = time(NULL);
+
+    // redirect_stdio should be called only in non-sideload mode. Otherwise
+    // we may have two logger instances with different timestamps.
+    redirect_stdio(TEMPORARY_LOG_FILE);
+
+    printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
+
+    load_volume_table();
+    has_cache = volume_for_path(CACHE_ROOT) != nullptr;
+
+    get_args(&argc, &argv);
+
+    const char *update_package = NULL;
+    bool should_wipe_data = false;
+    bool should_wipe_cache = false;
+    bool should_brick = false;
+    bool show_text = false;
+    bool sideload = false;
+    bool sideload_auto_reboot = false;
+    bool just_exit = false;
+    bool shutdown_after = false;
+    int retry_count = 0;
+
+    int arg;
+    int option_index;
+    while ((arg = getopt_long(argc, argv, "", OPTIONS, &option_index)) != -1) {
+        switch (arg) {
+        case 'n': android::base::ParseInt(optarg, &retry_count, 0); break;
+        case 'u': update_package = optarg; break;
+        case 'w': should_wipe_data = true; break;
+        case 'c': should_wipe_cache = true; break;
+        case 't': show_text = true; break;
+        case 's': sideload = true; break;
+        case 'a': sideload = true; sideload_auto_reboot = true; break;
+        case 'x': just_exit = true; break;
+        case 'l': locale = optarg; break;
+        case 'g': {
+            if (stage == NULL || *stage == '\0') {
+                char buffer[20] = "1/";
+                strncat(buffer, optarg, sizeof(buffer)-3);
+                stage = strdup(buffer);
+            }
+            break;
+        }
+        case 'p': shutdown_after = true; break;
+        case 'r': reason = optarg; break;
+        case 0: {
+            if (strcmp(OPTIONS[option_index].name, "brick") == 0) {
+                should_brick = true;
+                break;
+            }
+            break;
+        }
+        case '?':
+            LOGE("Invalid command argument\n");
+            continue;
+        }
+    }
+
+    if (locale == nullptr && has_cache) {
+        load_locale_from_cache();
+    }
+    printf("locale is [%s]\n", locale);
+    printf("stage is [%s]\n", stage);
+    printf("reason is [%s]\n", reason);
+
+    Device* device = make_device();
+    ui = device->GetUI();
+    gCurrentUI = ui;
+
+    ui->SetLocale(locale);
+    ui->Init();
+
+    int st_cur, st_max;
+    if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
+        ui->SetStage(st_cur, st_max);
+    }
+
+    ui->SetBackground(RecoveryUI::NONE);
+    if (show_text) ui->ShowText(true);
+
+    struct selinux_opt seopts[] = {
+      { SELABEL_OPT_PATH, "/file_contexts" }
+    };
+
+    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);
+
+    if (!sehandle) {
+        ui->Print("Warning: No file_contexts\n");
+    }
+
+    device->StartRecovery();
+
+    printf("Command:");
+    for (arg = 0; arg < argc; arg++) {
+        printf(" \"%s\"", argv[arg]);
+    }
+    printf("\n");
+
+    if (update_package) {
+        // For backwards compatibility on the cache partition only, if
+        // we're given an old 'root' path "CACHE:foo", change it to
+        // "/cache/foo".
+        if (strncmp(update_package, "CACHE:", 6) == 0) {
+            int len = strlen(update_package) + 10;
+            char* modified_path = (char*)malloc(len);
+            if (modified_path) {
+                strlcpy(modified_path, "/cache/", len);
+                strlcat(modified_path, update_package+6, len);
+                printf("(replacing path \"%s\" with \"%s\")\n",
+                       update_package, modified_path);
+                update_package = modified_path;
+            }
+            else
+                printf("modified_path allocation failed\n");
+        }
+    }
+    printf("\n");
+
+    property_list(print_property, NULL);
+    printf("\n");
+
+    ui->Print("Supported API: %d\n", RECOVERY_API_VERSION);
+
+    int status = INSTALL_SUCCESS;
+
+    if (update_package != NULL) {
+        // It's not entirely true that we will modify the flash. But we want
+        // to log the update attempt since update_package is non-NULL.
+        modified_flash = true;
+
+        if (!is_battery_ok()) {
+            ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
+                      BATTERY_OK_PERCENTAGE);
+            status = INSTALL_SKIPPED;
+        } else {
+            status = install_package(update_package, &should_wipe_cache,
+                                     TEMPORARY_INSTALL_FILE, true);
+            if (status == INSTALL_SUCCESS && should_wipe_cache) {
+                wipe_cache(false, device);
+            }
+            if (status != INSTALL_SUCCESS) {
+                ui->Print("Installation aborted.\n");
+                // When I/O error happens, reboot and retry installation EIO_RETRY_COUNT
+                // times before we abandon this OTA update.
+                if (status == INSTALL_RETRY && retry_count < EIO_RETRY_COUNT) {
+                    copy_logs();
+                    set_retry_bootloader_message(retry_count, argc, argv);
+                    // Print retry count on screen.
+                    ui->Print("Retry attempt %d\n", retry_count);
+
+                    // Reboot and retry the update
+                    int ret = property_set(ANDROID_RB_PROPERTY, "reboot,recovery");
+                    if (ret < 0) {
+                        ui->Print("Reboot failed\n");
+                    } else {
+                        while (true) {
+                            pause();
+                        }
+                    }
+                }
+                // If this is an eng or userdebug build, then automatically
+                // turn the text display on if the script fails so the error
+                // message is visible.
+                if (is_ro_debuggable()) {
+                    ui->ShowText(true);
+                }
+            }
+        }
+    } else if (should_wipe_data) {
+        if (!wipe_data(false, device)) {
+            status = INSTALL_ERROR;
+        }
+    } else if (should_wipe_cache) {
+        if (!wipe_cache(false, device)) {
+            status = INSTALL_ERROR;
+        }
+    } else if (should_brick) {
+        if (!brick_device()) {
+            status = INSTALL_ERROR;
+        }
+    } else if (sideload) {
+        // 'adb reboot sideload' acts the same as user presses key combinations
+        // to enter the sideload mode. When 'sideload-auto-reboot' is used, text
+        // display will NOT be turned on by default. And it will reboot after
+        // sideload finishes even if there are errors. Unless one turns on the
+        // text display during the installation. This is to enable automated
+        // testing.
+        if (!sideload_auto_reboot) {
+            ui->ShowText(true);
+        }
+        status = apply_from_adb(ui, &should_wipe_cache, TEMPORARY_INSTALL_FILE);
+        if (status == INSTALL_SUCCESS && should_wipe_cache) {
+            if (!wipe_cache(false, device)) {
+                status = INSTALL_ERROR;
+            }
+        }
+        ui->Print("\nInstall from ADB complete (status: %d).\n", status);
+        if (sideload_auto_reboot) {
+            ui->Print("Rebooting automatically.\n");
+        }
+    } else if (!just_exit) {
+        status = INSTALL_NONE;  // No command specified
+        ui->SetBackground(RecoveryUI::NO_COMMAND);
+
+        // http://b/17489952
+        // If this is an eng or userdebug build, automatically turn on the
+        // text display if no command is specified.
+        if (is_ro_debuggable()) {
+            ui->ShowText(true);
+        }
+    }
+
+    if (!sideload_auto_reboot && (status == INSTALL_ERROR || status == INSTALL_CORRUPT)) {
+        copy_logs();
+        ui->SetBackground(RecoveryUI::ERROR);
+    }
+
+    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
+    if ((status != INSTALL_SUCCESS && status != INSTALL_SKIPPED && !sideload_auto_reboot) ||
+            ui->IsTextVisible()) {
+        Device::BuiltinAction temp = prompt_and_wait(device, status);
+        if (temp != Device::NO_ACTION) {
+            after = temp;
+        }
+    }
+
+    // Save logs and clean up before rebooting or shutting down.
+    finish_recovery();
+
+    switch (after) {
+        case Device::SHUTDOWN:
+            ui->Print("Shutting down...\n");
+            property_set(ANDROID_RB_PROPERTY, "shutdown,");
+            break;
+
+        case Device::REBOOT_BOOTLOADER:
+            ui->Print("Rebooting to bootloader...\n");
+            property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
+            break;
+
+        default:
+            ui->Print("Rebooting...\n");
+            property_set(ANDROID_RB_PROPERTY, "reboot,");
+            break;
+    }
+    while (true) {
+      pause();
+    }
+    // Should be unreachable.
+    return EXIT_SUCCESS;
+}
diff --git a/recovery/res-560dpi b/recovery/res-560dpi
new file mode 120000
index 0000000..8576a9b
--- /dev/null
+++ b/recovery/res-560dpi
@@ -0,0 +1 @@
+res-xxhdpi
\ No newline at end of file
diff --git a/recovery/res-hdpi/images/erasing_text.png b/recovery/res-hdpi/images/erasing_text.png
new file mode 100644
index 0000000..2186c19
--- /dev/null
+++ b/recovery/res-hdpi/images/erasing_text.png
Binary files differ
diff --git a/recovery/res-hdpi/images/error_text.png b/recovery/res-hdpi/images/error_text.png
new file mode 100644
index 0000000..9700f45
--- /dev/null
+++ b/recovery/res-hdpi/images/error_text.png
Binary files differ
diff --git a/recovery/res-hdpi/images/icon_error.png b/recovery/res-hdpi/images/icon_error.png
new file mode 100644
index 0000000..cb3d1ab
--- /dev/null
+++ b/recovery/res-hdpi/images/icon_error.png
Binary files differ
diff --git a/recovery/res-hdpi/images/icon_installing.png b/recovery/res-hdpi/images/icon_installing.png
new file mode 100644
index 0000000..0fcfbc2
--- /dev/null
+++ b/recovery/res-hdpi/images/icon_installing.png
Binary files differ
diff --git a/recovery/res-hdpi/images/installing_security_text.png b/recovery/res-hdpi/images/installing_security_text.png
new file mode 100644
index 0000000..0f60595
--- /dev/null
+++ b/recovery/res-hdpi/images/installing_security_text.png
Binary files differ
diff --git a/recovery/res-hdpi/images/installing_text.png b/recovery/res-hdpi/images/installing_text.png
new file mode 100644
index 0000000..ec8b875
--- /dev/null
+++ b/recovery/res-hdpi/images/installing_text.png
Binary files differ
diff --git a/recovery/res-hdpi/images/no_command_text.png b/recovery/res-hdpi/images/no_command_text.png
new file mode 100644
index 0000000..3eddcbb
--- /dev/null
+++ b/recovery/res-hdpi/images/no_command_text.png
Binary files differ
diff --git a/recovery/res-hdpi/images/progress_empty.png b/recovery/res-hdpi/images/progress_empty.png
new file mode 100644
index 0000000..7258183
--- /dev/null
+++ b/recovery/res-hdpi/images/progress_empty.png
Binary files differ
diff --git a/recovery/res-hdpi/images/progress_fill.png b/recovery/res-hdpi/images/progress_fill.png
new file mode 100644
index 0000000..becf87b
--- /dev/null
+++ b/recovery/res-hdpi/images/progress_fill.png
Binary files differ
diff --git a/recovery/res-hdpi/images/stage_empty.png b/recovery/res-hdpi/images/stage_empty.png
new file mode 100644
index 0000000..251ec19
--- /dev/null
+++ b/recovery/res-hdpi/images/stage_empty.png
Binary files differ
diff --git a/recovery/res-hdpi/images/stage_fill.png b/recovery/res-hdpi/images/stage_fill.png
new file mode 100644
index 0000000..1ab79e8
--- /dev/null
+++ b/recovery/res-hdpi/images/stage_fill.png
Binary files differ
diff --git a/recovery/res-mdpi/images/erasing_text.png b/recovery/res-mdpi/images/erasing_text.png
new file mode 100644
index 0000000..b0dd3c6
--- /dev/null
+++ b/recovery/res-mdpi/images/erasing_text.png
Binary files differ
diff --git a/recovery/res-mdpi/images/error_text.png b/recovery/res-mdpi/images/error_text.png
new file mode 100644
index 0000000..6a47a59
--- /dev/null
+++ b/recovery/res-mdpi/images/error_text.png
Binary files differ
diff --git a/recovery/res-mdpi/images/icon_error.png b/recovery/res-mdpi/images/icon_error.png
new file mode 100644
index 0000000..cb3d1ab
--- /dev/null
+++ b/recovery/res-mdpi/images/icon_error.png
Binary files differ
diff --git a/recovery/res-mdpi/images/icon_installing.png b/recovery/res-mdpi/images/icon_installing.png
new file mode 100644
index 0000000..0fcfbc2
--- /dev/null
+++ b/recovery/res-mdpi/images/icon_installing.png
Binary files differ
diff --git a/recovery/res-mdpi/images/installing_security_text.png b/recovery/res-mdpi/images/installing_security_text.png
new file mode 100644
index 0000000..1499398
--- /dev/null
+++ b/recovery/res-mdpi/images/installing_security_text.png
Binary files differ
diff --git a/recovery/res-mdpi/images/installing_text.png b/recovery/res-mdpi/images/installing_text.png
new file mode 100644
index 0000000..01e9bfe
--- /dev/null
+++ b/recovery/res-mdpi/images/installing_text.png
Binary files differ
diff --git a/recovery/res-mdpi/images/no_command_text.png b/recovery/res-mdpi/images/no_command_text.png
new file mode 100644
index 0000000..d340df5
--- /dev/null
+++ b/recovery/res-mdpi/images/no_command_text.png
Binary files differ
diff --git a/recovery/res-mdpi/images/progress_empty.png b/recovery/res-mdpi/images/progress_empty.png
new file mode 100644
index 0000000..7258183
--- /dev/null
+++ b/recovery/res-mdpi/images/progress_empty.png
Binary files differ
diff --git a/recovery/res-mdpi/images/progress_fill.png b/recovery/res-mdpi/images/progress_fill.png
new file mode 100644
index 0000000..becf87b
--- /dev/null
+++ b/recovery/res-mdpi/images/progress_fill.png
Binary files differ
diff --git a/recovery/res-mdpi/images/stage_empty.png b/recovery/res-mdpi/images/stage_empty.png
new file mode 100644
index 0000000..251ec19
--- /dev/null
+++ b/recovery/res-mdpi/images/stage_empty.png
Binary files differ
diff --git a/recovery/res-mdpi/images/stage_fill.png b/recovery/res-mdpi/images/stage_fill.png
new file mode 100644
index 0000000..1ab79e8
--- /dev/null
+++ b/recovery/res-mdpi/images/stage_fill.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/erasing_text.png b/recovery/res-xhdpi/images/erasing_text.png
new file mode 100644
index 0000000..2f8b469
--- /dev/null
+++ b/recovery/res-xhdpi/images/erasing_text.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/error_text.png b/recovery/res-xhdpi/images/error_text.png
new file mode 100644
index 0000000..ad18851
--- /dev/null
+++ b/recovery/res-xhdpi/images/error_text.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/icon_error.png b/recovery/res-xhdpi/images/icon_error.png
new file mode 100644
index 0000000..cb3d1ab
--- /dev/null
+++ b/recovery/res-xhdpi/images/icon_error.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/icon_installing.png b/recovery/res-xhdpi/images/icon_installing.png
new file mode 100644
index 0000000..0fcfbc2
--- /dev/null
+++ b/recovery/res-xhdpi/images/icon_installing.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/installing_security_text.png b/recovery/res-xhdpi/images/installing_security_text.png
new file mode 100644
index 0000000..acc6a7c
--- /dev/null
+++ b/recovery/res-xhdpi/images/installing_security_text.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/installing_text.png b/recovery/res-xhdpi/images/installing_text.png
new file mode 100644
index 0000000..32897d0
--- /dev/null
+++ b/recovery/res-xhdpi/images/installing_text.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/no_command_text.png b/recovery/res-xhdpi/images/no_command_text.png
new file mode 100644
index 0000000..eb43c59
--- /dev/null
+++ b/recovery/res-xhdpi/images/no_command_text.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/progress_empty.png b/recovery/res-xhdpi/images/progress_empty.png
new file mode 100644
index 0000000..7258183
--- /dev/null
+++ b/recovery/res-xhdpi/images/progress_empty.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/progress_fill.png b/recovery/res-xhdpi/images/progress_fill.png
new file mode 100644
index 0000000..becf87b
--- /dev/null
+++ b/recovery/res-xhdpi/images/progress_fill.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/stage_empty.png b/recovery/res-xhdpi/images/stage_empty.png
new file mode 100644
index 0000000..251ec19
--- /dev/null
+++ b/recovery/res-xhdpi/images/stage_empty.png
Binary files differ
diff --git a/recovery/res-xhdpi/images/stage_fill.png b/recovery/res-xhdpi/images/stage_fill.png
new file mode 100644
index 0000000..1ab79e8
--- /dev/null
+++ b/recovery/res-xhdpi/images/stage_fill.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/erasing_text.png b/recovery/res-xxhdpi/images/erasing_text.png
new file mode 100644
index 0000000..8ff2b2f
--- /dev/null
+++ b/recovery/res-xxhdpi/images/erasing_text.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/error_text.png b/recovery/res-xxhdpi/images/error_text.png
new file mode 100644
index 0000000..658d4ea
--- /dev/null
+++ b/recovery/res-xxhdpi/images/error_text.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/icon_error.png b/recovery/res-xxhdpi/images/icon_error.png
new file mode 100644
index 0000000..cb3d1ab
--- /dev/null
+++ b/recovery/res-xxhdpi/images/icon_error.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/icon_installing.png b/recovery/res-xxhdpi/images/icon_installing.png
new file mode 100644
index 0000000..0fcfbc2
--- /dev/null
+++ b/recovery/res-xxhdpi/images/icon_installing.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/installing_security_text.png b/recovery/res-xxhdpi/images/installing_security_text.png
new file mode 100644
index 0000000..23fcaa4
--- /dev/null
+++ b/recovery/res-xxhdpi/images/installing_security_text.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/installing_text.png b/recovery/res-xxhdpi/images/installing_text.png
new file mode 100644
index 0000000..fd8e584
--- /dev/null
+++ b/recovery/res-xxhdpi/images/installing_text.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/no_command_text.png b/recovery/res-xxhdpi/images/no_command_text.png
new file mode 100644
index 0000000..23932d6
--- /dev/null
+++ b/recovery/res-xxhdpi/images/no_command_text.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/progress_empty.png b/recovery/res-xxhdpi/images/progress_empty.png
new file mode 100644
index 0000000..7258183
--- /dev/null
+++ b/recovery/res-xxhdpi/images/progress_empty.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/progress_fill.png b/recovery/res-xxhdpi/images/progress_fill.png
new file mode 100644
index 0000000..becf87b
--- /dev/null
+++ b/recovery/res-xxhdpi/images/progress_fill.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/stage_empty.png b/recovery/res-xxhdpi/images/stage_empty.png
new file mode 100644
index 0000000..251ec19
--- /dev/null
+++ b/recovery/res-xxhdpi/images/stage_empty.png
Binary files differ
diff --git a/recovery/res-xxhdpi/images/stage_fill.png b/recovery/res-xxhdpi/images/stage_fill.png
new file mode 100644
index 0000000..1ab79e8
--- /dev/null
+++ b/recovery/res-xxhdpi/images/stage_fill.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/erasing_text.png b/recovery/res-xxxhdpi/images/erasing_text.png
new file mode 100644
index 0000000..0315293
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/erasing_text.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/error_text.png b/recovery/res-xxxhdpi/images/error_text.png
new file mode 100644
index 0000000..dba127f
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/error_text.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/icon_error.png b/recovery/res-xxxhdpi/images/icon_error.png
new file mode 100644
index 0000000..cb3d1ab
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/icon_error.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/icon_installing.png b/recovery/res-xxxhdpi/images/icon_installing.png
new file mode 100644
index 0000000..0fcfbc2
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/icon_installing.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/installing_security_text.png b/recovery/res-xxxhdpi/images/installing_security_text.png
new file mode 100644
index 0000000..6cdbef4
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/installing_security_text.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/installing_text.png b/recovery/res-xxxhdpi/images/installing_text.png
new file mode 100644
index 0000000..32511a9
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/installing_text.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/no_command_text.png b/recovery/res-xxxhdpi/images/no_command_text.png
new file mode 100644
index 0000000..b6cdd77
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/no_command_text.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/progress_empty.png b/recovery/res-xxxhdpi/images/progress_empty.png
new file mode 100644
index 0000000..7258183
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/progress_empty.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/progress_fill.png b/recovery/res-xxxhdpi/images/progress_fill.png
new file mode 100644
index 0000000..becf87b
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/progress_fill.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/stage_empty.png b/recovery/res-xxxhdpi/images/stage_empty.png
new file mode 100644
index 0000000..251ec19
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/stage_empty.png
Binary files differ
diff --git a/recovery/res-xxxhdpi/images/stage_fill.png b/recovery/res-xxxhdpi/images/stage_fill.png
new file mode 100644
index 0000000..1ab79e8
--- /dev/null
+++ b/recovery/res-xxxhdpi/images/stage_fill.png
Binary files differ
diff --git a/recovery/roots.cpp b/recovery/roots.cpp
new file mode 100644
index 0000000..b113d9a
--- /dev/null
+++ b/recovery/roots.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#include "roots.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <fcntl.h>
+
+#include <fs_mgr.h>
+#include "common.h"
+#include "make_ext4fs.h"
+#include "mounts.h"
+#include "wipe.h"
+#include "cryptfs.h"
+
+static struct fstab *fstab = NULL;
+
+extern struct selabel_handle *sehandle;
+
+void load_volume_table()
+{
+    int i;
+    int ret;
+
+    fstab = fs_mgr_read_fstab("/etc/recovery.fstab");
+    if (!fstab) {
+        LOGE("failed to read /etc/recovery.fstab\n");
+        return;
+    }
+
+    ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
+    if (ret < 0 ) {
+        LOGE("failed to add /tmp entry to fstab\n");
+        fs_mgr_free_fstab(fstab);
+        fstab = NULL;
+        return;
+    }
+
+    printf("recovery filesystem table\n");
+    printf("=========================\n");
+    for (i = 0; i < fstab->num_entries; ++i) {
+        Volume* v = &fstab->recs[i];
+        printf("  %d %s %s %s %lld\n", i, v->mount_point, v->fs_type,
+               v->blk_device, v->length);
+    }
+    printf("\n");
+}
+
+Volume* volume_for_path(const char* path) {
+    return fs_mgr_get_entry_for_mount_point(fstab, path);
+}
+
+// Mount the volume specified by path at the given mount_point.
+int ensure_path_mounted_at(const char* path, const char* mount_point) {
+    Volume* v = volume_for_path(path);
+    if (v == NULL) {
+        LOGE("unknown volume for path [%s]\n", path);
+        return -1;
+    }
+    if (strcmp(v->fs_type, "ramdisk") == 0) {
+        // the ramdisk is always mounted.
+        return 0;
+    }
+
+    if (!scan_mounted_volumes()) {
+        LOGE("failed to scan mounted volumes\n");
+        return -1;
+    }
+
+    if (!mount_point) {
+        mount_point = v->mount_point;
+    }
+
+    MountedVolume* mv = find_mounted_volume_by_mount_point(mount_point);
+    if (mv) {
+        // volume is already mounted
+        return 0;
+    }
+
+    mkdir(mount_point, 0755);  // in case it doesn't already exist
+
+    if (strcmp(v->fs_type, "ext4") == 0 ||
+               strcmp(v->fs_type, "squashfs") == 0 ||
+               strcmp(v->fs_type, "vfat") == 0) {
+        int result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options);
+        if (result == -1 && fs_mgr_is_formattable(v)) {
+            LOGE("failed to mount %s (%s), formatting ...\n",
+                    mount_point, strerror(errno));
+            bool crypt_footer = fs_mgr_is_encryptable(v) && !strcmp(v->key_loc, "footer");
+            if (fs_mgr_do_format(v, crypt_footer) == 0) {
+                result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options);
+            } else {
+                LOGE("failed to format %s (%s)\n", mount_point, strerror(errno));
+                return -1;
+            }
+        }
+
+        if (result == -1) {
+            LOGE("failed to mount %s (%s)\n", mount_point, strerror(errno));
+            return -1;
+        }
+        return 0;
+    }
+
+    LOGE("unknown fs_type \"%s\" for %s\n", v->fs_type, mount_point);
+    return -1;
+}
+
+int ensure_path_mounted(const char* path) {
+    // Mount at the default mount point.
+    return ensure_path_mounted_at(path, nullptr);
+}
+
+int ensure_path_unmounted(const char* path) {
+    Volume* v = volume_for_path(path);
+    if (v == NULL) {
+        LOGE("unknown volume for path [%s]\n", path);
+        return -1;
+    }
+    if (strcmp(v->fs_type, "ramdisk") == 0) {
+        // the ramdisk is always mounted; you can't unmount it.
+        return -1;
+    }
+
+    if (!scan_mounted_volumes()) {
+        LOGE("failed to scan mounted volumes\n");
+        return -1;
+    }
+
+    MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
+    if (mv == NULL) {
+        // volume is already unmounted
+        return 0;
+    }
+
+    return unmount_mounted_volume(mv);
+}
+
+static int exec_cmd(const char* path, char* const argv[]) {
+    int status;
+    pid_t child;
+    if ((child = vfork()) == 0) {
+        execv(path, argv);
+        _exit(-1);
+    }
+    waitpid(child, &status, 0);
+    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+        LOGE("%s failed with status %d\n", path, WEXITSTATUS(status));
+    }
+    return WEXITSTATUS(status);
+}
+
+int format_volume(const char* volume) {
+    Volume* v = volume_for_path(volume);
+    if (v == NULL) {
+        LOGE("unknown volume \"%s\"\n", volume);
+        return -1;
+    }
+    if (strcmp(v->fs_type, "ramdisk") == 0) {
+        // you can't format the ramdisk.
+        LOGE("can't format_volume \"%s\"", volume);
+        return -1;
+    }
+    if (strcmp(v->mount_point, volume) != 0) {
+        LOGE("can't give path \"%s\" to format_volume\n", volume);
+        return -1;
+    }
+
+    if (ensure_path_unmounted(volume) != 0) {
+        LOGE("format_volume failed to unmount \"%s\"\n", v->mount_point);
+        return -1;
+    }
+
+    if (strcmp(v->fs_type, "ext4") == 0 || strcmp(v->fs_type, "f2fs") == 0) {
+        // if there's a key_loc that looks like a path, it should be a
+        // block device for storing encryption metadata.  wipe it too.
+        if (v->key_loc != NULL && v->key_loc[0] == '/') {
+            LOGI("wiping %s\n", v->key_loc);
+            int fd = open(v->key_loc, O_WRONLY | O_CREAT, 0644);
+            if (fd < 0) {
+                LOGE("format_volume: failed to open %s\n", v->key_loc);
+                return -1;
+            }
+            wipe_block_device(fd, get_file_size(fd));
+            close(fd);
+        }
+
+        ssize_t length = 0;
+        if (v->length != 0) {
+            length = v->length;
+        } else if (v->key_loc != NULL && strcmp(v->key_loc, "footer") == 0) {
+            length = -CRYPT_FOOTER_OFFSET;
+        }
+        int result;
+        if (strcmp(v->fs_type, "ext4") == 0) {
+            result = make_ext4fs(v->blk_device, length, volume, sehandle);
+        } else {   /* Has to be f2fs because we checked earlier. */
+            if (v->key_loc != NULL && strcmp(v->key_loc, "footer") == 0 && length < 0) {
+                LOGE("format_volume: crypt footer + negative length (%zd) not supported on %s\n", length, v->fs_type);
+                return -1;
+            }
+            if (length < 0) {
+                LOGE("format_volume: negative length (%zd) not supported on %s\n", length, v->fs_type);
+                return -1;
+            }
+            char *num_sectors;
+            if (asprintf(&num_sectors, "%zd", length / 512) <= 0) {
+                LOGE("format_volume: failed to create %s command for %s\n", v->fs_type, v->blk_device);
+                return -1;
+            }
+            const char *f2fs_path = "/sbin/mkfs.f2fs";
+            const char* const f2fs_argv[] = {"mkfs.f2fs", "-t", "-d1", v->blk_device, num_sectors, NULL};
+
+            result = exec_cmd(f2fs_path, (char* const*)f2fs_argv);
+            free(num_sectors);
+        }
+        if (result != 0) {
+            LOGE("format_volume: make %s failed on %s with %d(%s)\n", v->fs_type, v->blk_device, result, strerror(errno));
+            return -1;
+        }
+        return 0;
+    }
+
+    LOGE("format_volume: fs_type \"%s\" unsupported\n", v->fs_type);
+    return -1;
+}
+
+int setup_install_mounts() {
+    if (fstab == NULL) {
+        LOGE("can't set up install mounts: no fstab loaded\n");
+        return -1;
+    }
+    for (int i = 0; i < fstab->num_entries; ++i) {
+        Volume* v = fstab->recs + i;
+
+        if (strcmp(v->mount_point, "/tmp") == 0 ||
+            strcmp(v->mount_point, "/cache") == 0) {
+            if (ensure_path_mounted(v->mount_point) != 0) {
+                LOGE("failed to mount %s\n", v->mount_point);
+                return -1;
+            }
+
+        } else {
+            if (ensure_path_unmounted(v->mount_point) != 0) {
+                LOGE("failed to unmount %s\n", v->mount_point);
+                return -1;
+            }
+        }
+    }
+    return 0;
+}
diff --git a/recovery/roots.h b/recovery/roots.h
new file mode 100644
index 0000000..6e3b243
--- /dev/null
+++ b/recovery/roots.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef RECOVERY_ROOTS_H_
+#define RECOVERY_ROOTS_H_
+
+#include "common.h"
+
+// Load and parse volume data from /etc/recovery.fstab.
+void load_volume_table();
+
+// Return the Volume* record for this path (or NULL).
+Volume* volume_for_path(const char* path);
+
+// Make sure that the volume 'path' is on is mounted.  Returns 0 on
+// success (volume is mounted).
+int ensure_path_mounted(const char* path);
+
+// Similar to ensure_path_mounted, but allows one to specify the mount_point.
+int ensure_path_mounted_at(const char* path, const char* mount_point);
+
+// Make sure that the volume 'path' is on is unmounted.  Returns 0 on
+// success (volume is unmounted);
+int ensure_path_unmounted(const char* path);
+
+// Reformat the given volume (must be the mount point only, eg
+// "/cache"), no paths permitted.  Attempts to unmount the volume if
+// it is mounted.
+int format_volume(const char* volume);
+
+// Ensure that all and only the volumes that packages expect to find
+// mounted (/tmp and /cache) are mounted.  Returns 0 on success.
+int setup_install_mounts();
+
+#endif  // RECOVERY_ROOTS_H_
diff --git a/recovery/screen_ui.cpp b/recovery/screen_ui.cpp
new file mode 100644
index 0000000..1d33269
--- /dev/null
+++ b/recovery/screen_ui.cpp
@@ -0,0 +1,725 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/input.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include <android-base/strings.h>
+#include <android-base/stringprintf.h>
+#include <cutils/properties.h>
+
+#include "common.h"
+#include "device.h"
+#include "minui/minui.h"
+#include "screen_ui.h"
+#include "ui.h"
+
+static int char_width;
+static int char_height;
+
+// Return the current time as a double (including fractions of a second).
+static double now() {
+    struct timeval tv;
+    gettimeofday(&tv, nullptr);
+    return tv.tv_sec + tv.tv_usec / 1000000.0;
+}
+
+ScreenRecoveryUI::ScreenRecoveryUI() :
+    currentIcon(NONE),
+    installingFrame(0),
+    locale(nullptr),
+    rtl_locale(false),
+    progressBarType(EMPTY),
+    progressScopeStart(0),
+    progressScopeSize(0),
+    progress(0),
+    pagesIdentical(false),
+    text_cols_(0),
+    text_rows_(0),
+    text_(nullptr),
+    text_col_(0),
+    text_row_(0),
+    text_top_(0),
+    show_text(false),
+    show_text_ever(false),
+    menu_(nullptr),
+    show_menu(false),
+    menu_items(0),
+    menu_sel(0),
+    file_viewer_text_(nullptr),
+    animation_fps(-1),
+    installing_frames(-1),
+    stage(-1),
+    max_stage(-1) {
+
+    for (int i = 0; i < 5; i++) {
+        backgroundIcon[i] = nullptr;
+    }
+    pthread_mutex_init(&updateMutex, nullptr);
+}
+
+// Clear the screen and draw the currently selected background icon (if any).
+// Should only be called with updateMutex locked.
+void ScreenRecoveryUI::draw_background_locked(Icon icon) {
+    pagesIdentical = false;
+    gr_color(0, 0, 0, 255);
+    gr_clear();
+
+    if (icon) {
+        GRSurface* surface = backgroundIcon[icon];
+        if (icon == INSTALLING_UPDATE || icon == ERASING) {
+            surface = installation[installingFrame];
+        }
+        GRSurface* text_surface = backgroundText[icon];
+
+        int iconWidth = gr_get_width(surface);
+        int iconHeight = gr_get_height(surface);
+        int textWidth = gr_get_width(text_surface);
+        int textHeight = gr_get_height(text_surface);
+        int stageHeight = gr_get_height(stageMarkerEmpty);
+
+        int sh = (max_stage >= 0) ? stageHeight : 0;
+
+        iconX = (gr_fb_width() - iconWidth) / 2;
+        iconY = (gr_fb_height() - (iconHeight+textHeight+40+sh)) / 2;
+
+        int textX = (gr_fb_width() - textWidth) / 2;
+        int textY = ((gr_fb_height() - (iconHeight+textHeight+40+sh)) / 2) + iconHeight + 40;
+
+        gr_blit(surface, 0, 0, iconWidth, iconHeight, iconX, iconY);
+        if (stageHeight > 0) {
+            int sw = gr_get_width(stageMarkerEmpty);
+            int x = (gr_fb_width() - max_stage * gr_get_width(stageMarkerEmpty)) / 2;
+            int y = iconY + iconHeight + 20;
+            for (int i = 0; i < max_stage; ++i) {
+                gr_blit((i < stage) ? stageMarkerFill : stageMarkerEmpty,
+                        0, 0, sw, stageHeight, x, y);
+                x += sw;
+            }
+        }
+
+        gr_color(255, 255, 255, 255);
+        gr_texticon(textX, textY, text_surface);
+    }
+}
+
+// Draw the progress bar (if any) on the screen.  Does not flip pages.
+// Should only be called with updateMutex locked.
+void ScreenRecoveryUI::draw_progress_locked() {
+    if (currentIcon == ERROR) return;
+
+    if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
+        GRSurface* icon = installation[installingFrame];
+        gr_blit(icon, 0, 0, gr_get_width(icon), gr_get_height(icon), iconX, iconY);
+    }
+
+    if (progressBarType != EMPTY) {
+        int iconHeight = gr_get_height(backgroundIcon[INSTALLING_UPDATE]);
+        int width = gr_get_width(progressBarEmpty);
+        int height = gr_get_height(progressBarEmpty);
+
+        int dx = (gr_fb_width() - width)/2;
+        int dy = (3*gr_fb_height() + iconHeight - 2*height)/4;
+
+        // Erase behind the progress bar (in case this was a progress-only update)
+        gr_color(0, 0, 0, 255);
+        gr_fill(dx, dy, width, height);
+
+        if (progressBarType == DETERMINATE) {
+            float p = progressScopeStart + progress * progressScopeSize;
+            int pos = (int) (p * width);
+
+            if (rtl_locale) {
+                // Fill the progress bar from right to left.
+                if (pos > 0) {
+                    gr_blit(progressBarFill, width-pos, 0, pos, height, dx+width-pos, dy);
+                }
+                if (pos < width-1) {
+                    gr_blit(progressBarEmpty, 0, 0, width-pos, height, dx, dy);
+                }
+            } else {
+                // Fill the progress bar from left to right.
+                if (pos > 0) {
+                    gr_blit(progressBarFill, 0, 0, pos, height, dx, dy);
+                }
+                if (pos < width-1) {
+                    gr_blit(progressBarEmpty, pos, 0, width-pos, height, dx+pos, dy);
+                }
+            }
+        }
+    }
+}
+
+void ScreenRecoveryUI::SetColor(UIElement e) {
+    switch (e) {
+        case INFO:
+            gr_color(249, 194, 0, 255);
+            break;
+        case HEADER:
+            gr_color(247, 0, 6, 255);
+            break;
+        case MENU:
+        case MENU_SEL_BG:
+            gr_color(0, 106, 157, 255);
+            break;
+        case MENU_SEL_BG_ACTIVE:
+            gr_color(0, 156, 100, 255);
+            break;
+        case MENU_SEL_FG:
+            gr_color(255, 255, 255, 255);
+            break;
+        case LOG:
+            gr_color(196, 196, 196, 255);
+            break;
+        case TEXT_FILL:
+            gr_color(0, 0, 0, 160);
+            break;
+        default:
+            gr_color(255, 255, 255, 255);
+            break;
+    }
+}
+
+void ScreenRecoveryUI::DrawHorizontalRule(int* y) {
+    SetColor(MENU);
+    *y += 4;
+    gr_fill(0, *y, gr_fb_width(), *y + 2);
+    *y += 4;
+}
+
+void ScreenRecoveryUI::DrawTextLine(int* y, const char* line, bool bold) {
+    gr_text(4, *y, line, bold);
+    *y += char_height + 4;
+}
+
+void ScreenRecoveryUI::DrawTextLines(int* y, const char* const* lines) {
+    for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
+        DrawTextLine(y, lines[i], false);
+    }
+}
+
+static const char* REGULAR_HELP[] = {
+    "Use volume up/down and power.",
+    NULL
+};
+
+static const char* LONG_PRESS_HELP[] = {
+    "Any button cycles highlight.",
+    "Long-press activates.",
+    NULL
+};
+
+// Redraw everything on the screen.  Does not flip pages.
+// Should only be called with updateMutex locked.
+void ScreenRecoveryUI::draw_screen_locked() {
+    if (!show_text) {
+        draw_background_locked(currentIcon);
+        draw_progress_locked();
+    } else {
+        gr_color(0, 0, 0, 255);
+        gr_clear();
+
+        int y = 0;
+        if (show_menu) {
+            char recovery_fingerprint[PROPERTY_VALUE_MAX];
+            property_get("ro.bootimage.build.fingerprint", recovery_fingerprint, "");
+
+            SetColor(INFO);
+            DrawTextLine(&y, "Android Recovery", true);
+            for (auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
+                DrawTextLine(&y, chunk.c_str(), false);
+            }
+            DrawTextLines(&y, HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
+
+            SetColor(HEADER);
+            DrawTextLines(&y, menu_headers_);
+
+            SetColor(MENU);
+            DrawHorizontalRule(&y);
+            y += 4;
+            for (int i = 0; i < menu_items; ++i) {
+                if (i == menu_sel) {
+                    // Draw the highlight bar.
+                    SetColor(IsLongPress() ? MENU_SEL_BG_ACTIVE : MENU_SEL_BG);
+                    gr_fill(0, y - 2, gr_fb_width(), y + char_height + 2);
+                    // Bold white text for the selected item.
+                    SetColor(MENU_SEL_FG);
+                    gr_text(4, y, menu_[i], true);
+                    SetColor(MENU);
+                } else {
+                    gr_text(4, y, menu_[i], false);
+                }
+                y += char_height + 4;
+            }
+            DrawHorizontalRule(&y);
+        }
+
+        // display from the bottom up, until we hit the top of the
+        // screen, the bottom of the menu, or we've displayed the
+        // entire text buffer.
+        SetColor(LOG);
+        int row = (text_top_ + text_rows_ - 1) % text_rows_;
+        size_t count = 0;
+        for (int ty = gr_fb_height() - char_height;
+             ty >= y && count < text_rows_;
+             ty -= char_height, ++count) {
+            gr_text(0, ty, text_[row], false);
+            --row;
+            if (row < 0) row = text_rows_ - 1;
+        }
+    }
+}
+
+// Redraw everything on the screen and flip the screen (make it visible).
+// Should only be called with updateMutex locked.
+void ScreenRecoveryUI::update_screen_locked() {
+    draw_screen_locked();
+    gr_flip();
+}
+
+// Updates only the progress bar, if possible, otherwise redraws the screen.
+// Should only be called with updateMutex locked.
+void ScreenRecoveryUI::update_progress_locked() {
+    if (show_text || !pagesIdentical) {
+        draw_screen_locked();    // Must redraw the whole screen
+        pagesIdentical = true;
+    } else {
+        draw_progress_locked();  // Draw only the progress bar and overlays
+    }
+    gr_flip();
+}
+
+// Keeps the progress bar updated, even when the process is otherwise busy.
+void* ScreenRecoveryUI::ProgressThreadStartRoutine(void* data) {
+    reinterpret_cast<ScreenRecoveryUI*>(data)->ProgressThreadLoop();
+    return nullptr;
+}
+
+void ScreenRecoveryUI::ProgressThreadLoop() {
+    double interval = 1.0 / animation_fps;
+    while (true) {
+        double start = now();
+        pthread_mutex_lock(&updateMutex);
+
+        int redraw = 0;
+
+        // update the installation animation, if active
+        // skip this if we have a text overlay (too expensive to update)
+        if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) &&
+            installing_frames > 0 && !show_text) {
+            installingFrame = (installingFrame + 1) % installing_frames;
+            redraw = 1;
+        }
+
+        // move the progress bar forward on timed intervals, if configured
+        int duration = progressScopeDuration;
+        if (progressBarType == DETERMINATE && duration > 0) {
+            double elapsed = now() - progressScopeTime;
+            float p = 1.0 * elapsed / duration;
+            if (p > 1.0) p = 1.0;
+            if (p > progress) {
+                progress = p;
+                redraw = 1;
+            }
+        }
+
+        if (redraw) update_progress_locked();
+
+        pthread_mutex_unlock(&updateMutex);
+        double end = now();
+        // minimum of 20ms delay between frames
+        double delay = interval - (end-start);
+        if (delay < 0.02) delay = 0.02;
+        usleep(static_cast<useconds_t>(delay * 1000000));
+    }
+}
+
+void ScreenRecoveryUI::LoadBitmap(const char* filename, GRSurface** surface) {
+    int result = res_create_display_surface(filename, surface);
+    if (result < 0) {
+        LOGE("missing bitmap %s\n(Code %d)\n", filename, result);
+    }
+}
+
+void ScreenRecoveryUI::LoadBitmapArray(const char* filename, int* frames, int* fps,
+        GRSurface*** surface) {
+    int result = res_create_multi_display_surface(filename, frames, fps, surface);
+    if (result < 0) {
+        LOGE("missing bitmap %s\n(Code %d)\n", filename, result);
+    }
+}
+
+void ScreenRecoveryUI::LoadLocalizedBitmap(const char* filename, GRSurface** surface) {
+    int result = res_create_localized_alpha_surface(filename, locale, surface);
+    if (result < 0) {
+        LOGE("missing bitmap %s\n(Code %d)\n", filename, result);
+    }
+}
+
+static char** Alloc2d(size_t rows, size_t cols) {
+    char** result = new char*[rows];
+    for (size_t i = 0; i < rows; ++i) {
+        result[i] = new char[cols];
+        memset(result[i], 0, cols);
+    }
+    return result;
+}
+
+void ScreenRecoveryUI::Init() {
+    gr_init();
+
+    gr_font_size(&char_width, &char_height);
+    text_rows_ = gr_fb_height() / char_height;
+    text_cols_ = gr_fb_width() / char_width;
+
+    text_ = Alloc2d(text_rows_, text_cols_ + 1);
+    file_viewer_text_ = Alloc2d(text_rows_, text_cols_ + 1);
+    menu_ = Alloc2d(text_rows_, text_cols_ + 1);
+
+    text_col_ = text_row_ = 0;
+    text_top_ = 1;
+
+    backgroundIcon[NONE] = nullptr;
+    LoadBitmapArray("icon_installing", &installing_frames, &animation_fps, &installation);
+    backgroundIcon[INSTALLING_UPDATE] = installing_frames ? installation[0] : nullptr;
+    backgroundIcon[ERASING] = backgroundIcon[INSTALLING_UPDATE];
+    LoadBitmap("icon_error", &backgroundIcon[ERROR]);
+    backgroundIcon[NO_COMMAND] = backgroundIcon[ERROR];
+
+    LoadBitmap("progress_empty", &progressBarEmpty);
+    LoadBitmap("progress_fill", &progressBarFill);
+    LoadBitmap("stage_empty", &stageMarkerEmpty);
+    LoadBitmap("stage_fill", &stageMarkerFill);
+
+    LoadLocalizedBitmap("installing_text", &backgroundText[INSTALLING_UPDATE]);
+    LoadLocalizedBitmap("erasing_text", &backgroundText[ERASING]);
+    LoadLocalizedBitmap("no_command_text", &backgroundText[NO_COMMAND]);
+    LoadLocalizedBitmap("error_text", &backgroundText[ERROR]);
+
+    pthread_create(&progress_thread_, nullptr, ProgressThreadStartRoutine, this);
+
+    RecoveryUI::Init();
+}
+
+void ScreenRecoveryUI::SetLocale(const char* new_locale) {
+    if (new_locale) {
+        this->locale = new_locale;
+        char* lang = strdup(locale);
+        for (char* p = lang; *p; ++p) {
+            if (*p == '_') {
+                *p = '\0';
+                break;
+            }
+        }
+
+        // A bit cheesy: keep an explicit list of supported languages
+        // that are RTL.
+        if (strcmp(lang, "ar") == 0 ||   // Arabic
+            strcmp(lang, "fa") == 0 ||   // Persian (Farsi)
+            strcmp(lang, "he") == 0 ||   // Hebrew (new language code)
+            strcmp(lang, "iw") == 0 ||   // Hebrew (old language code)
+            strcmp(lang, "ur") == 0) {   // Urdu
+            rtl_locale = true;
+        }
+        free(lang);
+    } else {
+        new_locale = nullptr;
+    }
+}
+
+void ScreenRecoveryUI::SetBackground(Icon icon) {
+    pthread_mutex_lock(&updateMutex);
+
+    currentIcon = icon;
+    update_screen_locked();
+
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::SetProgressType(ProgressType type) {
+    pthread_mutex_lock(&updateMutex);
+    if (progressBarType != type) {
+        progressBarType = type;
+    }
+    progressScopeStart = 0;
+    progressScopeSize = 0;
+    progress = 0;
+    update_progress_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::ShowProgress(float portion, float seconds) {
+    pthread_mutex_lock(&updateMutex);
+    progressBarType = DETERMINATE;
+    progressScopeStart += progressScopeSize;
+    progressScopeSize = portion;
+    progressScopeTime = now();
+    progressScopeDuration = seconds;
+    progress = 0;
+    update_progress_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::SetProgress(float fraction) {
+    pthread_mutex_lock(&updateMutex);
+    if (fraction < 0.0) fraction = 0.0;
+    if (fraction > 1.0) fraction = 1.0;
+    if (progressBarType == DETERMINATE && fraction > progress) {
+        // Skip updates that aren't visibly different.
+        int width = gr_get_width(progressBarEmpty);
+        float scale = width * progressScopeSize;
+        if ((int) (progress * scale) != (int) (fraction * scale)) {
+            progress = fraction;
+            update_progress_locked();
+        }
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::SetStage(int current, int max) {
+    pthread_mutex_lock(&updateMutex);
+    stage = current;
+    max_stage = max;
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::PrintV(const char* fmt, bool copy_to_stdout, va_list ap) {
+    std::string str;
+    android::base::StringAppendV(&str, fmt, ap);
+
+    if (copy_to_stdout) {
+        fputs(str.c_str(), stdout);
+    }
+
+    pthread_mutex_lock(&updateMutex);
+    if (text_rows_ > 0 && text_cols_ > 0) {
+        for (const char* ptr = str.c_str(); *ptr != '\0'; ++ptr) {
+            if (*ptr == '\n' || text_col_ >= text_cols_) {
+                text_[text_row_][text_col_] = '\0';
+                text_col_ = 0;
+                text_row_ = (text_row_ + 1) % text_rows_;
+                if (text_row_ == text_top_) text_top_ = (text_top_ + 1) % text_rows_;
+            }
+            if (*ptr != '\n') text_[text_row_][text_col_++] = *ptr;
+        }
+        text_[text_row_][text_col_] = '\0';
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::Print(const char* fmt, ...) {
+    va_list ap;
+    va_start(ap, fmt);
+    PrintV(fmt, true, ap);
+    va_end(ap);
+}
+
+void ScreenRecoveryUI::PrintOnScreenOnly(const char *fmt, ...) {
+    va_list ap;
+    va_start(ap, fmt);
+    PrintV(fmt, false, ap);
+    va_end(ap);
+}
+
+void ScreenRecoveryUI::PutChar(char ch) {
+    pthread_mutex_lock(&updateMutex);
+    if (ch != '\n') text_[text_row_][text_col_++] = ch;
+    if (ch == '\n' || text_col_ >= text_cols_) {
+        text_col_ = 0;
+        ++text_row_;
+
+        if (text_row_ == text_top_) text_top_ = (text_top_ + 1) % text_rows_;
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::ClearText() {
+    pthread_mutex_lock(&updateMutex);
+    text_col_ = 0;
+    text_row_ = 0;
+    text_top_ = 1;
+    for (size_t i = 0; i < text_rows_; ++i) {
+        memset(text_[i], 0, text_cols_ + 1);
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::ShowFile(FILE* fp) {
+    std::vector<off_t> offsets;
+    offsets.push_back(ftello(fp));
+    ClearText();
+
+    struct stat sb;
+    fstat(fileno(fp), &sb);
+
+    bool show_prompt = false;
+    while (true) {
+        if (show_prompt) {
+            PrintOnScreenOnly("--(%d%% of %d bytes)--",
+                  static_cast<int>(100 * (double(ftello(fp)) / double(sb.st_size))),
+                  static_cast<int>(sb.st_size));
+            Redraw();
+            while (show_prompt) {
+                show_prompt = false;
+                int key = WaitKey();
+                if (key == KEY_POWER || key == KEY_ENTER) {
+                    return;
+                } else if (key == KEY_UP || key == KEY_VOLUMEUP) {
+                    if (offsets.size() <= 1) {
+                        show_prompt = true;
+                    } else {
+                        offsets.pop_back();
+                        fseek(fp, offsets.back(), SEEK_SET);
+                    }
+                } else {
+                    if (feof(fp)) {
+                        return;
+                    }
+                    offsets.push_back(ftello(fp));
+                }
+            }
+            ClearText();
+        }
+
+        int ch = getc(fp);
+        if (ch == EOF) {
+            while (text_row_ < text_rows_ - 1) PutChar('\n');
+            show_prompt = true;
+        } else {
+            PutChar(ch);
+            if (text_col_ == 0 && text_row_ >= text_rows_ - 1) {
+                show_prompt = true;
+            }
+        }
+    }
+}
+
+void ScreenRecoveryUI::ShowFile(const char* filename) {
+    FILE* fp = fopen_path(filename, "re");
+    if (fp == nullptr) {
+        Print("  Unable to open %s: %s\n", filename, strerror(errno));
+        return;
+    }
+
+    char** old_text = text_;
+    size_t old_text_col = text_col_;
+    size_t old_text_row = text_row_;
+    size_t old_text_top = text_top_;
+
+    // Swap in the alternate screen and clear it.
+    text_ = file_viewer_text_;
+    ClearText();
+
+    ShowFile(fp);
+    fclose(fp);
+
+    text_ = old_text;
+    text_col_ = old_text_col;
+    text_row_ = old_text_row;
+    text_top_ = old_text_top;
+}
+
+void ScreenRecoveryUI::StartMenu(const char* const * headers, const char* const * items,
+                                 int initial_selection) {
+    pthread_mutex_lock(&updateMutex);
+    if (text_rows_ > 0 && text_cols_ > 0) {
+        menu_headers_ = headers;
+        size_t i = 0;
+        for (; i < text_rows_ && items[i] != nullptr; ++i) {
+            strncpy(menu_[i], items[i], text_cols_ - 1);
+            menu_[i][text_cols_ - 1] = '\0';
+        }
+        menu_items = i;
+        show_menu = true;
+        menu_sel = initial_selection;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+int ScreenRecoveryUI::SelectMenu(int sel) {
+    pthread_mutex_lock(&updateMutex);
+    if (show_menu) {
+        int old_sel = menu_sel;
+        menu_sel = sel;
+
+        // Wrap at top and bottom.
+        if (menu_sel < 0) menu_sel = menu_items - 1;
+        if (menu_sel >= menu_items) menu_sel = 0;
+
+        sel = menu_sel;
+        if (menu_sel != old_sel) update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+    return sel;
+}
+
+void ScreenRecoveryUI::EndMenu() {
+    pthread_mutex_lock(&updateMutex);
+    if (show_menu && text_rows_ > 0 && text_cols_ > 0) {
+        show_menu = false;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+bool ScreenRecoveryUI::IsTextVisible() {
+    pthread_mutex_lock(&updateMutex);
+    int visible = show_text;
+    pthread_mutex_unlock(&updateMutex);
+    return visible;
+}
+
+bool ScreenRecoveryUI::WasTextEverVisible() {
+    pthread_mutex_lock(&updateMutex);
+    int ever_visible = show_text_ever;
+    pthread_mutex_unlock(&updateMutex);
+    return ever_visible;
+}
+
+void ScreenRecoveryUI::ShowText(bool visible) {
+    pthread_mutex_lock(&updateMutex);
+    show_text = visible;
+    if (show_text) show_text_ever = true;
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::Redraw() {
+    pthread_mutex_lock(&updateMutex);
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::KeyLongPress(int) {
+    // Redraw so that if we're in the menu, the highlight
+    // will change color to indicate a successful long press.
+    Redraw();
+}
diff --git a/recovery/screen_ui.h b/recovery/screen_ui.h
new file mode 100644
index 0000000..08a5f44
--- /dev/null
+++ b/recovery/screen_ui.h
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#ifndef RECOVERY_SCREEN_UI_H
+#define RECOVERY_SCREEN_UI_H
+
+#include <pthread.h>
+#include <stdio.h>
+
+#include "ui.h"
+#include "minui/minui.h"
+
+// Implementation of RecoveryUI appropriate for devices with a screen
+// (shows an icon + a progress bar, text logging, menu, etc.)
+class ScreenRecoveryUI : public RecoveryUI {
+  public:
+    ScreenRecoveryUI();
+
+    void Init();
+    void SetLocale(const char* locale);
+
+    // overall recovery state ("background image")
+    void SetBackground(Icon icon);
+
+    // progress indicator
+    void SetProgressType(ProgressType type);
+    void ShowProgress(float portion, float seconds);
+    void SetProgress(float fraction);
+
+    void SetStage(int current, int max);
+
+    // text log
+    void ShowText(bool visible);
+    bool IsTextVisible();
+    bool WasTextEverVisible();
+
+    // printing messages
+    void Print(const char* fmt, ...) __printflike(2, 3);
+    void PrintOnScreenOnly(const char* fmt, ...) __printflike(2, 3);
+    void ShowFile(const char* filename);
+
+    // menu display
+    void StartMenu(const char* const * headers, const char* const * items,
+                   int initial_selection);
+    int SelectMenu(int sel);
+    void EndMenu();
+
+    void KeyLongPress(int);
+
+    void Redraw();
+
+    enum UIElement {
+        HEADER, MENU, MENU_SEL_BG, MENU_SEL_BG_ACTIVE, MENU_SEL_FG, LOG, TEXT_FILL, INFO
+    };
+    void SetColor(UIElement e);
+
+  private:
+    Icon currentIcon;
+    int installingFrame;
+    const char* locale;
+    bool rtl_locale;
+
+    pthread_mutex_t updateMutex;
+    GRSurface* backgroundIcon[5];
+    GRSurface* backgroundText[5];
+    GRSurface** installation;
+    GRSurface* progressBarEmpty;
+    GRSurface* progressBarFill;
+    GRSurface* stageMarkerEmpty;
+    GRSurface* stageMarkerFill;
+
+    ProgressType progressBarType;
+
+    float progressScopeStart, progressScopeSize, progress;
+    double progressScopeTime, progressScopeDuration;
+
+    // true when both graphics pages are the same (except for the progress bar).
+    bool pagesIdentical;
+
+    size_t text_cols_, text_rows_;
+
+    // Log text overlay, displayed when a magic key is pressed.
+    char** text_;
+    size_t text_col_, text_row_, text_top_;
+
+    bool show_text;
+    bool show_text_ever;   // has show_text ever been true?
+
+    char** menu_;
+    const char* const* menu_headers_;
+    bool show_menu;
+    int menu_items, menu_sel;
+
+    // An alternate text screen, swapped with 'text_' when we're viewing a log file.
+    char** file_viewer_text_;
+
+    pthread_t progress_thread_;
+
+    // The following two are parsed from the image file
+    // (e.g. '/res/images/icon_installing.png').
+    int animation_fps;
+    int installing_frames;
+
+    int iconX, iconY;
+
+    int stage, max_stage;
+
+    void draw_background_locked(Icon icon);
+    void draw_progress_locked();
+    void draw_screen_locked();
+    void update_screen_locked();
+    void update_progress_locked();
+
+    static void* ProgressThreadStartRoutine(void* data);
+    void ProgressThreadLoop();
+
+    void ShowFile(FILE*);
+    void PrintV(const char*, bool, va_list);
+    void PutChar(char);
+    void ClearText();
+
+    void DrawHorizontalRule(int* y);
+    void DrawTextLine(int* y, const char* line, bool bold);
+    void DrawTextLines(int* y, const char* const* lines);
+
+    void LoadBitmap(const char* filename, GRSurface** surface);
+    void LoadBitmapArray(const char* filename, int* frames, int* fps, GRSurface*** surface);
+    void LoadLocalizedBitmap(const char* filename, GRSurface** surface);
+};
+
+#endif  // RECOVERY_UI_H
diff --git a/recovery/tests/Android.mk b/recovery/tests/Android.mk
new file mode 100644
index 0000000..a683395
--- /dev/null
+++ b/recovery/tests/Android.mk
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2014 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+# Unit tests
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_MODULE := recovery_unit_test
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+LOCAL_STATIC_LIBRARIES := libverifier
+LOCAL_SRC_FILES := unit/asn1_decoder_test.cpp
+LOCAL_SRC_FILES += unit/recovery_test.cpp
+LOCAL_C_INCLUDES := bootable/recovery
+LOCAL_SHARED_LIBRARIES := liblog
+include $(BUILD_NATIVE_TEST)
+
+# Component tests
+include $(CLEAR_VARS)
+LOCAL_CLANG := true
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+LOCAL_MODULE := recovery_component_test
+LOCAL_C_INCLUDES := bootable/recovery
+LOCAL_SRC_FILES := component/verifier_test.cpp
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+LOCAL_STATIC_LIBRARIES := \
+    libbase \
+    libverifier \
+    libcrypto_utils \
+    libcrypto \
+    libminui \
+    libminzip \
+    libcutils \
+    libc
+
+testdata_out_path := $(TARGET_OUT_DATA_NATIVE_TESTS)/recovery
+testdata_files := $(call find-subdir-files, testdata/*)
+
+GEN := $(addprefix $(testdata_out_path)/, $(testdata_files))
+$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
+$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
+$(GEN): $(testdata_out_path)/% : $(LOCAL_PATH)/%
+	$(transform-generated-source)
+LOCAL_GENERATED_SOURCES += $(GEN)
+include $(BUILD_NATIVE_TEST)
diff --git a/recovery/tests/component/verifier_test.cpp b/recovery/tests/component/verifier_test.cpp
new file mode 100644
index 0000000..2a78173
--- /dev/null
+++ b/recovery/tests/component/verifier_test.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2009 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 agree 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <openssl/sha.h>
+
+#include <android-base/stringprintf.h>
+
+#include "common.h"
+#include "minzip/SysUtil.h"
+#include "ui.h"
+#include "verifier.h"
+
+#if defined(__LP64__)
+#define NATIVE_TEST_PATH "/nativetest64"
+#else
+#define NATIVE_TEST_PATH "/nativetest"
+#endif
+
+static const char* DATA_PATH = getenv("ANDROID_DATA");
+static const char* TESTDATA_PATH = "/recovery/testdata/";
+
+RecoveryUI* ui = NULL;
+
+class MockUI : public RecoveryUI {
+    void Init() { }
+    void SetStage(int, int) { }
+    void SetLocale(const char*) { }
+    void SetBackground(Icon /*icon*/) { }
+
+    void SetProgressType(ProgressType /*determinate*/) { }
+    void ShowProgress(float /*portion*/, float /*seconds*/) { }
+    void SetProgress(float /*fraction*/) { }
+
+    void ShowText(bool /*visible*/) { }
+    bool IsTextVisible() { return false; }
+    bool WasTextEverVisible() { return false; }
+    void Print(const char* fmt, ...) {
+        va_list ap;
+        va_start(ap, fmt);
+        vfprintf(stderr, fmt, ap);
+        va_end(ap);
+    }
+    void PrintOnScreenOnly(const char* fmt, ...) {
+        va_list ap;
+        va_start(ap, fmt);
+        vfprintf(stderr, fmt, ap);
+        va_end(ap);
+    }
+    void ShowFile(const char*) { }
+
+    void StartMenu(const char* const* /*headers*/,
+                   const char* const* /*items*/,
+                   int /*initial_selection*/) { }
+    int SelectMenu(int /*sel*/) { return 0; }
+    void EndMenu() { }
+};
+
+void
+ui_print(const char* format, ...) {
+    va_list ap;
+    va_start(ap, format);
+    vfprintf(stdout, format, ap);
+    va_end(ap);
+}
+
+class VerifierTest : public testing::TestWithParam<std::vector<std::string>> {
+  public:
+    MemMapping memmap;
+    std::vector<Certificate> certs;
+
+    virtual void SetUp() {
+        std::vector<std::string> args = GetParam();
+        std::string package =
+            android::base::StringPrintf("%s%s%s%s", DATA_PATH, NATIVE_TEST_PATH,
+                                        TESTDATA_PATH, args[0].c_str());
+        if (sysMapFile(package.c_str(), &memmap) != 0) {
+            FAIL() << "Failed to mmap " << package << ": " << strerror(errno) << "\n";
+        }
+
+        for (auto it = ++(args.cbegin()); it != args.cend(); ++it) {
+            std::string public_key_file = android::base::StringPrintf(
+                    "%s%s%stestkey_%s.txt", DATA_PATH, NATIVE_TEST_PATH,
+                    TESTDATA_PATH, it->c_str());
+            ASSERT_TRUE(load_keys(public_key_file.c_str(), certs));
+        }
+    }
+
+    static void SetUpTestCase() {
+        ui = new MockUI();
+    }
+};
+
+class VerifierSuccessTest : public VerifierTest {
+};
+
+class VerifierFailureTest : public VerifierTest {
+};
+
+TEST_P(VerifierSuccessTest, VerifySucceed) {
+    ASSERT_EQ(verify_file(memmap.addr, memmap.length, certs), VERIFY_SUCCESS);
+}
+
+TEST_P(VerifierFailureTest, VerifyFailure) {
+    ASSERT_EQ(verify_file(memmap.addr, memmap.length, certs), VERIFY_FAILURE);
+}
+
+INSTANTIATE_TEST_CASE_P(SingleKeySuccess, VerifierSuccessTest,
+        ::testing::Values(
+            std::vector<std::string>({"otasigned_v1.zip", "v1"}),
+            std::vector<std::string>({"otasigned_v2.zip", "v2"}),
+            std::vector<std::string>({"otasigned_v3.zip", "v3"}),
+            std::vector<std::string>({"otasigned_v4.zip", "v4"}),
+            std::vector<std::string>({"otasigned_v5.zip", "v5"})));
+
+INSTANTIATE_TEST_CASE_P(MultiKeySuccess, VerifierSuccessTest,
+        ::testing::Values(
+            std::vector<std::string>({"otasigned_v1.zip", "v1", "v2"}),
+            std::vector<std::string>({"otasigned_v2.zip", "v5", "v2"}),
+            std::vector<std::string>({"otasigned_v3.zip", "v5", "v1", "v3"}),
+            std::vector<std::string>({"otasigned_v4.zip", "v5", "v1", "v4"}),
+            std::vector<std::string>({"otasigned_v5.zip", "v4", "v1", "v5"})));
+
+INSTANTIATE_TEST_CASE_P(WrongKey, VerifierFailureTest,
+        ::testing::Values(
+            std::vector<std::string>({"otasigned_v1.zip", "v2"}),
+            std::vector<std::string>({"otasigned_v2.zip", "v1"}),
+            std::vector<std::string>({"otasigned_v3.zip", "v5"}),
+            std::vector<std::string>({"otasigned_v4.zip", "v5"}),
+            std::vector<std::string>({"otasigned_v5.zip", "v3"})));
+
+INSTANTIATE_TEST_CASE_P(WrongHash, VerifierFailureTest,
+        ::testing::Values(
+            std::vector<std::string>({"otasigned_v1.zip", "v3"}),
+            std::vector<std::string>({"otasigned_v2.zip", "v4"}),
+            std::vector<std::string>({"otasigned_v3.zip", "v1"}),
+            std::vector<std::string>({"otasigned_v4.zip", "v2"})));
+
+INSTANTIATE_TEST_CASE_P(BadPackage, VerifierFailureTest,
+        ::testing::Values(
+            std::vector<std::string>({"random.zip", "v1"}),
+            std::vector<std::string>({"fake-eocd.zip", "v1"}),
+            std::vector<std::string>({"alter-metadata.zip", "v1"}),
+            std::vector<std::string>({"alter-footer.zip", "v1"})));
diff --git a/recovery/tests/testdata/alter-footer.zip b/recovery/tests/testdata/alter-footer.zip
new file mode 100644
index 0000000..f497ec0
--- /dev/null
+++ b/recovery/tests/testdata/alter-footer.zip
Binary files differ
diff --git a/recovery/tests/testdata/alter-metadata.zip b/recovery/tests/testdata/alter-metadata.zip
new file mode 100644
index 0000000..1c71fbc
--- /dev/null
+++ b/recovery/tests/testdata/alter-metadata.zip
Binary files differ
diff --git a/recovery/tests/testdata/fake-eocd.zip b/recovery/tests/testdata/fake-eocd.zip
new file mode 100644
index 0000000..15dc0a9
--- /dev/null
+++ b/recovery/tests/testdata/fake-eocd.zip
Binary files differ
diff --git a/recovery/tests/testdata/jarsigned.zip b/recovery/tests/testdata/jarsigned.zip
new file mode 100644
index 0000000..8b1ef8b
--- /dev/null
+++ b/recovery/tests/testdata/jarsigned.zip
Binary files differ
diff --git a/recovery/tests/testdata/otasigned_v1.zip b/recovery/tests/testdata/otasigned_v1.zip
new file mode 100644
index 0000000..a6bc53e
--- /dev/null
+++ b/recovery/tests/testdata/otasigned_v1.zip
Binary files differ
diff --git a/recovery/tests/testdata/otasigned_v2.zip b/recovery/tests/testdata/otasigned_v2.zip
new file mode 100644
index 0000000..dd1e4dd
--- /dev/null
+++ b/recovery/tests/testdata/otasigned_v2.zip
Binary files differ
diff --git a/recovery/tests/testdata/otasigned_v3.zip b/recovery/tests/testdata/otasigned_v3.zip
new file mode 100644
index 0000000..0ed4409
--- /dev/null
+++ b/recovery/tests/testdata/otasigned_v3.zip
Binary files differ
diff --git a/recovery/tests/testdata/otasigned_v4.zip b/recovery/tests/testdata/otasigned_v4.zip
new file mode 100644
index 0000000..3af408c
--- /dev/null
+++ b/recovery/tests/testdata/otasigned_v4.zip
Binary files differ
diff --git a/recovery/tests/testdata/otasigned_v5.zip b/recovery/tests/testdata/otasigned_v5.zip
new file mode 100644
index 0000000..999fcdd
--- /dev/null
+++ b/recovery/tests/testdata/otasigned_v5.zip
Binary files differ
diff --git a/recovery/tests/testdata/random.zip b/recovery/tests/testdata/random.zip
new file mode 100644
index 0000000..18c0b3b
--- /dev/null
+++ b/recovery/tests/testdata/random.zip
Binary files differ
diff --git a/recovery/tests/testdata/testkey_v1.pk8 b/recovery/tests/testdata/testkey_v1.pk8
new file mode 100644
index 0000000..586c1bd
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v1.pk8
Binary files differ
diff --git a/recovery/tests/testdata/testkey_v1.txt b/recovery/tests/testdata/testkey_v1.txt
new file mode 100644
index 0000000..53f5297
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v1.txt
@@ -0,0 +1 @@
+{64,0xc926ad21,{1795090719,2141396315,950055447,2581568430,4268923165,1920809988,546586521,3498997798,1776797858,3740060814,1805317999,1429410244,129622599,1422441418,1783893377,1222374759,2563319927,323993566,28517732,609753416,1826472888,215237850,4261642700,4049082591,3228462402,774857746,154822455,2497198897,2758199418,3019015328,2794777644,87251430,2534927978,120774784,571297800,3695899472,2479925187,3811625450,3401832990,2394869647,3267246207,950095497,555058928,414729973,1136544882,3044590084,465547824,4058146728,2731796054,1689838846,3890756939,1048029507,895090649,247140249,178744550,3547885223,3165179243,109881576,3944604415,1044303212,3772373029,2985150306,3737520932,3599964420},{3437017481,3784475129,2800224972,3086222688,251333580,2131931323,512774938,325948880,2657486437,2102694287,3820568226,792812816,1026422502,2053275343,2800889200,3113586810,165549746,4273519969,4065247892,1902789247,772932719,3941848426,3652744109,216871947,3164400649,1942378755,3996765851,1055777370,964047799,629391717,2232744317,3910558992,191868569,2758883837,3682816752,2997714732,2702529250,3570700455,3776873832,3924067546,3555689545,2758825434,1323144535,61311905,1997411085,376844204,213777604,4077323584,9135381,1625809335,2804742137,2952293945,1117190829,4237312782,1825108855,3013147971,1111251351,2568837572,1684324211,2520978805,367251975,810756730,2353784344,1175080310}}
diff --git a/recovery/tests/testdata/testkey_v1.x509.pem b/recovery/tests/testdata/testkey_v1.x509.pem
new file mode 100644
index 0000000..e242d83
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v1.x509.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
+qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
+wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
+4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
+RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
+zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
+HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
+AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
+EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa
+J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y
+LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe
++ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX
+31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr
+sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0=
+-----END CERTIFICATE-----
diff --git a/recovery/tests/testdata/testkey_v2.pk8 b/recovery/tests/testdata/testkey_v2.pk8
new file mode 100644
index 0000000..3052613
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v2.pk8
Binary files differ
diff --git a/recovery/tests/testdata/testkey_v2.txt b/recovery/tests/testdata/testkey_v2.txt
new file mode 100644
index 0000000..54ddbba
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v2.txt
@@ -0,0 +1 @@
+v2 {64,0xc9bd1f21,{293133087,3210546773,865313125,250921607,3158780490,943703457,1242806226,2986289859,2942743769,2457906415,2719374299,1783459420,149579627,3081531591,3440738617,2788543742,2758457512,1146764939,3699497403,2446203424,1744968926,1159130537,2370028300,3978231572,3392699980,1487782451,1180150567,2841334302,3753960204,961373345,3333628321,748825784,2978557276,1566596926,1613056060,2600292737,1847226629,50398611,1890374404,2878700735,2286201787,1401186359,619285059,731930817,2340993166,1156490245,2992241729,151498140,318782170,3480838990,2100383433,4223552555,3628927011,4247846280,1759029513,4215632601,2719154626,3490334597,1751299340,3487864726,3668753795,4217506054,3748782284,3150295088},{1772626313,445326068,3477676155,1758201194,2986784722,491035581,3922936562,702212696,2979856666,3324974564,2488428922,3056318590,1626954946,664714029,398585816,3964097931,3356701905,2298377729,2040082097,3025491477,539143308,3348777868,2995302452,3602465520,212480763,2691021393,1307177300,704008044,2031136606,1054106474,3838318865,2441343869,1477566916,700949900,2534790355,3353533667,336163563,4106790558,2701448228,1571536379,1103842411,3623110423,1635278839,1577828979,910322800,715583630,138128831,1017877531,2289162787,447994798,1897243165,4121561445,4150719842,2131821093,2262395396,3305771534,980753571,3256525190,3128121808,1072869975,3507939515,4229109952,118381341,2209831334}}
diff --git a/recovery/tests/testdata/testkey_v2.x509.pem b/recovery/tests/testdata/testkey_v2.x509.pem
new file mode 100644
index 0000000..814abcf
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v2.x509.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIJAKhkCO1dDYMaMA0GCSqGSIb3DQEBBQUAMG8xCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMT
+B1Rlc3QxMjMwHhcNMTIwNzI1MTg1NzAzWhcNMzkxMjExMTg1NzAzWjBvMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQD
+EwdUZXN0MTIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu8WwMN9x
+4Mz7YgkG2qy9g8/kl5ZoYrUM0ApHhaITAcL7RXLZaNipCf0w/YjYTQgj+75MK30x
+TsnPeWNOEwA62gkHrZyyWfxBRO6kBYuIuI4roGDBJOmKQ1OEaDeIRKu7q5V8v3Cs
+0wQDAQWTbhpxBZr9UYFgJUg8XWBfPrGJLVwsoiy4xrMhoTlNZKHfwOMMqVtSHkZX
+qydYrcIzyjh+TO0e/xSNQ8MMRRbtqWgCHN6Rzhog3IHZu0RaPoukariopjXM/s0V
+gTm3rHDHCOpna2pNblyiFlvbkoCs769mtNmx/yrDShO30jg/xaG8RypKDvTChzOT
+oWW/XQ5VEXjbHwIDAQABo4HUMIHRMB0GA1UdDgQWBBRlT2dEZJY1tmUM8mZ0xnhS
+GdD9TTCBoQYDVR0jBIGZMIGWgBRlT2dEZJY1tmUM8mZ0xnhSGdD9TaFzpHEwbzEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
+YWluIFZpZXcxDzANBgNVBAoTBkdvb2dsZTEQMA4GA1UECxMHQW5kcm9pZDEQMA4G
+A1UEAxMHVGVzdDEyM4IJAKhkCO1dDYMaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
+AQEFBQADggEBAHqnXHtE+h3hvGmHh24GT51vGAYLc68WUUtCVlMIU85zQ757wlxZ
+BmRypZ1i9hSqnXj5n+mETV5rFX3g2gvdAPVHkRycuDa2aUdZSE8cW4Z6qYFx6SaD
+e+3SyXokpUquW64RuHJrf/yd/FnGjneBe3Qpm2reuzGWNH90qZGdbsfNaCm5kx2L
+X+ZNHM3CcGMLaphY5++sM0JxSEcju5EK33ZYgLf4YdlbyMp8LDFVNd7ff0SFi9fF
+0ZlAsJWoS3QmVCj2744BFdsCu7UHpnYpG6X3MT4SHAawdOaT5zSuaCl2xx6H0O7t
+w/Fvbl/KVD1ZmLHgBKjDMNSh0OB9mSsDWpw=
+-----END CERTIFICATE-----
diff --git a/recovery/tests/testdata/testkey_v3.pk8 b/recovery/tests/testdata/testkey_v3.pk8
new file mode 120000
index 0000000..18ecf98
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v3.pk8
@@ -0,0 +1 @@
+testkey_v1.pk8
\ No newline at end of file
diff --git a/recovery/tests/testdata/testkey_v3.txt b/recovery/tests/testdata/testkey_v3.txt
new file mode 100644
index 0000000..3208571
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v3.txt
@@ -0,0 +1 @@
+v3 {64,0xc926ad21,{1795090719,2141396315,950055447,2581568430,4268923165,1920809988,546586521,3498997798,1776797858,3740060814,1805317999,1429410244,129622599,1422441418,1783893377,1222374759,2563319927,323993566,28517732,609753416,1826472888,215237850,4261642700,4049082591,3228462402,774857746,154822455,2497198897,2758199418,3019015328,2794777644,87251430,2534927978,120774784,571297800,3695899472,2479925187,3811625450,3401832990,2394869647,3267246207,950095497,555058928,414729973,1136544882,3044590084,465547824,4058146728,2731796054,1689838846,3890756939,1048029507,895090649,247140249,178744550,3547885223,3165179243,109881576,3944604415,1044303212,3772373029,2985150306,3737520932,3599964420},{3437017481,3784475129,2800224972,3086222688,251333580,2131931323,512774938,325948880,2657486437,2102694287,3820568226,792812816,1026422502,2053275343,2800889200,3113586810,165549746,4273519969,4065247892,1902789247,772932719,3941848426,3652744109,216871947,3164400649,1942378755,3996765851,1055777370,964047799,629391717,2232744317,3910558992,191868569,2758883837,3682816752,2997714732,2702529250,3570700455,3776873832,3924067546,3555689545,2758825434,1323144535,61311905,1997411085,376844204,213777604,4077323584,9135381,1625809335,2804742137,2952293945,1117190829,4237312782,1825108855,3013147971,1111251351,2568837572,1684324211,2520978805,367251975,810756730,2353784344,1175080310}}
diff --git a/recovery/tests/testdata/testkey_v3.x509.pem b/recovery/tests/testdata/testkey_v3.x509.pem
new file mode 100644
index 0000000..002ce89
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v3.x509.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+Fw0xMzA0MTAxODA1MzZaFw0xMzA1MTAxODA1MzZaMIGUMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
+qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
+wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
+4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
+RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
+zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
+HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
+AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
+EwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKRVj9hOaozH1W8Wb4CNj7sCWixh
+UMMZJXkxUtvUVHZGefp6MdtYiD/ZM7YRwZphm9aNhkykbHJdZ3lPzeL2csCa+sDQ
+8sIzGu0/aD6p4zgIKQZmz0mZHqPGbHoLWOmA9EexRCFZ7vO/kO56ZbyhfFz2DI3S
+Yez65CabErOFhNX6WukSPbV3zfsHRDD5JUStb/ko6t99HXsvIO0Ax9poj60PpCC1
+SiFzHZUY9mOnUfJFs+3NWCwKtP9nho3mZ3pJ1i+SeF6JiqbE3KHl4CDBeVGcu3CK
+fiUZ8e8iXVN471Cgc5GD6Ud1pS7ifNZJsKhbETQ63KmvHCLRPi4NmP67uDE=
+-----END CERTIFICATE-----
diff --git a/recovery/tests/testdata/testkey_v4.pk8 b/recovery/tests/testdata/testkey_v4.pk8
new file mode 120000
index 0000000..683b9a3
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v4.pk8
@@ -0,0 +1 @@
+testkey_v2.pk8
\ No newline at end of file
diff --git a/recovery/tests/testdata/testkey_v4.txt b/recovery/tests/testdata/testkey_v4.txt
new file mode 100644
index 0000000..532cbd5
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v4.txt
@@ -0,0 +1 @@
+v4 {64,0xc9bd1f21,{293133087,3210546773,865313125,250921607,3158780490,943703457,1242806226,2986289859,2942743769,2457906415,2719374299,1783459420,149579627,3081531591,3440738617,2788543742,2758457512,1146764939,3699497403,2446203424,1744968926,1159130537,2370028300,3978231572,3392699980,1487782451,1180150567,2841334302,3753960204,961373345,3333628321,748825784,2978557276,1566596926,1613056060,2600292737,1847226629,50398611,1890374404,2878700735,2286201787,1401186359,619285059,731930817,2340993166,1156490245,2992241729,151498140,318782170,3480838990,2100383433,4223552555,3628927011,4247846280,1759029513,4215632601,2719154626,3490334597,1751299340,3487864726,3668753795,4217506054,3748782284,3150295088},{1772626313,445326068,3477676155,1758201194,2986784722,491035581,3922936562,702212696,2979856666,3324974564,2488428922,3056318590,1626954946,664714029,398585816,3964097931,3356701905,2298377729,2040082097,3025491477,539143308,3348777868,2995302452,3602465520,212480763,2691021393,1307177300,704008044,2031136606,1054106474,3838318865,2441343869,1477566916,700949900,2534790355,3353533667,336163563,4106790558,2701448228,1571536379,1103842411,3623110423,1635278839,1577828979,910322800,715583630,138128831,1017877531,2289162787,447994798,1897243165,4121561445,4150719842,2131821093,2262395396,3305771534,980753571,3256525190,3128121808,1072869975,3507939515,4229109952,118381341,2209831334}}
diff --git a/recovery/tests/testdata/testkey_v4.x509.pem b/recovery/tests/testdata/testkey_v4.x509.pem
new file mode 100644
index 0000000..9d5376b
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v4.x509.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIJAKhkCO1dDYMaMA0GCSqGSIb3DQEBCwUAMG8xCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMT
+B1Rlc3QxMjMwHhcNMTMwNDEwMTcyMzUyWhcNMTMwNTEwMTcyMzUyWjBvMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQD
+EwdUZXN0MTIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu8WwMN9x
+4Mz7YgkG2qy9g8/kl5ZoYrUM0ApHhaITAcL7RXLZaNipCf0w/YjYTQgj+75MK30x
+TsnPeWNOEwA62gkHrZyyWfxBRO6kBYuIuI4roGDBJOmKQ1OEaDeIRKu7q5V8v3Cs
+0wQDAQWTbhpxBZr9UYFgJUg8XWBfPrGJLVwsoiy4xrMhoTlNZKHfwOMMqVtSHkZX
+qydYrcIzyjh+TO0e/xSNQ8MMRRbtqWgCHN6Rzhog3IHZu0RaPoukariopjXM/s0V
+gTm3rHDHCOpna2pNblyiFlvbkoCs769mtNmx/yrDShO30jg/xaG8RypKDvTChzOT
+oWW/XQ5VEXjbHwIDAQABo4HUMIHRMB0GA1UdDgQWBBRlT2dEZJY1tmUM8mZ0xnhS
+GdD9TTCBoQYDVR0jBIGZMIGWgBRlT2dEZJY1tmUM8mZ0xnhSGdD9TaFzpHEwbzEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
+YWluIFZpZXcxDzANBgNVBAoTBkdvb2dsZTEQMA4GA1UECxMHQW5kcm9pZDEQMA4G
+A1UEAxMHVGVzdDEyM4IJAKhkCO1dDYMaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
+AQELBQADggEBAKWWQ9S0V9wWjrMJe8exj1gklwD1Ysi0vi+h2tfixahelrpsNkWi
+EFjoUSHEkW9ThLmtui646uAlwSiWtSn1XkGGmIJ3s+gmAFUcMc0CaK0dgoq/M9zn
+fQ0Vkzc1tK4MLsf+CbPDywPycb6+T3dBkerbWn9GUpjGl1ANWlciXZZ3657m61sL
+HhwUOBxbZZ6sYP4ed2SVCf45GgMyJ0VoUg5yI2JzPAgOkGfeEIPVXE1M94edJY4G
+8eHYvXovJZwXvKFI+ZyS0KBPx8cpfw89RB9qmkxqNBIm8qWb3qBiuBEIPj+NF/7w
+sC/Fv8NNXkVquy0xa0qdyJBABzWE18zGcXs=
+-----END CERTIFICATE-----
diff --git a/recovery/tests/testdata/testkey_v5.pk8 b/recovery/tests/testdata/testkey_v5.pk8
new file mode 100644
index 0000000..9a521c8
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v5.pk8
Binary files differ
diff --git a/recovery/tests/testdata/testkey_v5.txt b/recovery/tests/testdata/testkey_v5.txt
new file mode 100644
index 0000000..72b4395
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v5.txt
@@ -0,0 +1 @@
+v5 {32,{36,250,86,214,202,22,20,147,198,120,2,28,76,190,78,23,106,35,24,96,86,22,186,69,132,93,192,232,0,213,14,103},{222,154,23,13,125,130,22,76,146,185,140,159,138,255,105,143,32,16,27,72,175,145,141,121,233,184,77,24,217,141,132,181}}
diff --git a/recovery/tests/testdata/testkey_v5.x509.pem b/recovery/tests/testdata/testkey_v5.x509.pem
new file mode 100644
index 0000000..b122836
--- /dev/null
+++ b/recovery/tests/testdata/testkey_v5.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBezCCASACCQC4g5wurPSmtzAKBggqhkjOPQQDAjBFMQswCQYDVQQGEwJBVTET
+MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMB4XDTEzMTAwODIxMTAxM1oXDTE0MTAwODIxMTAxM1owRTELMAkGA1UE
+BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
+ZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGcO1QDowF2E
+RboWVmAYI2oXTr5MHAJ4xpMUFsrWVvoktYSN2RhNuOl5jZGvSBsQII9p/4qfjLmS
+TBaCfQ0Xmt4wCgYIKoZIzj0EAwIDSQAwRgIhAIJjWmZAwngc2VcHUhYp2oSLoCQ+
+P+7AtbAn5242AqfOAiEAghO0t6jTKs0LUhLJrQwbOkHyZMVdZaG2vcwV9y9H5Qc=
+-----END CERTIFICATE-----
diff --git a/recovery/tests/testdata/unsigned.zip b/recovery/tests/testdata/unsigned.zip
new file mode 100644
index 0000000..24e3ead
--- /dev/null
+++ b/recovery/tests/testdata/unsigned.zip
Binary files differ
diff --git a/recovery/tests/unit/asn1_decoder_test.cpp b/recovery/tests/unit/asn1_decoder_test.cpp
new file mode 100644
index 0000000..af96d87
--- /dev/null
+++ b/recovery/tests/unit/asn1_decoder_test.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#define LOG_TAG "asn1_decoder_test"
+
+#include <cutils/log.h>
+#include <gtest/gtest.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include "asn1_decoder.h"
+
+namespace android {
+
+class Asn1DecoderTest : public testing::Test {
+};
+
+TEST_F(Asn1DecoderTest, Empty_Failure) {
+    uint8_t empty[] = { };
+    asn1_context_t* ctx = asn1_context_new(empty, sizeof(empty));
+
+    EXPECT_EQ(NULL, asn1_constructed_get(ctx));
+    EXPECT_FALSE(asn1_constructed_skip_all(ctx));
+    EXPECT_EQ(0, asn1_constructed_type(ctx));
+    EXPECT_EQ(NULL, asn1_sequence_get(ctx));
+    EXPECT_EQ(NULL, asn1_set_get(ctx));
+    EXPECT_FALSE(asn1_sequence_next(ctx));
+
+    uint8_t* junk;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ctx, &junk, &length));
+    EXPECT_FALSE(asn1_octet_string_get(ctx, &junk, &length));
+
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedGet_TruncatedLength_Failure) {
+    uint8_t truncated[] = { 0xA0, 0x82, };
+    asn1_context_t* ctx = asn1_context_new(truncated, sizeof(truncated));
+    EXPECT_EQ(NULL, asn1_constructed_get(ctx));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedGet_LengthTooBig_Failure) {
+    uint8_t truncated[] = { 0xA0, 0x8a, 0xA5, 0x5A, 0xA5, 0x5A,
+                            0xA5, 0x5A, 0xA5, 0x5A, 0xA5, 0x5A, };
+    asn1_context_t* ctx = asn1_context_new(truncated, sizeof(truncated));
+    EXPECT_EQ(NULL, asn1_constructed_get(ctx));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedGet_TooSmallForChild_Failure) {
+    uint8_t data[] = { 0xA5, 0x02, 0x06, 0x01, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_constructed_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    EXPECT_EQ(5, asn1_constructed_type(ptr));
+    uint8_t* oid;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ptr, &oid, &length));
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedGet_Success) {
+    uint8_t data[] = { 0xA5, 0x03, 0x06, 0x01, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_constructed_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    EXPECT_EQ(5, asn1_constructed_type(ptr));
+    uint8_t* oid;
+    size_t length;
+    ASSERT_TRUE(asn1_oid_get(ptr, &oid, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0x01U, *oid);
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedSkipAll_TruncatedLength_Failure) {
+    uint8_t truncated[] = { 0xA2, 0x82, };
+    asn1_context_t* ctx = asn1_context_new(truncated, sizeof(truncated));
+    EXPECT_FALSE(asn1_constructed_skip_all(ctx));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, ConstructedSkipAll_Success) {
+    uint8_t data[] = { 0xA0, 0x03, 0x02, 0x01, 0x01,
+                            0xA1, 0x03, 0x02, 0x01, 0x01,
+                            0x06, 0x01, 0xA5, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    ASSERT_TRUE(asn1_constructed_skip_all(ctx));
+    uint8_t* oid;
+    size_t length;
+    ASSERT_TRUE(asn1_oid_get(ctx, &oid, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0xA5U, *oid);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SequenceGet_TruncatedLength_Failure) {
+    uint8_t truncated[] = { 0x30, 0x82, };
+    asn1_context_t* ctx = asn1_context_new(truncated, sizeof(truncated));
+    EXPECT_EQ(NULL, asn1_sequence_get(ctx));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SequenceGet_TooSmallForChild_Failure) {
+    uint8_t data[] = { 0x30, 0x02, 0x06, 0x01, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_sequence_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    uint8_t* oid;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ptr, &oid, &length));
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SequenceGet_Success) {
+    uint8_t data[] = { 0x30, 0x03, 0x06, 0x01, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_sequence_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    uint8_t* oid;
+    size_t length;
+    ASSERT_TRUE(asn1_oid_get(ptr, &oid, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0x01U, *oid);
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SetGet_TruncatedLength_Failure) {
+    uint8_t truncated[] = { 0x31, 0x82, };
+    asn1_context_t* ctx = asn1_context_new(truncated, sizeof(truncated));
+    EXPECT_EQ(NULL, asn1_set_get(ctx));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SetGet_TooSmallForChild_Failure) {
+    uint8_t data[] = { 0x31, 0x02, 0x06, 0x01, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_set_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    uint8_t* oid;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ptr, &oid, &length));
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, SetGet_Success) {
+    uint8_t data[] = { 0x31, 0x03, 0x06, 0x01, 0xBA, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    asn1_context_t* ptr = asn1_set_get(ctx);
+    ASSERT_NE((asn1_context_t*)NULL, ptr);
+    uint8_t* oid;
+    size_t length;
+    ASSERT_TRUE(asn1_oid_get(ptr, &oid, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0xBAU, *oid);
+    asn1_context_free(ptr);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OidGet_LengthZero_Failure) {
+    uint8_t data[] = { 0x06, 0x00, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* oid;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ctx, &oid, &length));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OidGet_TooSmall_Failure) {
+    uint8_t data[] = { 0x06, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* oid;
+    size_t length;
+    EXPECT_FALSE(asn1_oid_get(ctx, &oid, &length));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OidGet_Success) {
+    uint8_t data[] = { 0x06, 0x01, 0x99, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* oid;
+    size_t length;
+    ASSERT_TRUE(asn1_oid_get(ctx, &oid, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0x99U, *oid);
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OctetStringGet_LengthZero_Failure) {
+    uint8_t data[] = { 0x04, 0x00, 0x55, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* string;
+    size_t length;
+    ASSERT_FALSE(asn1_octet_string_get(ctx, &string, &length));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OctetStringGet_TooSmall_Failure) {
+    uint8_t data[] = { 0x04, 0x01, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* string;
+    size_t length;
+    ASSERT_FALSE(asn1_octet_string_get(ctx, &string, &length));
+    asn1_context_free(ctx);
+}
+
+TEST_F(Asn1DecoderTest, OctetStringGet_Success) {
+    uint8_t data[] = { 0x04, 0x01, 0xAA, };
+    asn1_context_t* ctx = asn1_context_new(data, sizeof(data));
+    uint8_t* string;
+    size_t length;
+    ASSERT_TRUE(asn1_octet_string_get(ctx, &string, &length));
+    EXPECT_EQ(1U, length);
+    EXPECT_EQ(0xAAU, *string);
+    asn1_context_free(ctx);
+}
+
+} // namespace android
diff --git a/recovery/tests/unit/recovery_test.cpp b/recovery/tests/unit/recovery_test.cpp
new file mode 100644
index 0000000..f397f25
--- /dev/null
+++ b/recovery/tests/unit/recovery_test.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#include <fcntl.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <android/log.h>
+#include <gtest/gtest.h>
+#include <log/logger.h>
+#include <private/android_logger.h>
+
+static const char myFilename[] = "/data/misc/recovery/inject.txt";
+static const char myContent[] = "Hello World\nWelcome to my recovery\n";
+
+// Failure is expected on systems that do not deliver either the
+// recovery-persist or recovery-refresh executables. Tests also require
+// a reboot sequence of test to truly verify.
+
+static ssize_t __pmsg_fn(log_id_t logId, char prio, const char *filename,
+                         const char *buf, size_t len, void *arg) {
+    EXPECT_EQ(LOG_ID_SYSTEM, logId);
+    EXPECT_EQ(ANDROID_LOG_INFO, prio);
+    EXPECT_EQ(0, NULL == strstr(myFilename,filename));
+    EXPECT_EQ(0, strcmp(myContent, buf));
+    EXPECT_EQ(sizeof(myContent), len);
+    EXPECT_EQ(0, NULL != arg);
+    return len;
+}
+
+// recovery.refresh - May fail. Requires recovery.inject, two reboots,
+//                    then expect success after second reboot.
+TEST(recovery, refresh) {
+    EXPECT_EQ(0, access("/system/bin/recovery-refresh", F_OK));
+
+    ssize_t ret = __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, NULL);
+    if (ret == -ENOENT) {
+        EXPECT_LT(0, __android_log_pmsg_file_write(
+            LOG_ID_SYSTEM, ANDROID_LOG_INFO,
+            myFilename, myContent, sizeof(myContent)));
+        fprintf(stderr, "injected test data, "
+                        "requires two intervening reboots "
+                        "to check for replication\n");
+    }
+    EXPECT_EQ((ssize_t)sizeof(myContent), ret);
+}
+
+// recovery.persist - Requires recovery.inject, then a reboot, then
+//                    expect success after for this test on that boot.
+TEST(recovery, persist) {
+    EXPECT_EQ(0, access("/system/bin/recovery-persist", F_OK));
+
+    ssize_t ret = __android_log_pmsg_file_read(
+        LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, NULL);
+    if (ret == -ENOENT) {
+        EXPECT_LT(0, __android_log_pmsg_file_write(
+            LOG_ID_SYSTEM, ANDROID_LOG_INFO,
+            myFilename, myContent, sizeof(myContent)));
+        fprintf(stderr, "injected test data, "
+                        "requires intervening reboot "
+                        "to check for storage\n");
+    }
+
+    int fd = open(myFilename, O_RDONLY);
+    EXPECT_LE(0, fd);
+
+    char buf[sizeof(myContent) + 32];
+    ret = read(fd, buf, sizeof(buf));
+    close(fd);
+    EXPECT_EQ(ret, (ssize_t)sizeof(myContent));
+    EXPECT_EQ(0, strcmp(myContent, buf));
+    if (fd >= 0) {
+        fprintf(stderr, "Removing persistent test data, "
+                        "check if reconstructed on reboot\n");
+    }
+    EXPECT_EQ(0, unlink(myFilename));
+}
diff --git a/recovery/tools/Android.mk b/recovery/tools/Android.mk
new file mode 100644
index 0000000..6571161
--- /dev/null
+++ b/recovery/tools/Android.mk
@@ -0,0 +1 @@
+include $(all-subdir-makefiles)
diff --git a/recovery/tools/dumpkey/Android.mk b/recovery/tools/dumpkey/Android.mk
new file mode 100644
index 0000000..3154914
--- /dev/null
+++ b/recovery/tools/dumpkey/Android.mk
@@ -0,0 +1,22 @@
+# Copyright (C) 2008 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := dumpkey
+LOCAL_SRC_FILES := DumpPublicKey.java
+LOCAL_JAR_MANIFEST := DumpPublicKey.mf
+LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host
+include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/recovery/tools/dumpkey/DumpPublicKey.java b/recovery/tools/dumpkey/DumpPublicKey.java
new file mode 100644
index 0000000..3eb1398
--- /dev/null
+++ b/recovery/tools/dumpkey/DumpPublicKey.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2008 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.dumpkey;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import java.io.FileInputStream;
+import java.math.BigInteger;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.KeyStore;
+import java.security.Key;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.ECPoint;
+
+/**
+ * Command line tool to extract RSA public keys from X.509 certificates
+ * and output source code with data initializers for the keys.
+ * @hide
+ */
+class DumpPublicKey {
+    /**
+     * @param key to perform sanity checks on
+     * @return version number of key.  Supported versions are:
+     *     1: 2048-bit RSA key with e=3 and SHA-1 hash
+     *     2: 2048-bit RSA key with e=65537 and SHA-1 hash
+     *     3: 2048-bit RSA key with e=3 and SHA-256 hash
+     *     4: 2048-bit RSA key with e=65537 and SHA-256 hash
+     * @throws Exception if the key has the wrong size or public exponent
+     */
+    static int checkRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
+        BigInteger pubexp = key.getPublicExponent();
+        BigInteger modulus = key.getModulus();
+        int version;
+
+        if (pubexp.equals(BigInteger.valueOf(3))) {
+            version = useSHA256 ? 3 : 1;
+        } else if (pubexp.equals(BigInteger.valueOf(65537))) {
+            version = useSHA256 ? 4 : 2;
+        } else {
+            throw new Exception("Public exponent should be 3 or 65537 but is " +
+                                pubexp.toString(10) + ".");
+        }
+
+        if (modulus.bitLength() != 2048) {
+             throw new Exception("Modulus should be 2048 bits long but is " +
+                        modulus.bitLength() + " bits.");
+        }
+
+        return version;
+    }
+
+    /**
+     * @param key to perform sanity checks on
+     * @return version number of key.  Supported versions are:
+     *     5: 256-bit EC key with curve NIST P-256
+     * @throws Exception if the key has the wrong size or public exponent
+     */
+    static int checkEC(ECPublicKey key) throws Exception {
+        if (key.getParams().getCurve().getField().getFieldSize() != 256) {
+            throw new Exception("Curve must be NIST P-256");
+        }
+
+        return 5;
+    }
+
+    /**
+     * Perform sanity check on public key.
+     */
+    static int check(PublicKey key, boolean useSHA256) throws Exception {
+        if (key instanceof RSAPublicKey) {
+            return checkRSA((RSAPublicKey) key, useSHA256);
+        } else if (key instanceof ECPublicKey) {
+            if (!useSHA256) {
+                throw new Exception("Must use SHA-256 with EC keys!");
+            }
+            return checkEC((ECPublicKey) key);
+        } else {
+            throw new Exception("Unsupported key class: " + key.getClass().getName());
+        }
+    }
+
+    /**
+     * @param key to output
+     * @return a String representing this public key.  If the key is a
+     *    version 1 key, the string will be a C initializer; this is
+     *    not true for newer key versions.
+     */
+    static String printRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
+        int version = check(key, useSHA256);
+
+        BigInteger N = key.getModulus();
+
+        StringBuilder result = new StringBuilder();
+
+        int nwords = N.bitLength() / 32;    // # of 32 bit integers in modulus
+
+        if (version > 1) {
+            result.append("v");
+            result.append(Integer.toString(version));
+            result.append(" ");
+        }
+
+        result.append("{");
+        result.append(nwords);
+
+        BigInteger B = BigInteger.valueOf(0x100000000L);  // 2^32
+        BigInteger N0inv = B.subtract(N.modInverse(B));   // -1 / N[0] mod 2^32
+
+        result.append(",0x");
+        result.append(N0inv.toString(16));
+
+        BigInteger R = BigInteger.valueOf(2).pow(N.bitLength());
+        BigInteger RR = R.multiply(R).mod(N);    // 2^4096 mod N
+
+        // Write out modulus as little endian array of integers.
+        result.append(",{");
+        for (int i = 0; i < nwords; ++i) {
+            long n = N.mod(B).longValue();
+            result.append(n);
+
+            if (i != nwords - 1) {
+                result.append(",");
+            }
+
+            N = N.divide(B);
+        }
+        result.append("}");
+
+        // Write R^2 as little endian array of integers.
+        result.append(",{");
+        for (int i = 0; i < nwords; ++i) {
+            long rr = RR.mod(B).longValue();
+            result.append(rr);
+
+            if (i != nwords - 1) {
+                result.append(",");
+            }
+
+            RR = RR.divide(B);
+        }
+        result.append("}");
+
+        result.append("}");
+        return result.toString();
+    }
+
+    /**
+     * @param key to output
+     * @return a String representing this public key.  If the key is a
+     *    version 1 key, the string will be a C initializer; this is
+     *    not true for newer key versions.
+     */
+    static String printEC(ECPublicKey key) throws Exception {
+        int version = checkEC(key);
+
+        StringBuilder result = new StringBuilder();
+
+        result.append("v");
+        result.append(Integer.toString(version));
+        result.append(" ");
+
+        BigInteger X = key.getW().getAffineX();
+        BigInteger Y = key.getW().getAffineY();
+        int nbytes = key.getParams().getCurve().getField().getFieldSize() / 8;    // # of 32 bit integers in X coordinate
+
+        result.append("{");
+        result.append(nbytes);
+
+        BigInteger B = BigInteger.valueOf(0x100L);  // 2^8
+
+        // Write out Y coordinate as array of characters.
+        result.append(",{");
+        for (int i = 0; i < nbytes; ++i) {
+            long n = X.mod(B).longValue();
+            result.append(n);
+
+            if (i != nbytes - 1) {
+                result.append(",");
+            }
+
+            X = X.divide(B);
+        }
+        result.append("}");
+
+        // Write out Y coordinate as array of characters.
+        result.append(",{");
+        for (int i = 0; i < nbytes; ++i) {
+            long n = Y.mod(B).longValue();
+            result.append(n);
+
+            if (i != nbytes - 1) {
+                result.append(",");
+            }
+
+            Y = Y.divide(B);
+        }
+        result.append("}");
+
+        result.append("}");
+        return result.toString();
+    }
+
+    static String print(PublicKey key, boolean useSHA256) throws Exception {
+        if (key instanceof RSAPublicKey) {
+            return printRSA((RSAPublicKey) key, useSHA256);
+        } else if (key instanceof ECPublicKey) {
+            return printEC((ECPublicKey) key);
+        } else {
+            throw new Exception("Unsupported key class: " + key.getClass().getName());
+        }
+    }
+
+    public static void main(String[] args) {
+        if (args.length < 1) {
+            System.err.println("Usage: DumpPublicKey certfile ... > source.c");
+            System.exit(1);
+        }
+        Security.addProvider(new BouncyCastleProvider());
+        try {
+            for (int i = 0; i < args.length; i++) {
+                FileInputStream input = new FileInputStream(args[i]);
+                CertificateFactory cf = CertificateFactory.getInstance("X.509");
+                X509Certificate cert = (X509Certificate) cf.generateCertificate(input);
+
+                boolean useSHA256 = false;
+                String sigAlg = cert.getSigAlgName();
+                if ("SHA1withRSA".equals(sigAlg) || "MD5withRSA".equals(sigAlg)) {
+                    // SignApk has historically accepted "MD5withRSA"
+                    // certificates, but treated them as "SHA1withRSA"
+                    // anyway.  Continue to do so for backwards
+                    // compatibility.
+                  useSHA256 = false;
+                } else if ("SHA256withRSA".equals(sigAlg) || "SHA256withECDSA".equals(sigAlg)) {
+                  useSHA256 = true;
+                } else {
+                  System.err.println(args[i] + ": unsupported signature algorithm \"" +
+                                     sigAlg + "\"");
+                  System.exit(1);
+                }
+
+                PublicKey key = cert.getPublicKey();
+                check(key, useSHA256);
+                System.out.print(print(key, useSHA256));
+                System.out.println(i < args.length - 1 ? "," : "");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(1);
+        }
+        System.exit(0);
+    }
+}
diff --git a/recovery/tools/dumpkey/DumpPublicKey.mf b/recovery/tools/dumpkey/DumpPublicKey.mf
new file mode 100644
index 0000000..7bb3bc8
--- /dev/null
+++ b/recovery/tools/dumpkey/DumpPublicKey.mf
@@ -0,0 +1 @@
+Main-Class: com.android.dumpkey.DumpPublicKey
diff --git a/recovery/tools/recovery_l10n/Android.mk b/recovery/tools/recovery_l10n/Android.mk
new file mode 100644
index 0000000..937abd1
--- /dev/null
+++ b/recovery/tools/recovery_l10n/Android.mk
@@ -0,0 +1,12 @@
+# Copyright 2012 Google Inc. All Rights Reserved.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := RecoveryLocalizer
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_PACKAGE)
diff --git a/recovery/tools/recovery_l10n/AndroidManifest.xml b/recovery/tools/recovery_l10n/AndroidManifest.xml
new file mode 100644
index 0000000..8c51a4e
--- /dev/null
+++ b/recovery/tools/recovery_l10n/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.recovery_l10n">
+
+  <application android:label="Recovery Localizer">
+    <activity android:name="Main"
+              android:label="Recovery Localizer">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+
+</manifest>
+
+
diff --git a/recovery/tools/recovery_l10n/README.md b/recovery/tools/recovery_l10n/README.md
new file mode 100644
index 0000000..1554f66
--- /dev/null
+++ b/recovery/tools/recovery_l10n/README.md
@@ -0,0 +1,33 @@
+# Steps to regenerate background text images under res-*dpi/images/
+
+1.  Build the recovery_l10n app:
+
+    cd bootable/recovery && mma -j32
+
+2.  Install the app on the device (or emulator) with the intended dpi.
+
+    *   For example, we can use Nexus 5 to generate the text images under
+        res-xxhdpi.
+    *   When using the emulator, make sure the NDK version matches the current
+        repository. Otherwise, the app may not work properly.
+
+    adb install $PATH_TO_APP
+
+3.  Run the app, select the string to translate and press the 'go' button.
+
+4.  After the app goes through the strings for all locales, pull the output png
+    file from the device.
+
+    adb root && adb pull /data/data/com.android.recovery_l10n/files/text-out.png
+
+5.  Compress the output file put it under the corresponding directory.
+
+    *   "pngcrush -c 0 ..." converts "text-out.png" into a 1-channel image,
+        which is accepted by Recovery. This also compresses the image file by
+        ~60%.
+    *   zopflipng could further compress the png files by ~10%, more details
+        in https://github.com/google/zopfli/blob/master/README.zopflipng
+    *   If you're using other png compression tools, make sure the final text
+        image works by running graphic tests under the recovery mode.
+
+    pngcrush -c 0 text-out.png $OUTPUT_PNG
diff --git a/recovery/tools/recovery_l10n/res/layout/main.xml b/recovery/tools/recovery_l10n/res/layout/main.xml
new file mode 100644
index 0000000..0900b11
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/layout/main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              >
+
+  <Spinner android:id="@+id/which"
+           android:layout_width="wrap_content"
+           android:layout_height="wrap_content"
+           />
+
+  <Button android:id="@+id/go"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:text="@string/go"
+          />
+
+  <TextView android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#ffffffff"
+            android:background="#ff000000"
+            android:maxWidth="480px"
+            android:gravity="center"
+            />
+
+
+</LinearLayout>
+
+
diff --git a/recovery/tools/recovery_l10n/res/values-af/strings.xml b/recovery/tools/recovery_l10n/res/values-af/strings.xml
new file mode 100644
index 0000000..d526418
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-af/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installeer tans stelselopdatering..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Vee tans uit..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Geen bevel."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Fout!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-am/strings.xml b/recovery/tools/recovery_l10n/res/values-am/strings.xml
new file mode 100644
index 0000000..cddb099
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-am/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"የስርዓት ዝማኔ በመጫን ላይ…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"በመደምሰስ ላይ…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ምንም ትዕዛዝ የለም።"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ስህተት!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ar/strings.xml b/recovery/tools/recovery_l10n/res/values-ar/strings.xml
new file mode 100644
index 0000000..d06b966
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ar/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"جارٍ تثبيت تحديث النظام…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"جارٍ المسح…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ليس هناك أي أمر."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"خطأ!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-az-rAZ/strings.xml b/recovery/tools/recovery_l10n/res/values-az-rAZ/strings.xml
new file mode 100644
index 0000000..3435573
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-az-rAZ/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Sistem güncəlləməsi quraşdırılır..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Silinir..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Əmr yoxdur."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Xəta!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-bg/strings.xml b/recovery/tools/recovery_l10n/res/values-bg/strings.xml
new file mode 100644
index 0000000..004f3b9
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-bg/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Системната актуализация се инсталира…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Изтрива се…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Без команда."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Грешка!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-bn-rBD/strings.xml b/recovery/tools/recovery_l10n/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..4d2e590
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-bn-rBD/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"সিস্টেম আপডেট ইনস্টল করা হচ্ছে…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"মোছা হচ্ছে…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"কোনো নির্দেশ নেই।"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ত্রুটি!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ca/strings.xml b/recovery/tools/recovery_l10n/res/values-ca/strings.xml
new file mode 100644
index 0000000..5d7b652
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ca/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"S\'està instal·lant l\'actualització del sistema..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"S\'està esborrant..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Cap ordre."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-cs/strings.xml b/recovery/tools/recovery_l10n/res/values-cs/strings.xml
new file mode 100644
index 0000000..771235d
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-cs/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalace aktualizace systému..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Mazání…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Žádný příkaz."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Chyba!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-da/strings.xml b/recovery/tools/recovery_l10n/res/values-da/strings.xml
new file mode 100644
index 0000000..c28a76f
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-da/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Systemopdateringen installeres…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Sletter…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ingen kommando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Fejl!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-de/strings.xml b/recovery/tools/recovery_l10n/res/values-de/strings.xml
new file mode 100644
index 0000000..02d2590
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-de/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Systemupdate wird installiert…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Wird gelöscht…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Kein Befehl"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Fehler"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-el/strings.xml b/recovery/tools/recovery_l10n/res/values-el/strings.xml
new file mode 100644
index 0000000..aa2626b
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-el/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Εγκατάσταση ενημέρωσης συστήματος…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Διαγραφή…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Καμία εντολή."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Σφάλμα!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-en-rAU/strings.xml b/recovery/tools/recovery_l10n/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..b70d678
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-en-rAU/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installing system update…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Erasing…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"No command."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-en-rGB/strings.xml b/recovery/tools/recovery_l10n/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..b70d678
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-en-rGB/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installing system update…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Erasing…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"No command."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-en-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..b70d678
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-en-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installing system update…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Erasing…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"No command."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-es-rUS/strings.xml b/recovery/tools/recovery_l10n/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..256272a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-es-rUS/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalando actualización del sistema…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Borrando…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ningún comando"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-es/strings.xml b/recovery/tools/recovery_l10n/res/values-es/strings.xml
new file mode 100644
index 0000000..323f055
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-es/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalando actualización del sistema…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Borrando…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Sin comandos"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-et-rEE/strings.xml b/recovery/tools/recovery_l10n/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..407a53d
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-et-rEE/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Süsteemivärskenduste installimine ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Kustutamine ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Käsk puudub."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Viga!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-eu-rES/strings.xml b/recovery/tools/recovery_l10n/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..08d9c06
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-eu-rES/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Sistemaren eguneratzea instalatzen…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Ezabatzen…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ez dago agindurik."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Errorea!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-fa/strings.xml b/recovery/tools/recovery_l10n/res/values-fa/strings.xml
new file mode 100644
index 0000000..dd002fa
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-fa/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"در حال نصب به‌روزرسانی سیستم ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"پاک کردن..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"فرمانی موجود نیست."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"خطا!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-fi/strings.xml b/recovery/tools/recovery_l10n/res/values-fi/strings.xml
new file mode 100644
index 0000000..b77417a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-fi/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Asennetaan järjestelmäpäivitystä..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Tyhjennetään..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ei komentoa."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Virhe!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-fr-rCA/strings.xml b/recovery/tools/recovery_l10n/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..f2a85d8
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-fr-rCA/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installation de la mise à jour du système en cours…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Effacement en cours…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Aucune commande."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erreur!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-fr/strings.xml b/recovery/tools/recovery_l10n/res/values-fr/strings.xml
new file mode 100644
index 0000000..cdb4a26
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-fr/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installation de la mise à jour du système en cours…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Effacement en cours…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Aucune commande."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erreur !"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-gl-rES/strings.xml b/recovery/tools/recovery_l10n/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..7546fbd
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-gl-rES/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalando actualización do sistema..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Borrando..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ningún comando"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erro"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-gu-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000..a364b52
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-gu-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"સિસ્ટમ અપડેટ ઇન્સ્ટોલ કરી રહ્યાં છે…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"કાઢી નાખી રહ્યાં છે…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"કોઈ આદેશ નથી."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ભૂલ!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-hi/strings.xml b/recovery/tools/recovery_l10n/res/values-hi/strings.xml
new file mode 100644
index 0000000..a470d12
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-hi/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"सिस्टम के बारे में नई जानकारी मिल रही है…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"मिटा रहा है…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"कोई आदेश नहीं."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"त्रुटि!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-hr/strings.xml b/recovery/tools/recovery_l10n/res/values-hr/strings.xml
new file mode 100644
index 0000000..56225c0
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-hr/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instaliranje ažuriranja sustava…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Brisanje…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nema naredbe."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Pogreška!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-hu/strings.xml b/recovery/tools/recovery_l10n/res/values-hu/strings.xml
new file mode 100644
index 0000000..a64f501
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-hu/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Rendszerfrissítés telepítése..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Törlés..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nincs parancs."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Hiba!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-hy-rAM/strings.xml b/recovery/tools/recovery_l10n/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..7babe80
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-hy-rAM/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Համակարգի թարմացման տեղադրում…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Ջնջում…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Հրամանը տրված չէ:"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Սխալ"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-in/strings.xml b/recovery/tools/recovery_l10n/res/values-in/strings.xml
new file mode 100644
index 0000000..93f9c28
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-in/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Memasang pembaruan sistem…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Menghapus..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Tidak ada perintah."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Kesalahan!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-is-rIS/strings.xml b/recovery/tools/recovery_l10n/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..926e851
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-is-rIS/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Setur upp kerfisuppfærslu…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Þurrkar út…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Engin skipun."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Villa!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-it/strings.xml b/recovery/tools/recovery_l10n/res/values-it/strings.xml
new file mode 100644
index 0000000..9defe36
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-it/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installazione aggiornamento di sistema…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Cancellazione…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nessun comando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Errore!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-iw/strings.xml b/recovery/tools/recovery_l10n/res/values-iw/strings.xml
new file mode 100644
index 0000000..e43bb20
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-iw/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"מתקין עדכון מערכת…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"מוחק…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"אין פקודה."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"שגיאה!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ja/strings.xml b/recovery/tools/recovery_l10n/res/values-ja/strings.xml
new file mode 100644
index 0000000..da0fa62
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ja/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"システムアップデートをインストールしています…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"消去しています…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"コマンドが指定されていません。"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"エラーです"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ka-rGE/strings.xml b/recovery/tools/recovery_l10n/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..2d27c17
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ka-rGE/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"სისტემის განახლების დაყენება…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"მიმდინარეობს წაშლა…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ბრძანება არ არის."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"შეცდომა!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-kk-rKZ/strings.xml b/recovery/tools/recovery_l10n/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000..3ca05b9
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Жүйе жаңартуларын орнатуда…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Өшіруде..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Пәрмен берілген жоқ."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Қате!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-km-rKH/strings.xml b/recovery/tools/recovery_l10n/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..0c1c272
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-km-rKH/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"កំពុង​ដំឡើង​បច្ចុប្បន្នភាព​ប្រព័ន្ធ…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"កំពុង​លុប…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"គ្មាន​ពាក្យ​បញ្ជា។"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"កំហុស!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-kn-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..be25d7a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-kn-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"ಸಿಸ್ಟಂ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"ಅಳಿಸಲಾಗುತ್ತಿದೆ…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ಯಾವುದೇ ಆದೇಶವಿಲ್ಲ."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ದೋಷ!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ko/strings.xml b/recovery/tools/recovery_l10n/res/values-ko/strings.xml
new file mode 100644
index 0000000..e46a876
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ko/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"시스템 업데이트 설치 중…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"지우는 중…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"명령어가 없습니다."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"오류!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ky-rKG/strings.xml b/recovery/tools/recovery_l10n/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..e2ced27
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ky-rKG/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Системдик жаңыртууларды орнотуу…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Өчүрүлүүдө…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Буйрук берилген жок."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Ката!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-lo-rLA/strings.xml b/recovery/tools/recovery_l10n/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..5880cca
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-lo-rLA/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"ກຳລັງຕິດຕັ້ງການອັບເດດລະບົບ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"ກຳລັງລຶບ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ບໍ່ມີຄຳສັ່ງ."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ຜິດພາດ!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-lt/strings.xml b/recovery/tools/recovery_l10n/res/values-lt/strings.xml
new file mode 100644
index 0000000..957ac75
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-lt/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Diegiamas sistemos naujinys…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Ištrinama…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nėra komandos."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Klaida!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-lv/strings.xml b/recovery/tools/recovery_l10n/res/values-lv/strings.xml
new file mode 100644
index 0000000..c5d5b93
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-lv/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Notiek sistēmas atjauninājuma instalēšana..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Notiek dzēšana..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nav nevienas komandas."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Kļūda!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-mk-rMK/strings.xml b/recovery/tools/recovery_l10n/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..d91a67c
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-mk-rMK/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Се инсталира ажурирање на системот..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Се брише..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Нема наредба."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Грешка!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ml-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..38ebcd1
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ml-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"സിസ്റ്റം അപ്‌ഡേറ്റ് ഇൻസ്റ്റാളുചെയ്യുന്നു…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"മായ്‌ക്കുന്നു…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"കമാൻഡ് ഒന്നുമില്ല."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"പിശക്!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-mn-rMN/strings.xml b/recovery/tools/recovery_l10n/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..463cafe
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-mn-rMN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Системийн шинэчлэлтийг суулгаж байна…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Арилгаж байна…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Команд байхгүй."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Алдаа!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-mr-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..25c5d0c
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-mr-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"सिस्टम अद्यतन स्थापित करीत आहे..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"मिटवित आहे…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"कोणताही आदेश नाही."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"त्रुटी!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ms-rMY/strings.xml b/recovery/tools/recovery_l10n/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..f563591
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ms-rMY/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Memasang kemas kini sistem..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Memadam..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Tiada arahan."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Ralat!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-my-rMM/strings.xml b/recovery/tools/recovery_l10n/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..4091b19
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-my-rMM/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"စနစ်အား အဆင့်မြှင့်ခြင်း လုပ်ဆောင်နေသည်…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"ဖျက်နေသည် ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ညွှန်ကြားချက်မပေးထားပါ"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"မှားနေပါသည်!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-nb/strings.xml b/recovery/tools/recovery_l10n/res/values-nb/strings.xml
new file mode 100644
index 0000000..4e89ad7
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-nb/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installerer systemoppdateringen ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Sletter ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ingen kommando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Feil!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ne-rNP/strings.xml b/recovery/tools/recovery_l10n/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..835f275
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ne-rNP/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"प्रणाली अद्यावधिक स्थापना गर्दै..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"मेटाइदै..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"कुनै आदेश छैन।"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"त्रुटि!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-nl/strings.xml b/recovery/tools/recovery_l10n/res/values-nl/strings.xml
new file mode 100644
index 0000000..be80a6b
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-nl/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Systeemupdate installeren…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Wissen…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Geen opdracht."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Fout!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-pa-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000..39ef32f
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-pa-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"ਸਿਸਟਮ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕਰ ਰਿਹਾ ਹੈ…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"ਹਟਾ ਰਿਹਾ ਹੈ…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ਕੋਈ ਕਮਾਂਡ ਨਹੀਂ।"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ਅਸ਼ੁੱਧੀ!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-pl/strings.xml b/recovery/tools/recovery_l10n/res/values-pl/strings.xml
new file mode 100644
index 0000000..b1e5b7b
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-pl/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instaluję aktualizację systemu…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Usuwam…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Brak polecenia."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Błąd"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-pt-rBR/strings.xml b/recovery/tools/recovery_l10n/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..3cc5723
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-pt-rBR/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalando atualização do sistema..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Apagando..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nenhum comando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erro!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-pt-rPT/strings.xml b/recovery/tools/recovery_l10n/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..7d6bc18
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-pt-rPT/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"A instalar a atualização do sistema..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"A apagar…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nenhum comando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erro!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-pt/strings.xml b/recovery/tools/recovery_l10n/res/values-pt/strings.xml
new file mode 100644
index 0000000..3cc5723
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-pt/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Instalando atualização do sistema..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Apagando..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nenhum comando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Erro!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ro/strings.xml b/recovery/tools/recovery_l10n/res/values-ro/strings.xml
new file mode 100644
index 0000000..ad924da
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ro/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Se instalează actualizarea de sistem…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Se efectuează ștergerea…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nicio comandă."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Eroare!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ru/strings.xml b/recovery/tools/recovery_l10n/res/values-ru/strings.xml
new file mode 100644
index 0000000..de0da40
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ru/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Установка обновления системы…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Удаление…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Команды нет"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Ошибка"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-si-rLK/strings.xml b/recovery/tools/recovery_l10n/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..e717a97
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-si-rLK/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"පද්ධති යාවත්කාල ස්ථාපනය කරමින්…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"මකමින්...."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"විධානයක් නොමැත."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"දෝෂය!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sk/strings.xml b/recovery/tools/recovery_l10n/res/values-sk/strings.xml
new file mode 100644
index 0000000..cae6bce
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sk/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Inštalácia aktualizácie systému..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Prebieha mazanie..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Žiadny príkaz."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Chyba!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sl/strings.xml b/recovery/tools/recovery_l10n/res/values-sl/strings.xml
new file mode 100644
index 0000000..3f8d46f
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sl/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Namestitev posodobitve sistema ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Brisanje ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Ni ukaza"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Napaka"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sq-rAL/strings.xml b/recovery/tools/recovery_l10n/res/values-sq-rAL/strings.xml
new file mode 100644
index 0000000..29f8ef5
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sq-rAL/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Po instalon përditësimin e sistemit..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Po spastron..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Nuk ka komanda."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Gabim!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sr/strings.xml b/recovery/tools/recovery_l10n/res/values-sr/strings.xml
new file mode 100644
index 0000000..9553260
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sr/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Инсталирање ажурирања система..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Брисање..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Нема команде."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Грешка!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sv/strings.xml b/recovery/tools/recovery_l10n/res/values-sv/strings.xml
new file mode 100644
index 0000000..f875d30
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sv/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Installerar systemuppdatering ..."</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Tar bort ..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Inget kommando."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Fel!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-sw/strings.xml b/recovery/tools/recovery_l10n/res/values-sw/strings.xml
new file mode 100644
index 0000000..1a53046
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-sw/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Inasakinisha sasisho la mfumo…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Inafuta…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Hakuna amri."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Hitilafu!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ta-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..f6f3e0e
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ta-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"முறைமை புதுப்பிப்பை நிறுவுகிறது…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"அழிக்கிறது…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"கட்டளை இல்லை."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"பிழை!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-te-rIN/strings.xml b/recovery/tools/recovery_l10n/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..6d0d17a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-te-rIN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"సిస్టమ్ నవీకరణను ఇన్‍స్టాల్ చేస్తోంది…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"ఎరేజ్ చేస్తోంది…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ఆదేశం లేదు."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"లోపం!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-th/strings.xml b/recovery/tools/recovery_l10n/res/values-th/strings.xml
new file mode 100644
index 0000000..bcdfa2b
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-th/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"กำลังติดตั้งการอัปเดตระบบ…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"กำลังลบ…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"ไม่มีคำสั่ง"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"ข้อผิดพลาด!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-tl/strings.xml b/recovery/tools/recovery_l10n/res/values-tl/strings.xml
new file mode 100644
index 0000000..be2ba26
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-tl/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Ini-install ang update sa system…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Binubura…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Walang command."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Error!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-tr/strings.xml b/recovery/tools/recovery_l10n/res/values-tr/strings.xml
new file mode 100644
index 0000000..8629029
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-tr/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Sistem güncellemesi yükleniyor…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Siliniyor…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Komut yok."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Hata!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-uk/strings.xml b/recovery/tools/recovery_l10n/res/values-uk/strings.xml
new file mode 100644
index 0000000..762c06f
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-uk/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Встановлення оновлення системи…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Стирання…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Немає команди."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Помилка!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-ur-rPK/strings.xml b/recovery/tools/recovery_l10n/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000..dc6eb6a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-ur-rPK/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"سسٹم اپ ڈیٹ انسٹال ہو رہا ہے…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"صاف کر رہا ہے…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"کوئی کمانڈ نہیں ہے۔"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"خرابی!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-uz-rUZ/strings.xml b/recovery/tools/recovery_l10n/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000..2874484
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Tizim yangilanishi o‘rnatilmoqda…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Tozalanmoqda…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Buyruq yo‘q."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Xato!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-vi/strings.xml b/recovery/tools/recovery_l10n/res/values-vi/strings.xml
new file mode 100644
index 0000000..ab4005b
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-vi/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Đang cài đặt bản cập nhật hệ thống…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Đang xóa…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Không có lệnh nào."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Lỗi!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-zh-rCN/strings.xml b/recovery/tools/recovery_l10n/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..2e1a6f5
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-zh-rCN/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"正在安装系统更新…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"正在清除…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"无命令。"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"出错了!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-zh-rHK/strings.xml b/recovery/tools/recovery_l10n/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..f615c7a
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-zh-rHK/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"正在安裝系統更新…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"正在清除…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"沒有指令。"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"錯誤!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-zh-rTW/strings.xml b/recovery/tools/recovery_l10n/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..f3f6a2c
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-zh-rTW/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"正在安裝系統更新…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"清除中..."</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"沒有指令。"</string>
+    <string name="recovery_error" msgid="4550265746256727080">"錯誤!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values-zu/strings.xml b/recovery/tools/recovery_l10n/res/values-zu/strings.xml
new file mode 100644
index 0000000..1f904a2
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values-zu/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recovery_installing" msgid="7864047928003865598">"Ifaka isibuyekezo sesistimu…"</string>
+    <string name="recovery_erasing" msgid="4612809744968710197">"Iyasula…"</string>
+    <string name="recovery_no_command" msgid="1915703879031023455">"Awukho umyalo."</string>
+    <string name="recovery_error" msgid="4550265746256727080">"Iphutha!"</string>
+</resources>
diff --git a/recovery/tools/recovery_l10n/res/values/strings.xml b/recovery/tools/recovery_l10n/res/values/strings.xml
new file mode 100644
index 0000000..48d18da
--- /dev/null
+++ b/recovery/tools/recovery_l10n/res/values/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <!-- Do not translate. -->
+  <string translatable="false" name="go">Go</string>
+
+  <!-- Do not translate. -->
+  <string-array translatable="false" name="string_options">
+    <item>installing</item>
+    <item>erasing</item>
+    <item>no_command</item>
+    <item>error</item>
+    <item>installing_security</item>
+  </string-array>
+
+  <!-- Displayed on the screen beneath the animated android while the
+       system is installing an update. [CHAR LIMIT=60] -->
+  <string name="recovery_installing">Installing system update\u2026</string>
+
+  <!-- Displayed on the screen beneath the animated android while the
+       system is erasing a partition (either a data wipe aka "factory
+       reset", or a cache wipe). [CHAR LIMIT=60] -->
+  <string name="recovery_erasing">Erasing\u2026</string>
+
+  <!-- Displayed on the screen when the user has gotten into recovery
+       mode without a command to run.  Will not normally happen, but
+       users (especially developers) may boot into recovery mode
+       manually via special key combinations.  [CHAR LIMIT=60] -->
+  <string name="recovery_no_command">No command.</string>
+
+  <!-- Displayed on the triangle-! screen when a system update
+       installation or data wipe procedure encounters an error.  [CHAR
+       LIMIT=60] -->
+  <string name="recovery_error">Error!</string>
+
+  <!-- Displayed on the screen beneath the animation while the
+       system is installing a security update. [CHAR LIMIT=60] -->
+  <string name="recovery_installing_security">Installing security update\u2026</string>
+
+</resources>
diff --git a/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java b/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java
new file mode 100644
index 0000000..a974fa3
--- /dev/null
+++ b/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2012 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.recovery_l10n;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Spinner;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+
+/**
+ * This activity assists in generating the specially-formatted bitmaps
+ * of text needed for recovery's localized text display.  Each image
+ * contains all the translations of a single string; above each
+ * translation is a "header row" that encodes that subimage's width,
+ * height, and locale using pixel values.
+ *
+ * To use this app to generate new translations:
+ *
+ *   - Update the string resources in res/values-*
+ *
+ *   - Build and run the app.  Select the string you want to
+ *     translate, and press the "Go" button.
+ *
+ *   - Wait for it to finish cycling through all the strings, then
+ *     pull /data/data/com.android.recovery_l10n/files/text-out.png
+ *     from the device.
+ *
+ *   - "pngcrush -c 0 text-out.png output.png"
+ *
+ *   - Put output.png in bootable/recovery/res/images/ (renamed
+ *     appropriately).
+ *
+ * Recovery expects 8-bit 1-channel images (white text on black
+ * background).  pngcrush -c 0 will convert the output of this program
+ * to such an image.  If you use any other image handling tools,
+ * remember that they must be lossless to preserve the exact values of
+ * pixels in the header rows; don't convert them to jpeg or anything.
+ */
+
+public class Main extends Activity {
+    private static final String TAG = "RecoveryL10N";
+
+    HashMap<Locale, Bitmap> savedBitmaps;
+    TextView mText;
+    int mStringId = R.string.recovery_installing;
+
+    public class TextCapture implements Runnable {
+        private Locale nextLocale;
+        private Locale thisLocale;
+        private Runnable next;
+
+        TextCapture(Locale thisLocale, Locale nextLocale, Runnable next) {
+            this.nextLocale = nextLocale;
+            this.thisLocale = thisLocale;
+            this.next = next;
+        }
+
+        public void run() {
+            Bitmap b = mText.getDrawingCache();
+            savedBitmaps.put(thisLocale, b.copy(Bitmap.Config.ARGB_8888, false));
+
+            if (nextLocale != null) {
+                switchTo(nextLocale);
+            }
+
+            if (next != null) {
+                mText.postDelayed(next, 200);
+            }
+        }
+    }
+
+    private void switchTo(Locale locale) {
+        Resources standardResources = getResources();
+        AssetManager assets = standardResources.getAssets();
+        DisplayMetrics metrics = standardResources.getDisplayMetrics();
+        Configuration config = new Configuration(standardResources.getConfiguration());
+        config.locale = locale;
+        Resources defaultResources = new Resources(assets, metrics, config);
+
+        mText.setText(mStringId);
+
+        mText.setDrawingCacheEnabled(false);
+        mText.setDrawingCacheEnabled(true);
+        mText.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstance) {
+        super.onCreate(savedInstance);
+        setContentView(R.layout.main);
+
+        savedBitmaps = new HashMap<Locale, Bitmap>();
+
+        Spinner spinner = (Spinner) findViewById(R.id.which);
+        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
+            this, R.array.string_options, android.R.layout.simple_spinner_item);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinner.setAdapter(adapter);
+        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView parent, View view,
+                                       int pos, long id) {
+                switch (pos) {
+                    case 0: mStringId = R.string.recovery_installing; break;
+                    case 1: mStringId = R.string.recovery_erasing; break;
+                    case 2: mStringId = R.string.recovery_no_command; break;
+                    case 3: mStringId = R.string.recovery_error; break;
+                    case 4: mStringId = R.string.recovery_installing_security; break;
+                }
+            }
+            @Override public void onNothingSelected(AdapterView parent) { }
+            });
+
+        mText = (TextView) findViewById(R.id.text);
+
+        String[] localeNames = getAssets().getLocales();
+        Arrays.sort(localeNames);
+        ArrayList<Locale> locales = new ArrayList<Locale>();
+        for (String ln : localeNames) {
+            int u = ln.indexOf('_');
+            if (u >= 0) {
+                Log.i(TAG, "locale = " + ln);
+                locales.add(new Locale(ln.substring(0, u), ln.substring(u+1)));
+            }
+        }
+
+        final Runnable seq = buildSequence(locales.toArray(new Locale[0]));
+
+        Button b = (Button) findViewById(R.id.go);
+        b.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View ignore) {
+                mText.post(seq);
+            }
+            });
+    }
+
+    private Runnable buildSequence(final Locale[] locales) {
+        Runnable head = new Runnable() { public void run() { mergeBitmaps(locales); } };
+        Locale prev = null;
+        for (Locale loc : locales) {
+            head = new TextCapture(loc, prev, head);
+            prev = loc;
+        }
+        final Runnable fhead = head;
+        final Locale floc = prev;
+        return new Runnable() { public void run() { startSequence(fhead, floc); } };
+    }
+
+    private void startSequence(Runnable firstRun, Locale firstLocale) {
+        savedBitmaps.clear();
+        switchTo(firstLocale);
+        mText.postDelayed(firstRun, 200);
+    }
+
+    private void saveBitmap(Bitmap b, String filename) {
+        try {
+            FileOutputStream fos = openFileOutput(filename, 0);
+            b.compress(Bitmap.CompressFormat.PNG, 100, fos);
+            fos.close();
+        } catch (IOException e) {
+            Log.i(TAG, "failed to write PNG", e);
+        }
+    }
+
+    private int colorFor(byte b) {
+        return 0xff000000 | (b<<16) | (b<<8) | b;
+    }
+
+    private int colorFor(int b) {
+        return 0xff000000 | (b<<16) | (b<<8) | b;
+    }
+
+    private void mergeBitmaps(final Locale[] locales) {
+        HashMap<String, Integer> countByLanguage = new HashMap<String, Integer>();
+
+        int height = 2;
+        int width = 10;
+        int maxHeight = 0;
+        for (Locale loc : locales) {
+            Bitmap b = savedBitmaps.get(loc);
+            int h = b.getHeight();
+            int w = b.getWidth();
+            height += h+1;
+            if (h > maxHeight) maxHeight = h;
+            if (w > width) width = w;
+
+            String lang = loc.getLanguage();
+            if (countByLanguage.containsKey(lang)) {
+                countByLanguage.put(lang, countByLanguage.get(lang)+1);
+            } else {
+                countByLanguage.put(lang, 1);
+            }
+        }
+
+        Log.i(TAG, "output bitmap is " + width + " x " + height);
+        Bitmap out = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        out.eraseColor(0xff000000);
+        int[] pixels = new int[maxHeight * width];
+
+        int p = 0;
+        for (Locale loc : locales) {
+            Bitmap bm = savedBitmaps.get(loc);
+            int h = bm.getHeight();
+            int w = bm.getWidth();
+
+            bm.getPixels(pixels, 0, w, 0, 0, w, h);
+
+            // Find the rightmost and leftmost columns with any
+            // nonblack pixels; we'll copy just that region to the
+            // output image.
+
+            int right = w;
+            while (right > 1) {
+                boolean all_black = true;
+                for (int j = 0; j < h; ++j) {
+                    if (pixels[j*w+right-1] != 0xff000000) {
+                        all_black = false;
+                        break;
+                    }
+                }
+                if (all_black) {
+                    --right;
+                } else {
+                    break;
+                }
+            }
+
+            int left = 0;
+            while (left < right-1) {
+                boolean all_black = true;
+                for (int j = 0; j < h; ++j) {
+                    if (pixels[j*w+left] != 0xff000000) {
+                        all_black = false;
+                        break;
+                    }
+                }
+                if (all_black) {
+                    ++left;
+                } else {
+                    break;
+                }
+            }
+
+            // Make the last country variant for a given language be
+            // the catch-all for that language (because recovery will
+            // take the first one that matches).
+            String lang = loc.getLanguage();
+            if (countByLanguage.get(lang) > 1) {
+                countByLanguage.put(lang, countByLanguage.get(lang)-1);
+                lang = loc.toString();
+            }
+            int tw = right - left;
+            Log.i(TAG, "encoding \"" + loc + "\" as \"" + lang + "\": " + tw + " x " + h);
+            byte[] langBytes = lang.getBytes();
+            out.setPixel(0, p, colorFor(tw & 0xff));
+            out.setPixel(1, p, colorFor(tw >>> 8));
+            out.setPixel(2, p, colorFor(h & 0xff));
+            out.setPixel(3, p, colorFor(h >>> 8));
+            out.setPixel(4, p, colorFor(langBytes.length));
+            int x = 5;
+            for (byte b : langBytes) {
+                out.setPixel(x, p, colorFor(b));
+                x++;
+            }
+            out.setPixel(x, p, colorFor(0));
+
+            p++;
+
+            out.setPixels(pixels, left, w, 0, p, tw, h);
+            p += h;
+        }
+
+        // if no languages match, suppress text display by using a
+        // single black pixel as the image.
+        out.setPixel(0, p, colorFor(1));
+        out.setPixel(1, p, colorFor(0));
+        out.setPixel(2, p, colorFor(1));
+        out.setPixel(3, p, colorFor(0));
+        out.setPixel(4, p, colorFor(0));
+        p++;
+
+        saveBitmap(out, "text-out.png");
+        Log.i(TAG, "wrote text-out.png");
+    }
+}
diff --git a/recovery/ui.cpp b/recovery/ui.cpp
new file mode 100644
index 0000000..2efb759
--- /dev/null
+++ b/recovery/ui.cpp
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/input.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <cutils/properties.h>
+#include <cutils/android_reboot.h>
+
+#include "common.h"
+#include "roots.h"
+#include "device.h"
+#include "minui/minui.h"
+#include "screen_ui.h"
+#include "ui.h"
+
+#define UI_WAIT_KEY_TIMEOUT_SEC    120
+
+RecoveryUI::RecoveryUI()
+        : key_queue_len(0),
+          key_last_down(-1),
+          key_long_press(false),
+          key_down_count(0),
+          enable_reboot(true),
+          consecutive_power_keys(0),
+          last_key(-1),
+          has_power_key(false),
+          has_up_key(false),
+          has_down_key(false) {
+    pthread_mutex_init(&key_queue_mutex, nullptr);
+    pthread_cond_init(&key_queue_cond, nullptr);
+    memset(key_pressed, 0, sizeof(key_pressed));
+}
+
+void RecoveryUI::OnKeyDetected(int key_code) {
+    if (key_code == KEY_POWER) {
+        has_power_key = true;
+    } else if (key_code == KEY_DOWN || key_code == KEY_VOLUMEDOWN) {
+        has_down_key = true;
+    } else if (key_code == KEY_UP || key_code == KEY_VOLUMEUP) {
+        has_up_key = true;
+    }
+}
+
+int RecoveryUI::InputCallback(int fd, uint32_t epevents, void* data) {
+    return reinterpret_cast<RecoveryUI*>(data)->OnInputEvent(fd, epevents);
+}
+
+// Reads input events, handles special hot keys, and adds to the key queue.
+static void* InputThreadLoop(void*) {
+    while (true) {
+        if (!ev_wait(-1)) {
+            ev_dispatch();
+        }
+    }
+    return nullptr;
+}
+
+void RecoveryUI::Init() {
+    ev_init(InputCallback, this);
+
+    ev_iterate_available_keys(std::bind(&RecoveryUI::OnKeyDetected, this, std::placeholders::_1));
+
+    pthread_create(&input_thread_, nullptr, InputThreadLoop, nullptr);
+}
+
+int RecoveryUI::OnInputEvent(int fd, uint32_t epevents) {
+    struct input_event ev;
+    if (ev_get_input(fd, epevents, &ev) == -1) {
+        return -1;
+    }
+
+    if (ev.type == EV_SYN) {
+        return 0;
+    } else if (ev.type == EV_REL) {
+        if (ev.code == REL_Y) {
+            // accumulate the up or down motion reported by
+            // the trackball.  When it exceeds a threshold
+            // (positive or negative), fake an up/down
+            // key event.
+            rel_sum += ev.value;
+            if (rel_sum > 3) {
+                ProcessKey(KEY_DOWN, 1);   // press down key
+                ProcessKey(KEY_DOWN, 0);   // and release it
+                rel_sum = 0;
+            } else if (rel_sum < -3) {
+                ProcessKey(KEY_UP, 1);     // press up key
+                ProcessKey(KEY_UP, 0);     // and release it
+                rel_sum = 0;
+            }
+        }
+    } else {
+        rel_sum = 0;
+    }
+
+    if (ev.type == EV_KEY && ev.code <= KEY_MAX) {
+        ProcessKey(ev.code, ev.value);
+    }
+
+    return 0;
+}
+
+// Process a key-up or -down event.  A key is "registered" when it is
+// pressed and then released, with no other keypresses or releases in
+// between.  Registered keys are passed to CheckKey() to see if it
+// should trigger a visibility toggle, an immediate reboot, or be
+// queued to be processed next time the foreground thread wants a key
+// (eg, for the menu).
+//
+// We also keep track of which keys are currently down so that
+// CheckKey can call IsKeyPressed to see what other keys are held when
+// a key is registered.
+//
+// updown == 1 for key down events; 0 for key up events
+void RecoveryUI::ProcessKey(int key_code, int updown) {
+    bool register_key = false;
+    bool long_press = false;
+    bool reboot_enabled;
+
+    pthread_mutex_lock(&key_queue_mutex);
+    key_pressed[key_code] = updown;
+    if (updown) {
+        ++key_down_count;
+        key_last_down = key_code;
+        key_long_press = false;
+        key_timer_t* info = new key_timer_t;
+        info->ui = this;
+        info->key_code = key_code;
+        info->count = key_down_count;
+        pthread_t thread;
+        pthread_create(&thread, nullptr, &RecoveryUI::time_key_helper, info);
+        pthread_detach(thread);
+    } else {
+        if (key_last_down == key_code) {
+            long_press = key_long_press;
+            register_key = true;
+        }
+        key_last_down = -1;
+    }
+    reboot_enabled = enable_reboot;
+    pthread_mutex_unlock(&key_queue_mutex);
+
+    if (register_key) {
+        switch (CheckKey(key_code, long_press)) {
+          case RecoveryUI::IGNORE:
+            break;
+
+          case RecoveryUI::TOGGLE:
+            ShowText(!IsTextVisible());
+            break;
+
+          case RecoveryUI::REBOOT:
+            if (reboot_enabled) {
+                property_set(ANDROID_RB_PROPERTY, "reboot,");
+                while (true) { pause(); }
+            }
+            break;
+
+          case RecoveryUI::ENQUEUE:
+            EnqueueKey(key_code);
+            break;
+        }
+    }
+}
+
+void* RecoveryUI::time_key_helper(void* cookie) {
+    key_timer_t* info = (key_timer_t*) cookie;
+    info->ui->time_key(info->key_code, info->count);
+    delete info;
+    return nullptr;
+}
+
+void RecoveryUI::time_key(int key_code, int count) {
+    usleep(750000);  // 750 ms == "long"
+    bool long_press = false;
+    pthread_mutex_lock(&key_queue_mutex);
+    if (key_last_down == key_code && key_down_count == count) {
+        long_press = key_long_press = true;
+    }
+    pthread_mutex_unlock(&key_queue_mutex);
+    if (long_press) KeyLongPress(key_code);
+}
+
+void RecoveryUI::EnqueueKey(int key_code) {
+    pthread_mutex_lock(&key_queue_mutex);
+    const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
+    if (key_queue_len < queue_max) {
+        key_queue[key_queue_len++] = key_code;
+        pthread_cond_signal(&key_queue_cond);
+    }
+    pthread_mutex_unlock(&key_queue_mutex);
+}
+
+int RecoveryUI::WaitKey() {
+    pthread_mutex_lock(&key_queue_mutex);
+
+    // Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is
+    // plugged in.
+    do {
+        struct timeval now;
+        struct timespec timeout;
+        gettimeofday(&now, nullptr);
+        timeout.tv_sec = now.tv_sec;
+        timeout.tv_nsec = now.tv_usec * 1000;
+        timeout.tv_sec += UI_WAIT_KEY_TIMEOUT_SEC;
+
+        int rc = 0;
+        while (key_queue_len == 0 && rc != ETIMEDOUT) {
+            rc = pthread_cond_timedwait(&key_queue_cond, &key_queue_mutex, &timeout);
+        }
+    } while (IsUsbConnected() && key_queue_len == 0);
+
+    int key = -1;
+    if (key_queue_len > 0) {
+        key = key_queue[0];
+        memcpy(&key_queue[0], &key_queue[1], sizeof(int) * --key_queue_len);
+    }
+    pthread_mutex_unlock(&key_queue_mutex);
+    return key;
+}
+
+bool RecoveryUI::IsUsbConnected() {
+    int fd = open("/sys/class/android_usb/android0/state", O_RDONLY);
+    if (fd < 0) {
+        printf("failed to open /sys/class/android_usb/android0/state: %s\n",
+               strerror(errno));
+        return 0;
+    }
+
+    char buf;
+    // USB is connected if android_usb state is CONNECTED or CONFIGURED.
+    int connected = (TEMP_FAILURE_RETRY(read(fd, &buf, 1)) == 1) && (buf == 'C');
+    if (close(fd) < 0) {
+        printf("failed to close /sys/class/android_usb/android0/state: %s\n",
+               strerror(errno));
+    }
+    return connected;
+}
+
+bool RecoveryUI::IsKeyPressed(int key) {
+    pthread_mutex_lock(&key_queue_mutex);
+    int pressed = key_pressed[key];
+    pthread_mutex_unlock(&key_queue_mutex);
+    return pressed;
+}
+
+bool RecoveryUI::IsLongPress() {
+    pthread_mutex_lock(&key_queue_mutex);
+    bool result = key_long_press;
+    pthread_mutex_unlock(&key_queue_mutex);
+    return result;
+}
+
+bool RecoveryUI::HasThreeButtons() {
+    return has_power_key && has_up_key && has_down_key;
+}
+
+void RecoveryUI::FlushKeys() {
+    pthread_mutex_lock(&key_queue_mutex);
+    key_queue_len = 0;
+    pthread_mutex_unlock(&key_queue_mutex);
+}
+
+RecoveryUI::KeyAction RecoveryUI::CheckKey(int key, bool is_long_press) {
+    pthread_mutex_lock(&key_queue_mutex);
+    key_long_press = false;
+    pthread_mutex_unlock(&key_queue_mutex);
+
+    // If we have power and volume up keys, that chord is the signal to toggle the text display.
+    if (HasThreeButtons()) {
+        if (key == KEY_VOLUMEUP && IsKeyPressed(KEY_POWER)) {
+            return TOGGLE;
+        }
+    } else {
+        // Otherwise long press of any button toggles to the text display,
+        // and there's no way to toggle back (but that's pretty useless anyway).
+        if (is_long_press && !IsTextVisible()) {
+            return TOGGLE;
+        }
+
+        // Also, for button-limited devices, a long press is translated to KEY_ENTER.
+        if (is_long_press && IsTextVisible()) {
+            EnqueueKey(KEY_ENTER);
+            return IGNORE;
+        }
+    }
+
+    // Press power seven times in a row to reboot.
+    if (key == KEY_POWER) {
+        pthread_mutex_lock(&key_queue_mutex);
+        bool reboot_enabled = enable_reboot;
+        pthread_mutex_unlock(&key_queue_mutex);
+
+        if (reboot_enabled) {
+            ++consecutive_power_keys;
+            if (consecutive_power_keys >= 7) {
+                return REBOOT;
+            }
+        }
+    } else {
+        consecutive_power_keys = 0;
+    }
+
+    last_key = key;
+    return IsTextVisible() ? ENQUEUE : IGNORE;
+}
+
+void RecoveryUI::KeyLongPress(int) {
+}
+
+void RecoveryUI::SetEnableReboot(bool enabled) {
+    pthread_mutex_lock(&key_queue_mutex);
+    enable_reboot = enabled;
+    pthread_mutex_unlock(&key_queue_mutex);
+}
diff --git a/recovery/ui.h b/recovery/ui.h
new file mode 100644
index 0000000..ca72911
--- /dev/null
+++ b/recovery/ui.h
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+#ifndef RECOVERY_UI_H
+#define RECOVERY_UI_H
+
+#include <linux/input.h>
+#include <pthread.h>
+#include <time.h>
+
+// Abstract class for controlling the user interface during recovery.
+class RecoveryUI {
+  public:
+    RecoveryUI();
+
+    virtual ~RecoveryUI() { }
+
+    // Initialize the object; called before anything else.
+    virtual void Init();
+    // Show a stage indicator.  Call immediately after Init().
+    virtual void SetStage(int current, int max) = 0;
+
+    // After calling Init(), you can tell the UI what locale it is operating in.
+    virtual void SetLocale(const char* locale) = 0;
+
+    // Set the overall recovery state ("background image").
+    enum Icon { NONE, INSTALLING_UPDATE, ERASING, NO_COMMAND, ERROR };
+    virtual void SetBackground(Icon icon) = 0;
+
+    // --- progress indicator ---
+    enum ProgressType { EMPTY, INDETERMINATE, DETERMINATE };
+    virtual void SetProgressType(ProgressType determinate) = 0;
+
+    // Show a progress bar and define the scope of the next operation:
+    //   portion - fraction of the progress bar the next operation will use
+    //   seconds - expected time interval (progress bar moves at this minimum rate)
+    virtual void ShowProgress(float portion, float seconds) = 0;
+
+    // Set progress bar position (0.0 - 1.0 within the scope defined
+    // by the last call to ShowProgress).
+    virtual void SetProgress(float fraction) = 0;
+
+    // --- text log ---
+
+    virtual void ShowText(bool visible) = 0;
+
+    virtual bool IsTextVisible() = 0;
+
+    virtual bool WasTextEverVisible() = 0;
+
+    // Write a message to the on-screen log (shown if the user has
+    // toggled on the text display). Print() will also dump the message
+    // to stdout / log file, while PrintOnScreenOnly() not.
+    virtual void Print(const char* fmt, ...) __printflike(2, 3) = 0;
+    virtual void PrintOnScreenOnly(const char* fmt, ...) __printflike(2, 3) = 0;
+
+    virtual void ShowFile(const char* filename) = 0;
+
+    // --- key handling ---
+
+    // Wait for a key and return it.  May return -1 after timeout.
+    virtual int WaitKey();
+
+    virtual bool IsKeyPressed(int key);
+    virtual bool IsLongPress();
+
+    // Returns true if you have the volume up/down and power trio typical
+    // of phones and tablets, false otherwise.
+    virtual bool HasThreeButtons();
+
+    // Erase any queued-up keys.
+    virtual void FlushKeys();
+
+    // Called on each key press, even while operations are in progress.
+    // Return value indicates whether an immediate operation should be
+    // triggered (toggling the display, rebooting the device), or if
+    // the key should be enqueued for use by the main thread.
+    enum KeyAction { ENQUEUE, TOGGLE, REBOOT, IGNORE };
+    virtual KeyAction CheckKey(int key, bool is_long_press);
+
+    // Called when a key is held down long enough to have been a
+    // long-press (but before the key is released).  This means that
+    // if the key is eventually registered (released without any other
+    // keys being pressed in the meantime), CheckKey will be called with
+    // 'is_long_press' true.
+    virtual void KeyLongPress(int key);
+
+    // Normally in recovery there's a key sequence that triggers
+    // immediate reboot of the device, regardless of what recovery is
+    // doing (with the default CheckKey implementation, it's pressing
+    // the power button 7 times in row).  Call this to enable or
+    // disable that feature.  It is enabled by default.
+    virtual void SetEnableReboot(bool enabled);
+
+    // --- menu display ---
+
+    // Display some header text followed by a menu of items, which appears
+    // at the top of the screen (in place of any scrolling ui_print()
+    // output, if necessary).
+    virtual void StartMenu(const char* const * headers, const char* const * items,
+                           int initial_selection) = 0;
+
+    // Set the menu highlight to the given index, wrapping if necessary.
+    // Returns the actual item selected.
+    virtual int SelectMenu(int sel) = 0;
+
+    // End menu mode, resetting the text overlay so that ui_print()
+    // statements will be displayed.
+    virtual void EndMenu() = 0;
+
+protected:
+    void EnqueueKey(int key_code);
+
+private:
+    // Key event input queue
+    pthread_mutex_t key_queue_mutex;
+    pthread_cond_t key_queue_cond;
+    int key_queue[256], key_queue_len;
+    char key_pressed[KEY_MAX + 1];     // under key_queue_mutex
+    int key_last_down;                 // under key_queue_mutex
+    bool key_long_press;               // under key_queue_mutex
+    int key_down_count;                // under key_queue_mutex
+    bool enable_reboot;                // under key_queue_mutex
+    int rel_sum;
+
+    int consecutive_power_keys;
+    int last_key;
+
+    bool has_power_key;
+    bool has_up_key;
+    bool has_down_key;
+
+    struct key_timer_t {
+        RecoveryUI* ui;
+        int key_code;
+        int count;
+    };
+
+    pthread_t input_thread_;
+
+    void OnKeyDetected(int key_code);
+
+    static int InputCallback(int fd, uint32_t epevents, void* data);
+    int OnInputEvent(int fd, uint32_t epevents);
+    void ProcessKey(int key_code, int updown);
+
+    bool IsUsbConnected();
+
+    static void* time_key_helper(void* cookie);
+    void time_key(int key_code, int count);
+};
+
+#endif  // RECOVERY_UI_H
diff --git a/recovery/uncrypt/Android.mk b/recovery/uncrypt/Android.mk
new file mode 100644
index 0000000..6422cb2
--- /dev/null
+++ b/recovery/uncrypt/Android.mk
@@ -0,0 +1,31 @@
+# Copyright (C) 2014 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CLANG := true
+
+LOCAL_SRC_FILES := uncrypt.cpp
+
+LOCAL_C_INCLUDES := $(LOCAL_PATH)/..
+
+LOCAL_MODULE := uncrypt
+
+LOCAL_STATIC_LIBRARIES := libbase liblog libfs_mgr libcutils
+
+LOCAL_INIT_RC := uncrypt.rc
+
+include $(BUILD_EXECUTABLE)
diff --git a/recovery/uncrypt/uncrypt.cpp b/recovery/uncrypt/uncrypt.cpp
new file mode 100644
index 0000000..a1de6a1
--- /dev/null
+++ b/recovery/uncrypt/uncrypt.cpp
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+// This program takes a file on an ext4 filesystem and produces a list
+// of the blocks that file occupies, which enables the file contents
+// to be read directly from the block device without mounting the
+// filesystem.
+//
+// If the filesystem is using an encrypted block device, it will also
+// read the file and rewrite it to the same blocks of the underlying
+// (unencrypted) block device, so the file contents can be read
+// without the need for the decryption key.
+//
+// The output of this program is a "block map" which looks like this:
+//
+//     /dev/block/platform/msm_sdcc.1/by-name/userdata     # block device
+//     49652 4096                        # file size in bytes, block size
+//     3                                 # count of block ranges
+//     1000 1008                         # block range 0
+//     2100 2102                         # ... block range 1
+//     30 33                             # ... block range 2
+//
+// Each block range represents a half-open interval; the line "30 33"
+// reprents the blocks [30, 31, 32].
+//
+// Recovery can take this block map file and retrieve the underlying
+// file data to use as an update package.
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <libgen.h>
+#include <linux/fs.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <cutils/android_reboot.h>
+#include <cutils/properties.h>
+#include <fs_mgr.h>
+
+#define LOG_TAG "uncrypt"
+#include <log/log.h>
+
+#include "bootloader.h"
+
+#define WINDOW_SIZE 5
+
+static const std::string CACHE_BLOCK_MAP = "/cache/recovery/block.map";
+static const std::string COMMAND_FILE = "/cache/recovery/command";
+static const std::string STATUS_FILE = "/cache/recovery/uncrypt_status";
+static const std::string UNCRYPT_PATH_FILE = "/cache/recovery/uncrypt_file";
+
+static struct fstab* fstab = NULL;
+
+static int write_at_offset(unsigned char* buffer, size_t size, int wfd, off64_t offset) {
+    if (TEMP_FAILURE_RETRY(lseek64(wfd, offset, SEEK_SET)) == -1) {
+        ALOGE("error seeking to offset %" PRId64 ": %s", offset, strerror(errno));
+        return -1;
+    }
+    if (!android::base::WriteFully(wfd, buffer, size)) {
+        ALOGE("error writing offset %" PRId64 ": %s", offset, strerror(errno));
+        return -1;
+    }
+    return 0;
+}
+
+static void add_block_to_ranges(std::vector<int>& ranges, int new_block) {
+    if (!ranges.empty() && new_block == ranges.back()) {
+        // If the new block comes immediately after the current range,
+        // all we have to do is extend the current range.
+        ++ranges.back();
+    } else {
+        // We need to start a new range.
+        ranges.push_back(new_block);
+        ranges.push_back(new_block + 1);
+    }
+}
+
+static struct fstab* read_fstab() {
+    fstab = NULL;
+
+    // The fstab path is always "/fstab.${ro.hardware}".
+    char fstab_path[PATH_MAX+1] = "/fstab.";
+    if (!property_get("ro.hardware", fstab_path+strlen(fstab_path), "")) {
+        ALOGE("failed to get ro.hardware");
+        return NULL;
+    }
+
+    fstab = fs_mgr_read_fstab(fstab_path);
+    if (!fstab) {
+        ALOGE("failed to read %s", fstab_path);
+        return NULL;
+    }
+
+    return fstab;
+}
+
+static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted) {
+    // Look for a volume whose mount point is the prefix of path and
+    // return its block device.  Set encrypted if it's currently
+    // encrypted.
+    for (int i = 0; i < fstab->num_entries; ++i) {
+        struct fstab_rec* v = &fstab->recs[i];
+        if (!v->mount_point) {
+            continue;
+        }
+        int len = strlen(v->mount_point);
+        if (strncmp(path, v->mount_point, len) == 0 &&
+            (path[len] == '/' || path[len] == 0)) {
+            *encrypted = false;
+            *encryptable = false;
+            if (fs_mgr_is_encryptable(v) || fs_mgr_is_file_encrypted(v)) {
+                *encryptable = true;
+                char buffer[PROPERTY_VALUE_MAX+1];
+                if (property_get("ro.crypto.state", buffer, "") &&
+                    strcmp(buffer, "encrypted") == 0) {
+                    *encrypted = true;
+                }
+            }
+            return v->blk_device;
+        }
+    }
+
+    return NULL;
+}
+
+// Parse uncrypt_file to find the update package name.
+static bool find_uncrypt_package(const std::string& uncrypt_path_file, std::string* package_name) {
+    CHECK(package_name != nullptr);
+    std::string uncrypt_path;
+    if (!android::base::ReadFileToString(uncrypt_path_file, &uncrypt_path)) {
+        ALOGE("failed to open \"%s\": %s", uncrypt_path_file.c_str(), strerror(errno));
+        return false;
+    }
+
+    // Remove the trailing '\n' if present.
+    *package_name = android::base::Trim(uncrypt_path);
+    return true;
+}
+
+static int produce_block_map(const char* path, const char* map_file, const char* blk_dev,
+                             bool encrypted, int status_fd) {
+    std::string err;
+    if (!android::base::RemoveFileIfExists(map_file, &err)) {
+        ALOGE("failed to remove the existing map file %s: %s", map_file, err.c_str());
+        return -1;
+    }
+    std::string tmp_map_file = std::string(map_file) + ".tmp";
+    android::base::unique_fd mapfd(open(tmp_map_file.c_str(),
+                                        O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR));
+    if (mapfd == -1) {
+        ALOGE("failed to open %s: %s\n", tmp_map_file.c_str(), strerror(errno));
+        return -1;
+    }
+
+    // Make sure we can write to the status_file.
+    if (!android::base::WriteStringToFd("0\n", status_fd)) {
+        ALOGE("failed to update \"%s\"\n", STATUS_FILE.c_str());
+        return -1;
+    }
+
+    struct stat sb;
+    if (stat(path, &sb) != 0) {
+        ALOGE("failed to stat %s", path);
+        return -1;
+    }
+
+    ALOGI(" block size: %ld bytes", static_cast<long>(sb.st_blksize));
+
+    int blocks = ((sb.st_size-1) / sb.st_blksize) + 1;
+    ALOGI("  file size: %" PRId64 " bytes, %d blocks", sb.st_size, blocks);
+
+    std::vector<int> ranges;
+
+    std::string s = android::base::StringPrintf("%s\n%" PRId64 " %" PRId64 "\n",
+                       blk_dev, static_cast<int64_t>(sb.st_size),
+                       static_cast<int64_t>(sb.st_blksize));
+    if (!android::base::WriteStringToFd(s, mapfd)) {
+        ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
+        return -1;
+    }
+
+    std::vector<std::vector<unsigned char>> buffers;
+    if (encrypted) {
+        buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize));
+    }
+    int head_block = 0;
+    int head = 0, tail = 0;
+
+    android::base::unique_fd fd(open(path, O_RDONLY));
+    if (fd == -1) {
+        ALOGE("failed to open %s for reading: %s", path, strerror(errno));
+        return -1;
+    }
+
+    android::base::unique_fd wfd;
+    if (encrypted) {
+        wfd.reset(open(blk_dev, O_WRONLY));
+        if (wfd == -1) {
+            ALOGE("failed to open fd for writing: %s", strerror(errno));
+            return -1;
+        }
+    }
+
+    off64_t pos = 0;
+    int last_progress = 0;
+    while (pos < sb.st_size) {
+        // Update the status file, progress must be between [0, 99].
+        int progress = static_cast<int>(100 * (double(pos) / double(sb.st_size)));
+        if (progress > last_progress) {
+          last_progress = progress;
+          android::base::WriteStringToFd(std::to_string(progress) + "\n", status_fd);
+        }
+
+        if ((tail+1) % WINDOW_SIZE == head) {
+            // write out head buffer
+            int block = head_block;
+            if (ioctl(fd, FIBMAP, &block) != 0) {
+                ALOGE("failed to find block %d", head_block);
+                return -1;
+            }
+            add_block_to_ranges(ranges, block);
+            if (encrypted) {
+                if (write_at_offset(buffers[head].data(), sb.st_blksize, wfd,
+                                    static_cast<off64_t>(sb.st_blksize) * block) != 0) {
+                    return -1;
+                }
+            }
+            head = (head + 1) % WINDOW_SIZE;
+            ++head_block;
+        }
+
+        // read next block to tail
+        if (encrypted) {
+            size_t to_read = static_cast<size_t>(
+                    std::min(static_cast<off64_t>(sb.st_blksize), sb.st_size - pos));
+            if (!android::base::ReadFully(fd, buffers[tail].data(), to_read)) {
+                ALOGE("failed to read: %s", strerror(errno));
+                return -1;
+            }
+            pos += to_read;
+        } else {
+            // If we're not encrypting; we don't need to actually read
+            // anything, just skip pos forward as if we'd read a
+            // block.
+            pos += sb.st_blksize;
+        }
+        tail = (tail+1) % WINDOW_SIZE;
+    }
+
+    while (head != tail) {
+        // write out head buffer
+        int block = head_block;
+        if (ioctl(fd, FIBMAP, &block) != 0) {
+            ALOGE("failed to find block %d", head_block);
+            return -1;
+        }
+        add_block_to_ranges(ranges, block);
+        if (encrypted) {
+            if (write_at_offset(buffers[head].data(), sb.st_blksize, wfd,
+                                static_cast<off64_t>(sb.st_blksize) * block) != 0) {
+                return -1;
+            }
+        }
+        head = (head + 1) % WINDOW_SIZE;
+        ++head_block;
+    }
+
+    if (!android::base::WriteStringToFd(
+            android::base::StringPrintf("%zu\n", ranges.size() / 2), mapfd)) {
+        ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
+        return -1;
+    }
+    for (size_t i = 0; i < ranges.size(); i += 2) {
+        if (!android::base::WriteStringToFd(
+                android::base::StringPrintf("%d %d\n", ranges[i], ranges[i+1]), mapfd)) {
+            ALOGE("failed to write %s: %s", tmp_map_file.c_str(), strerror(errno));
+            return -1;
+        }
+    }
+
+    if (fsync(mapfd) == -1) {
+        ALOGE("failed to fsync \"%s\": %s", tmp_map_file.c_str(), strerror(errno));
+        return -1;
+    }
+    if (close(mapfd.release()) == -1) {
+        ALOGE("failed to close %s: %s", tmp_map_file.c_str(), strerror(errno));
+        return -1;
+    }
+
+    if (encrypted) {
+        if (fsync(wfd) == -1) {
+            ALOGE("failed to fsync \"%s\": %s", blk_dev, strerror(errno));
+            return -1;
+        }
+        if (close(wfd.release()) == -1) {
+            ALOGE("failed to close %s: %s", blk_dev, strerror(errno));
+            return -1;
+        }
+    }
+
+    if (rename(tmp_map_file.c_str(), map_file) == -1) {
+        ALOGE("failed to rename %s to %s: %s", tmp_map_file.c_str(), map_file, strerror(errno));
+        return -1;
+    }
+    // Sync dir to make rename() result written to disk.
+    std::string file_name = map_file;
+    std::string dir_name = dirname(&file_name[0]);
+    android::base::unique_fd dfd(open(dir_name.c_str(), O_RDONLY | O_DIRECTORY));
+    if (dfd == -1) {
+        ALOGE("failed to open dir %s: %s", dir_name.c_str(), strerror(errno));
+        return -1;
+    }
+    if (fsync(dfd) == -1) {
+        ALOGE("failed to fsync %s: %s", dir_name.c_str(), strerror(errno));
+        return -1;
+    }
+    if (close(dfd.release()) == -1) {
+        ALOGE("failed to close %s: %s", dir_name.c_str(), strerror(errno));
+        return -1;
+    }
+    return 0;
+}
+
+static std::string get_misc_blk_device() {
+    struct fstab* fstab = read_fstab();
+    if (fstab == nullptr) {
+        return "";
+    }
+    for (int i = 0; i < fstab->num_entries; ++i) {
+        fstab_rec* v = &fstab->recs[i];
+        if (v->mount_point != nullptr && strcmp(v->mount_point, "/misc") == 0) {
+            return v->blk_device;
+        }
+    }
+    return "";
+}
+
+static int write_bootloader_message(const bootloader_message* in) {
+    std::string misc_blk_device = get_misc_blk_device();
+    if (misc_blk_device.empty()) {
+        ALOGE("failed to find /misc partition.");
+        return -1;
+    }
+    android::base::unique_fd fd(open(misc_blk_device.c_str(), O_WRONLY | O_SYNC));
+    if (fd == -1) {
+        ALOGE("failed to open %s: %s", misc_blk_device.c_str(), strerror(errno));
+        return -1;
+    }
+    if (!android::base::WriteFully(fd, in, sizeof(*in))) {
+        ALOGE("failed to write %s: %s", misc_blk_device.c_str(), strerror(errno));
+        return -1;
+    }
+    // TODO: O_SYNC and fsync() duplicates each other?
+    if (fsync(fd) == -1) {
+        ALOGE("failed to fsync %s: %s", misc_blk_device.c_str(), strerror(errno));
+        return -1;
+    }
+    return 0;
+}
+
+static void reboot_to_recovery() {
+    ALOGI("rebooting to recovery");
+    property_set("sys.powerctl", "reboot,recovery");
+    while (true) {
+      pause();
+    }
+    ALOGE("reboot didn't succeed?");
+}
+
+static int uncrypt(const char* input_path, const char* map_file, int status_fd) {
+
+    ALOGI("update package is \"%s\"", input_path);
+
+    // Turn the name of the file we're supposed to convert into an
+    // absolute path, so we can find what filesystem it's on.
+    char path[PATH_MAX+1];
+    if (realpath(input_path, path) == NULL) {
+        ALOGE("failed to convert \"%s\" to absolute path: %s", input_path, strerror(errno));
+        return 1;
+    }
+
+    if (read_fstab() == NULL) {
+        return 1;
+    }
+
+    bool encryptable;
+    bool encrypted;
+    const char* blk_dev = find_block_device(path, &encryptable, &encrypted);
+    if (blk_dev == NULL) {
+        ALOGE("failed to find block device for %s", path);
+        return 1;
+    }
+
+    // If the filesystem it's on isn't encrypted, we only produce the
+    // block map, we don't rewrite the file contents (it would be
+    // pointless to do so).
+    ALOGI("encryptable: %s", encryptable ? "yes" : "no");
+    ALOGI("  encrypted: %s", encrypted ? "yes" : "no");
+
+    // Recovery supports installing packages from 3 paths: /cache,
+    // /data, and /sdcard.  (On a particular device, other locations
+    // may work, but those are three we actually expect.)
+    //
+    // On /data we want to convert the file to a block map so that we
+    // can read the package without mounting the partition.  On /cache
+    // and /sdcard we leave the file alone.
+    if (strncmp(path, "/data/", 6) == 0) {
+        ALOGI("writing block map %s", map_file);
+        if (produce_block_map(path, map_file, blk_dev, encrypted, status_fd) != 0) {
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+static int uncrypt_wrapper(const char* input_path, const char* map_file,
+                           const std::string& status_file) {
+    // The pipe has been created by the system server.
+    android::base::unique_fd status_fd(open(status_file.c_str(),
+                                            O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR));
+    if (status_fd == -1) {
+        ALOGE("failed to open pipe \"%s\": %s", status_file.c_str(), strerror(errno));
+        return 1;
+    }
+
+    std::string package;
+    if (input_path == nullptr) {
+        if (!find_uncrypt_package(UNCRYPT_PATH_FILE, &package)) {
+            android::base::WriteStringToFd("-1\n", status_fd);
+            return 1;
+        }
+        input_path = package.c_str();
+    }
+    CHECK(map_file != nullptr);
+    int status = uncrypt(input_path, map_file, status_fd);
+    if (status != 0) {
+        android::base::WriteStringToFd("-1\n", status_fd);
+        return 1;
+    }
+    android::base::WriteStringToFd("100\n", status_fd);
+    return 0;
+}
+
+static int clear_bcb(const std::string& status_file) {
+    android::base::unique_fd status_fd(open(status_file.c_str(),
+                                            O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR));
+    if (status_fd == -1) {
+        ALOGE("failed to open pipe \"%s\": %s", status_file.c_str(), strerror(errno));
+        return 1;
+    }
+    bootloader_message boot = {};
+    if (write_bootloader_message(&boot) != 0) {
+        android::base::WriteStringToFd("-1\n", status_fd);
+        return 1;
+    }
+    android::base::WriteStringToFd("100\n", status_fd);
+    return 0;
+}
+
+static int setup_bcb(const std::string& command_file, const std::string& status_file) {
+    android::base::unique_fd status_fd(open(status_file.c_str(),
+                                            O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR));
+    if (status_fd == -1) {
+        ALOGE("failed to open pipe \"%s\": %s", status_file.c_str(), strerror(errno));
+        return 1;
+    }
+    std::string content;
+    if (!android::base::ReadFileToString(command_file, &content)) {
+        ALOGE("failed to read \"%s\": %s", command_file.c_str(), strerror(errno));
+        android::base::WriteStringToFd("-1\n", status_fd);
+        return 1;
+    }
+    bootloader_message boot = {};
+    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
+    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
+    strlcat(boot.recovery, content.c_str(), sizeof(boot.recovery));
+    if (write_bootloader_message(&boot) != 0) {
+        ALOGE("failed to set bootloader message");
+        android::base::WriteStringToFd("-1\n", status_fd);
+        return 1;
+    }
+    android::base::WriteStringToFd("100\n", status_fd);
+    return 0;
+}
+
+static void usage(const char* exename) {
+    fprintf(stderr, "Usage of %s:\n", exename);
+    fprintf(stderr, "%s [<package_path> <map_file>]  Uncrypt ota package.\n", exename);
+    fprintf(stderr, "%s --reboot  Clear BCB data and reboot to recovery.\n", exename);
+    fprintf(stderr, "%s --clear-bcb  Clear BCB data in misc partition.\n", exename);
+    fprintf(stderr, "%s --setup-bcb  Setup BCB data by command file.\n", exename);
+}
+
+int main(int argc, char** argv) {
+    if (argc == 2) {
+        if (strcmp(argv[1], "--reboot") == 0) {
+            reboot_to_recovery();
+        } else if (strcmp(argv[1], "--clear-bcb") == 0) {
+            return clear_bcb(STATUS_FILE);
+        } else if (strcmp(argv[1], "--setup-bcb") == 0) {
+            return setup_bcb(COMMAND_FILE, STATUS_FILE);
+        }
+    } else if (argc == 1 || argc == 3) {
+        const char* input_path = nullptr;
+        const char* map_file = CACHE_BLOCK_MAP.c_str();
+        if (argc == 3) {
+            input_path = argv[1];
+            map_file = argv[2];
+        }
+        return uncrypt_wrapper(input_path, map_file, STATUS_FILE);
+    }
+    usage(argv[0]);
+    return 2;
+}
diff --git a/recovery/uncrypt/uncrypt.rc b/recovery/uncrypt/uncrypt.rc
new file mode 100644
index 0000000..b07c1da
--- /dev/null
+++ b/recovery/uncrypt/uncrypt.rc
@@ -0,0 +1,19 @@
+service uncrypt /system/bin/uncrypt
+    class main
+    disabled
+    oneshot
+
+service pre-recovery /system/bin/uncrypt --reboot
+    class main
+    disabled
+    oneshot
+
+service setup-bcb /system/bin/uncrypt --setup-bcb
+    class main
+    disabled
+    oneshot
+
+service clear-bcb /system/bin/uncrypt --clear-bcb
+    class main
+    disabled
+    oneshot
\ No newline at end of file
diff --git a/recovery/update_verifier/Android.mk b/recovery/update_verifier/Android.mk
new file mode 100644
index 0000000..7f28bce
--- /dev/null
+++ b/recovery/update_verifier/Android.mk
@@ -0,0 +1,24 @@
+# Copyright (C) 2015 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := update_verifier.cpp
+LOCAL_MODULE := update_verifier
+LOCAL_SHARED_LIBRARIES := libhardware liblog
+
+include $(BUILD_EXECUTABLE)
diff --git a/recovery/update_verifier/update_verifier.cpp b/recovery/update_verifier/update_verifier.cpp
new file mode 100644
index 0000000..be70cec
--- /dev/null
+++ b/recovery/update_verifier/update_verifier.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+/*
+ * This program verifies the integrity of the partitions after an A/B OTA
+ * update. It gets invoked by init, and will only perform the verification if
+ * it's the first boot post an A/B OTA update.
+ *
+ * It relies on dm-verity to capture any corruption on the partitions being
+ * verified. dm-verity must be in enforcing mode, so that it will reboot the
+ * device on dm-verity failures. When that happens, the bootloader should
+ * mark the slot as unbootable and stops trying. We should never see a device
+ * started in dm-verity logging mode but with isSlotMarkedSuccessful equals to
+ * 0.
+ *
+ * The current slot will be marked as having booted successfully if the
+ * verifier reaches the end after the verification.
+ *
+ * TODO: The actual verification part will be added later after we have the
+ * A/B OTA package format in place.
+ */
+
+#include <string.h>
+
+#include <hardware/boot_control.h>
+
+#define LOG_TAG       "update_verifier"
+#include <log/log.h>
+
+int main(int argc, char** argv) {
+  for (int i = 1; i < argc; i++) {
+    SLOGI("Started with arg %d: %s\n", i, argv[i]);
+  }
+
+  const hw_module_t* hw_module;
+  if (hw_get_module("bootctrl", &hw_module) != 0) {
+    SLOGE("Error getting bootctrl module.\n");
+    return -1;
+  }
+
+  boot_control_module_t* module = reinterpret_cast<boot_control_module_t*>(
+      const_cast<hw_module_t*>(hw_module));
+  module->init(module);
+
+  unsigned current_slot = module->getCurrentSlot(module);
+  int is_successful= module->isSlotMarkedSuccessful(module, current_slot);
+  SLOGI("Booting slot %u: isSlotMarkedSuccessful=%d\n", current_slot, is_successful);
+
+  if (is_successful == 0) {
+    // The current slot has not booted successfully.
+
+    // TODO: Add the actual verification after we have the A/B OTA package
+    // format in place.
+
+    // TODO: Assert the dm-verity mode. Bootloader should never boot a newly
+    // flashed slot (isSlotMarkedSuccessful == 0) with dm-verity logging mode.
+
+    int ret = module->markBootSuccessful(module);
+    if (ret != 0) {
+      SLOGE("Error marking booted successfully: %s\n", strerror(-ret));
+      return -1;
+    }
+    SLOGI("Marked slot %u as booted successfully.\n", current_slot);
+  }
+
+  SLOGI("Leaving update_verifier.\n");
+  return 0;
+}
diff --git a/recovery/updater/Android.mk b/recovery/updater/Android.mk
new file mode 100644
index 0000000..b4d427c
--- /dev/null
+++ b/recovery/updater/Android.mk
@@ -0,0 +1,115 @@
+# Copyright 2009 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# updater (static executable)
+# ===============================
+# Build a statically-linked binary to include in OTA packages.
+include $(CLEAR_VARS)
+
+updater_src_files := \
+    install.cpp \
+    blockimg.cpp \
+    updater.cpp
+
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := $(updater_src_files)
+
+LOCAL_STATIC_LIBRARIES += \
+    $(TARGET_RECOVERY_UPDATER_LIBS) \
+    $(TARGET_RECOVERY_UPDATER_EXTRA_LIBS) \
+    libfec \
+    libfec_rs \
+    libext4_utils_static \
+    libsquashfs_utils \
+    libcrypto_utils \
+    libcrypto \
+    libapplypatch \
+    libbase \
+    libotafault \
+    libedify \
+    libminzip \
+    libmounts \
+    libz \
+    libbz \
+    libcutils \
+    liblog \
+    libselinux
+
+tune2fs_static_libraries := \
+    libext2_com_err \
+    libext2_blkid \
+    libext2_quota \
+    libext2_uuid_static \
+    libext2_e2p \
+    libext2fs
+
+LOCAL_STATIC_LIBRARIES += \
+    libtune2fs \
+    $(tune2fs_static_libraries)
+
+LOCAL_CFLAGS += -Wno-unused-parameter
+LOCAL_C_INCLUDES += system/extras/ext4_utils
+LOCAL_STATIC_LIBRARIES += \
+    libsparse_static \
+    libz
+
+LOCAL_C_INCLUDES += external/e2fsprogs/misc
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/..
+
+# Each library in TARGET_RECOVERY_UPDATER_LIBS should have a function
+# named "Register_<libname>()".  Here we emit a little C function that
+# gets #included by updater.c.  It calls all those registration
+# functions.
+
+# Devices can also add libraries to TARGET_RECOVERY_UPDATER_EXTRA_LIBS.
+# These libs are also linked in with updater, but we don't try to call
+# any sort of registration function for these.  Use this variable for
+# any subsidiary static libraries required for your registered
+# extension libs.
+
+inc := $(call intermediates-dir-for,PACKAGING,updater_extensions)/register.inc
+
+# Encode the value of TARGET_RECOVERY_UPDATER_LIBS into the filename of the dependency.
+# So if TARGET_RECOVERY_UPDATER_LIBS is changed, a new dependency file will be generated.
+# Note that we have to remove any existing depency files before creating new one,
+# so no obsolete dependecy file gets used if you switch back to an old value.
+inc_dep_file := $(inc).dep.$(subst $(space),-,$(sort $(TARGET_RECOVERY_UPDATER_LIBS)))
+$(inc_dep_file): stem := $(inc).dep
+$(inc_dep_file) :
+	$(hide) mkdir -p $(dir $@)
+	$(hide) rm -f $(stem).*
+	$(hide) touch $@
+
+$(inc) : libs := $(TARGET_RECOVERY_UPDATER_LIBS)
+$(inc) : $(inc_dep_file)
+	$(hide) mkdir -p $(dir $@)
+	$(hide) echo "" > $@
+	$(hide) $(foreach lib,$(libs),echo "extern void Register_$(lib)(void);" >> $@;)
+	$(hide) echo "void RegisterDeviceExtensions() {" >> $@
+	$(hide) $(foreach lib,$(libs),echo "  Register_$(lib)();" >> $@;)
+	$(hide) echo "}" >> $@
+
+$(call intermediates-dir-for,EXECUTABLES,updater,,,$(TARGET_PREFER_32_BIT))/updater.o : $(inc)
+LOCAL_C_INCLUDES += $(dir $(inc))
+
+inc :=
+inc_dep_file :=
+
+LOCAL_MODULE := updater
+
+LOCAL_FORCE_STATIC_EXECUTABLE := true
+
+include $(BUILD_EXECUTABLE)
diff --git a/recovery/updater/blockimg.cpp b/recovery/updater/blockimg.cpp
new file mode 100644
index 0000000..12a549d
--- /dev/null
+++ b/recovery/updater/blockimg.cpp
@@ -0,0 +1,1819 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/fs.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/ioctl.h>
+#include <time.h>
+#include <unistd.h>
+#include <fec/io.h>
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+
+#include "applypatch/applypatch.h"
+#include "edify/expr.h"
+#include "install.h"
+#include "openssl/sha.h"
+#include "minzip/Hash.h"
+#include "ota_io.h"
+#include "print_sha1.h"
+#include "updater.h"
+
+#define BLOCKSIZE 4096
+
+// Set this to 0 to interpret 'erase' transfers to mean do a
+// BLKDISCARD ioctl (the normal behavior).  Set to 1 to interpret
+// erase to mean fill the region with zeroes.
+#define DEBUG_ERASE  0
+
+#define STASH_DIRECTORY_BASE "/cache/recovery"
+#define STASH_DIRECTORY_MODE 0700
+#define STASH_FILE_MODE 0600
+
+struct RangeSet {
+    size_t count;             // Limit is INT_MAX.
+    size_t size;
+    std::vector<size_t> pos;  // Actual limit is INT_MAX.
+};
+
+static std::map<std::string, RangeSet> stash_map;
+
+static void parse_range(const std::string& range_text, RangeSet& rs) {
+
+    std::vector<std::string> pieces = android::base::Split(range_text, ",");
+    if (pieces.size() < 3) {
+        goto err;
+    }
+
+    size_t num;
+    if (!android::base::ParseUint(pieces[0].c_str(), &num, static_cast<size_t>(INT_MAX))) {
+        goto err;
+    }
+
+    if (num == 0 || num % 2) {
+        goto err; // must be even
+    } else if (num != pieces.size() - 1) {
+        goto err;
+    }
+
+    rs.pos.resize(num);
+    rs.count = num / 2;
+    rs.size = 0;
+
+    for (size_t i = 0; i < num; i += 2) {
+        if (!android::base::ParseUint(pieces[i+1].c_str(), &rs.pos[i],
+                                      static_cast<size_t>(INT_MAX))) {
+            goto err;
+        }
+
+        if (!android::base::ParseUint(pieces[i+2].c_str(), &rs.pos[i+1],
+                                      static_cast<size_t>(INT_MAX))) {
+            goto err;
+        }
+
+        if (rs.pos[i] >= rs.pos[i+1]) {
+            goto err; // empty or negative range
+        }
+
+        size_t sz = rs.pos[i+1] - rs.pos[i];
+        if (rs.size > SIZE_MAX - sz) {
+            goto err; // overflow
+        }
+
+        rs.size += sz;
+    }
+
+    return;
+
+err:
+    fprintf(stderr, "failed to parse range '%s'\n", range_text.c_str());
+    exit(1);
+}
+
+static bool range_overlaps(const RangeSet& r1, const RangeSet& r2) {
+    for (size_t i = 0; i < r1.count; ++i) {
+        size_t r1_0 = r1.pos[i * 2];
+        size_t r1_1 = r1.pos[i * 2 + 1];
+
+        for (size_t j = 0; j < r2.count; ++j) {
+            size_t r2_0 = r2.pos[j * 2];
+            size_t r2_1 = r2.pos[j * 2 + 1];
+
+            if (!(r2_0 >= r1_1 || r1_0 >= r2_1)) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+static int read_all(int fd, uint8_t* data, size_t size) {
+    size_t so_far = 0;
+    while (so_far < size) {
+        ssize_t r = TEMP_FAILURE_RETRY(ota_read(fd, data+so_far, size-so_far));
+        if (r == -1) {
+            fprintf(stderr, "read failed: %s\n", strerror(errno));
+            return -1;
+        }
+        so_far += r;
+    }
+    return 0;
+}
+
+static int read_all(int fd, std::vector<uint8_t>& buffer, size_t size) {
+    return read_all(fd, buffer.data(), size);
+}
+
+static int write_all(int fd, const uint8_t* data, size_t size) {
+    size_t written = 0;
+    while (written < size) {
+        ssize_t w = TEMP_FAILURE_RETRY(ota_write(fd, data+written, size-written));
+        if (w == -1) {
+            fprintf(stderr, "write failed: %s\n", strerror(errno));
+            return -1;
+        }
+        written += w;
+    }
+
+    return 0;
+}
+
+static int write_all(int fd, const std::vector<uint8_t>& buffer, size_t size) {
+    return write_all(fd, buffer.data(), size);
+}
+
+static bool check_lseek(int fd, off64_t offset, int whence) {
+    off64_t rc = TEMP_FAILURE_RETRY(lseek64(fd, offset, whence));
+    if (rc == -1) {
+        fprintf(stderr, "lseek64 failed: %s\n", strerror(errno));
+        return false;
+    }
+    return true;
+}
+
+static void allocate(size_t size, std::vector<uint8_t>& buffer) {
+    // if the buffer's big enough, reuse it.
+    if (size <= buffer.size()) return;
+
+    buffer.resize(size);
+}
+
+struct RangeSinkState {
+    explicit RangeSinkState(RangeSet& rs) : tgt(rs) { };
+
+    int fd;
+    const RangeSet& tgt;
+    size_t p_block;
+    size_t p_remain;
+};
+
+static ssize_t RangeSinkWrite(const uint8_t* data, ssize_t size, void* token) {
+    RangeSinkState* rss = reinterpret_cast<RangeSinkState*>(token);
+
+    if (rss->p_remain == 0) {
+        fprintf(stderr, "range sink write overrun");
+        return 0;
+    }
+
+    ssize_t written = 0;
+    while (size > 0) {
+        size_t write_now = size;
+
+        if (rss->p_remain < write_now) {
+            write_now = rss->p_remain;
+        }
+
+        if (write_all(rss->fd, data, write_now) == -1) {
+            break;
+        }
+
+        data += write_now;
+        size -= write_now;
+
+        rss->p_remain -= write_now;
+        written += write_now;
+
+        if (rss->p_remain == 0) {
+            // move to the next block
+            ++rss->p_block;
+            if (rss->p_block < rss->tgt.count) {
+                rss->p_remain = (rss->tgt.pos[rss->p_block * 2 + 1] -
+                                 rss->tgt.pos[rss->p_block * 2]) * BLOCKSIZE;
+
+                if (!check_lseek(rss->fd, (off64_t)rss->tgt.pos[rss->p_block*2] * BLOCKSIZE,
+                                 SEEK_SET)) {
+                    break;
+                }
+            } else {
+                // we can't write any more; return how many bytes have
+                // been written so far.
+                break;
+            }
+        }
+    }
+
+    return written;
+}
+
+// All of the data for all the 'new' transfers is contained in one
+// file in the update package, concatenated together in the order in
+// which transfers.list will need it.  We want to stream it out of the
+// archive (it's compressed) without writing it to a temp file, but we
+// can't write each section until it's that transfer's turn to go.
+//
+// To achieve this, we expand the new data from the archive in a
+// background thread, and block that threads 'receive uncompressed
+// data' function until the main thread has reached a point where we
+// want some new data to be written.  We signal the background thread
+// with the destination for the data and block the main thread,
+// waiting for the background thread to complete writing that section.
+// Then it signals the main thread to wake up and goes back to
+// blocking waiting for a transfer.
+//
+// NewThreadInfo is the struct used to pass information back and forth
+// between the two threads.  When the main thread wants some data
+// written, it sets rss to the destination location and signals the
+// condition.  When the background thread is done writing, it clears
+// rss and signals the condition again.
+
+struct NewThreadInfo {
+    ZipArchive* za;
+    const ZipEntry* entry;
+
+    RangeSinkState* rss;
+
+    pthread_mutex_t mu;
+    pthread_cond_t cv;
+};
+
+static bool receive_new_data(const unsigned char* data, int size, void* cookie) {
+    NewThreadInfo* nti = reinterpret_cast<NewThreadInfo*>(cookie);
+
+    while (size > 0) {
+        // Wait for nti->rss to be non-null, indicating some of this
+        // data is wanted.
+        pthread_mutex_lock(&nti->mu);
+        while (nti->rss == nullptr) {
+            pthread_cond_wait(&nti->cv, &nti->mu);
+        }
+        pthread_mutex_unlock(&nti->mu);
+
+        // At this point nti->rss is set, and we own it.  The main
+        // thread is waiting for it to disappear from nti.
+        ssize_t written = RangeSinkWrite(data, size, nti->rss);
+        data += written;
+        size -= written;
+
+        if (nti->rss->p_block == nti->rss->tgt.count) {
+            // we have written all the bytes desired by this rss.
+
+            pthread_mutex_lock(&nti->mu);
+            nti->rss = nullptr;
+            pthread_cond_broadcast(&nti->cv);
+            pthread_mutex_unlock(&nti->mu);
+        }
+    }
+
+    return true;
+}
+
+static void* unzip_new_data(void* cookie) {
+    NewThreadInfo* nti = (NewThreadInfo*) cookie;
+    mzProcessZipEntryContents(nti->za, nti->entry, receive_new_data, nti);
+    return nullptr;
+}
+
+static int ReadBlocks(const RangeSet& src, std::vector<uint8_t>& buffer, int fd) {
+    size_t p = 0;
+    uint8_t* data = buffer.data();
+
+    for (size_t i = 0; i < src.count; ++i) {
+        if (!check_lseek(fd, (off64_t) src.pos[i * 2] * BLOCKSIZE, SEEK_SET)) {
+            return -1;
+        }
+
+        size_t size = (src.pos[i * 2 + 1] - src.pos[i * 2]) * BLOCKSIZE;
+
+        if (read_all(fd, data + p, size) == -1) {
+            return -1;
+        }
+
+        p += size;
+    }
+
+    return 0;
+}
+
+static int WriteBlocks(const RangeSet& tgt, const std::vector<uint8_t>& buffer, int fd) {
+    const uint8_t* data = buffer.data();
+
+    size_t p = 0;
+    for (size_t i = 0; i < tgt.count; ++i) {
+        if (!check_lseek(fd, (off64_t) tgt.pos[i * 2] * BLOCKSIZE, SEEK_SET)) {
+            return -1;
+        }
+
+        size_t size = (tgt.pos[i * 2 + 1] - tgt.pos[i * 2]) * BLOCKSIZE;
+
+        if (write_all(fd, data + p, size) == -1) {
+            return -1;
+        }
+
+        p += size;
+    }
+
+    return 0;
+}
+
+// Parameters for transfer list command functions
+struct CommandParameters {
+    std::vector<std::string> tokens;
+    size_t cpos;
+    const char* cmdname;
+    const char* cmdline;
+    std::string freestash;
+    std::string stashbase;
+    bool canwrite;
+    int createdstash;
+    android::base::unique_fd fd;
+    bool foundwrites;
+    bool isunresumable;
+    int version;
+    size_t written;
+    NewThreadInfo nti;
+    pthread_t thread;
+    std::vector<uint8_t> buffer;
+    uint8_t* patch_start;
+};
+
+// Do a source/target load for move/bsdiff/imgdiff in version 1.
+// We expect to parse the remainder of the parameter tokens as:
+//
+//    <src_range> <tgt_range>
+//
+// The source range is loaded into the provided buffer, reallocating
+// it to make it larger if necessary.
+
+static int LoadSrcTgtVersion1(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
+        std::vector<uint8_t>& buffer, int fd) {
+
+    if (params.cpos + 1 >= params.tokens.size()) {
+        fprintf(stderr, "invalid parameters\n");
+        return -1;
+    }
+
+    // <src_range>
+    RangeSet src;
+    parse_range(params.tokens[params.cpos++], src);
+
+    // <tgt_range>
+    parse_range(params.tokens[params.cpos++], tgt);
+
+    allocate(src.size * BLOCKSIZE, buffer);
+    int rc = ReadBlocks(src, buffer, fd);
+    src_blocks = src.size;
+
+    return rc;
+}
+
+static int VerifyBlocks(const std::string& expected, const std::vector<uint8_t>& buffer,
+        const size_t blocks, bool printerror) {
+    uint8_t digest[SHA_DIGEST_LENGTH];
+    const uint8_t* data = buffer.data();
+
+    SHA1(data, blocks * BLOCKSIZE, digest);
+
+    std::string hexdigest = print_sha1(digest);
+
+    if (hexdigest != expected) {
+        if (printerror) {
+            fprintf(stderr, "failed to verify blocks (expected %s, read %s)\n",
+                    expected.c_str(), hexdigest.c_str());
+        }
+        return -1;
+    }
+
+    return 0;
+}
+
+static std::string GetStashFileName(const std::string& base, const std::string& id,
+        const std::string& postfix) {
+    if (base.empty()) {
+        return "";
+    }
+
+    std::string fn(STASH_DIRECTORY_BASE);
+    fn += "/" + base + "/" + id + postfix;
+
+    return fn;
+}
+
+typedef void (*StashCallback)(const std::string&, void*);
+
+// Does a best effort enumeration of stash files. Ignores possible non-file
+// items in the stash directory and continues despite of errors. Calls the
+// 'callback' function for each file and passes 'data' to the function as a
+// parameter.
+
+static void EnumerateStash(const std::string& dirname, StashCallback callback, void* data) {
+    if (dirname.empty() || callback == nullptr) {
+        return;
+    }
+
+    std::unique_ptr<DIR, int(*)(DIR*)> directory(opendir(dirname.c_str()), closedir);
+
+    if (directory == nullptr) {
+        if (errno != ENOENT) {
+            fprintf(stderr, "opendir \"%s\" failed: %s\n", dirname.c_str(), strerror(errno));
+        }
+        return;
+    }
+
+    struct dirent* item;
+    while ((item = readdir(directory.get())) != nullptr) {
+        if (item->d_type != DT_REG) {
+            continue;
+        }
+
+        std::string fn = dirname + "/" + std::string(item->d_name);
+        callback(fn, data);
+    }
+}
+
+static void UpdateFileSize(const std::string& fn, void* data) {
+    if (fn.empty() || !data) {
+        return;
+    }
+
+    struct stat sb;
+    if (stat(fn.c_str(), &sb) == -1) {
+        fprintf(stderr, "stat \"%s\" failed: %s\n", fn.c_str(), strerror(errno));
+        return;
+    }
+
+    int* size = reinterpret_cast<int*>(data);
+    *size += sb.st_size;
+}
+
+// Deletes the stash directory and all files in it. Assumes that it only
+// contains files. There is nothing we can do about unlikely, but possible
+// errors, so they are merely logged.
+
+static void DeleteFile(const std::string& fn, void* /* data */) {
+    if (!fn.empty()) {
+        fprintf(stderr, "deleting %s\n", fn.c_str());
+
+        if (unlink(fn.c_str()) == -1 && errno != ENOENT) {
+            fprintf(stderr, "unlink \"%s\" failed: %s\n", fn.c_str(), strerror(errno));
+        }
+    }
+}
+
+static void DeletePartial(const std::string& fn, void* data) {
+    if (android::base::EndsWith(fn, ".partial")) {
+        DeleteFile(fn, data);
+    }
+}
+
+static void DeleteStash(const std::string& base) {
+    if (base.empty()) {
+        return;
+    }
+
+    fprintf(stderr, "deleting stash %s\n", base.c_str());
+
+    std::string dirname = GetStashFileName(base, "", "");
+    EnumerateStash(dirname, DeleteFile, nullptr);
+
+    if (rmdir(dirname.c_str()) == -1) {
+        if (errno != ENOENT && errno != ENOTDIR) {
+            fprintf(stderr, "rmdir \"%s\" failed: %s\n", dirname.c_str(), strerror(errno));
+        }
+    }
+}
+
+static int LoadStash(CommandParameters& params, const std::string& base, const std::string& id,
+        bool verify, size_t* blocks, std::vector<uint8_t>& buffer, bool printnoent) {
+    // In verify mode, if source range_set was saved for the given hash,
+    // check contents in the source blocks first. If the check fails,
+    // search for the stashed files on /cache as usual.
+    if (!params.canwrite) {
+        if (stash_map.find(id) != stash_map.end()) {
+            const RangeSet& src = stash_map[id];
+            allocate(src.size * BLOCKSIZE, buffer);
+
+            if (ReadBlocks(src, buffer, params.fd) == -1) {
+                fprintf(stderr, "failed to read source blocks in stash map.\n");
+                return -1;
+            }
+            if (VerifyBlocks(id, buffer, src.size, true) != 0) {
+                fprintf(stderr, "failed to verify loaded source blocks in stash map.\n");
+                return -1;
+            }
+            return 0;
+        }
+    }
+
+    if (base.empty()) {
+        return -1;
+    }
+
+    size_t blockcount = 0;
+
+    if (!blocks) {
+        blocks = &blockcount;
+    }
+
+    std::string fn = GetStashFileName(base, id, "");
+
+    struct stat sb;
+    int res = stat(fn.c_str(), &sb);
+
+    if (res == -1) {
+        if (errno != ENOENT || printnoent) {
+            fprintf(stderr, "stat \"%s\" failed: %s\n", fn.c_str(), strerror(errno));
+        }
+        return -1;
+    }
+
+    fprintf(stderr, " loading %s\n", fn.c_str());
+
+    if ((sb.st_size % BLOCKSIZE) != 0) {
+        fprintf(stderr, "%s size %" PRId64 " not multiple of block size %d",
+                fn.c_str(), static_cast<int64_t>(sb.st_size), BLOCKSIZE);
+        return -1;
+    }
+
+    android::base::unique_fd fd(TEMP_FAILURE_RETRY(ota_open(fn.c_str(), O_RDONLY)));
+    if (fd == -1) {
+        fprintf(stderr, "open \"%s\" failed: %s\n", fn.c_str(), strerror(errno));
+        return -1;
+    }
+
+    allocate(sb.st_size, buffer);
+
+    if (read_all(fd, buffer, sb.st_size) == -1) {
+        return -1;
+    }
+
+    *blocks = sb.st_size / BLOCKSIZE;
+
+    if (verify && VerifyBlocks(id, buffer, *blocks, true) != 0) {
+        fprintf(stderr, "unexpected contents in %s\n", fn.c_str());
+        DeleteFile(fn, nullptr);
+        return -1;
+    }
+
+    return 0;
+}
+
+static int WriteStash(const std::string& base, const std::string& id, int blocks,
+        std::vector<uint8_t>& buffer, bool checkspace, bool *exists) {
+    if (base.empty()) {
+        return -1;
+    }
+
+    if (checkspace && CacheSizeCheck(blocks * BLOCKSIZE) != 0) {
+        fprintf(stderr, "not enough space to write stash\n");
+        return -1;
+    }
+
+    std::string fn = GetStashFileName(base, id, ".partial");
+    std::string cn = GetStashFileName(base, id, "");
+
+    if (exists) {
+        struct stat sb;
+        int res = stat(cn.c_str(), &sb);
+
+        if (res == 0) {
+            // The file already exists and since the name is the hash of the contents,
+            // it's safe to assume the contents are identical (accidental hash collisions
+            // are unlikely)
+            fprintf(stderr, " skipping %d existing blocks in %s\n", blocks, cn.c_str());
+            *exists = true;
+            return 0;
+        }
+
+        *exists = false;
+    }
+
+    fprintf(stderr, " writing %d blocks to %s\n", blocks, cn.c_str());
+
+    android::base::unique_fd fd(TEMP_FAILURE_RETRY(ota_open(fn.c_str(),
+                                                            O_WRONLY | O_CREAT | O_TRUNC,
+                                                            STASH_FILE_MODE)));
+    if (fd == -1) {
+        fprintf(stderr, "failed to create \"%s\": %s\n", fn.c_str(), strerror(errno));
+        return -1;
+    }
+
+    if (write_all(fd, buffer, blocks * BLOCKSIZE) == -1) {
+        return -1;
+    }
+
+    if (ota_fsync(fd) == -1) {
+        fprintf(stderr, "fsync \"%s\" failed: %s\n", fn.c_str(), strerror(errno));
+        return -1;
+    }
+
+    if (rename(fn.c_str(), cn.c_str()) == -1) {
+        fprintf(stderr, "rename(\"%s\", \"%s\") failed: %s\n", fn.c_str(), cn.c_str(),
+                strerror(errno));
+        return -1;
+    }
+
+    std::string dname = GetStashFileName(base, "", "");
+    android::base::unique_fd dfd(TEMP_FAILURE_RETRY(ota_open(dname.c_str(),
+                                                             O_RDONLY | O_DIRECTORY)));
+    if (dfd == -1) {
+        fprintf(stderr, "failed to open \"%s\" failed: %s\n", dname.c_str(), strerror(errno));
+        return -1;
+    }
+
+    if (ota_fsync(dfd) == -1) {
+        fprintf(stderr, "fsync \"%s\" failed: %s\n", dname.c_str(), strerror(errno));
+        return -1;
+    }
+
+    return 0;
+}
+
+// Creates a directory for storing stash files and checks if the /cache partition
+// hash enough space for the expected amount of blocks we need to store. Returns
+// >0 if we created the directory, zero if it existed already, and <0 of failure.
+
+static int CreateStash(State* state, int maxblocks, const char* blockdev, std::string& base) {
+    if (blockdev == nullptr) {
+        return -1;
+    }
+
+    // Stash directory should be different for each partition to avoid conflicts
+    // when updating multiple partitions at the same time, so we use the hash of
+    // the block device name as the base directory
+    uint8_t digest[SHA_DIGEST_LENGTH];
+    SHA1(reinterpret_cast<const uint8_t*>(blockdev), strlen(blockdev), digest);
+    base = print_sha1(digest);
+
+    std::string dirname = GetStashFileName(base, "", "");
+    struct stat sb;
+    int res = stat(dirname.c_str(), &sb);
+
+    if (res == -1 && errno != ENOENT) {
+        ErrorAbort(state, "stat \"%s\" failed: %s\n", dirname.c_str(), strerror(errno));
+        return -1;
+    } else if (res != 0) {
+        fprintf(stderr, "creating stash %s\n", dirname.c_str());
+        res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE);
+
+        if (res != 0) {
+            ErrorAbort(state, "mkdir \"%s\" failed: %s\n", dirname.c_str(), strerror(errno));
+            return -1;
+        }
+
+        if (CacheSizeCheck(maxblocks * BLOCKSIZE) != 0) {
+            ErrorAbort(state, "not enough space for stash\n");
+            return -1;
+        }
+
+        return 1;  // Created directory
+    }
+
+    fprintf(stderr, "using existing stash %s\n", dirname.c_str());
+
+    // If the directory already exists, calculate the space already allocated to
+    // stash files and check if there's enough for all required blocks. Delete any
+    // partially completed stash files first.
+
+    EnumerateStash(dirname, DeletePartial, nullptr);
+    int size = 0;
+    EnumerateStash(dirname, UpdateFileSize, &size);
+
+    size = maxblocks * BLOCKSIZE - size;
+
+    if (size > 0 && CacheSizeCheck(size) != 0) {
+        ErrorAbort(state, "not enough space for stash (%d more needed)\n", size);
+        return -1;
+    }
+
+    return 0; // Using existing directory
+}
+
+static int SaveStash(CommandParameters& params, const std::string& base,
+        std::vector<uint8_t>& buffer, int fd, bool usehash) {
+
+    // <stash_id> <src_range>
+    if (params.cpos + 1 >= params.tokens.size()) {
+        fprintf(stderr, "missing id and/or src range fields in stash command\n");
+        return -1;
+    }
+    const std::string& id = params.tokens[params.cpos++];
+
+    size_t blocks = 0;
+    if (usehash && LoadStash(params, base, id, true, &blocks, buffer, false) == 0) {
+        // Stash file already exists and has expected contents. Do not
+        // read from source again, as the source may have been already
+        // overwritten during a previous attempt.
+        return 0;
+    }
+
+    RangeSet src;
+    parse_range(params.tokens[params.cpos++], src);
+
+    allocate(src.size * BLOCKSIZE, buffer);
+    if (ReadBlocks(src, buffer, fd) == -1) {
+        return -1;
+    }
+    blocks = src.size;
+
+    if (usehash && VerifyBlocks(id, buffer, blocks, true) != 0) {
+        // Source blocks have unexpected contents. If we actually need this
+        // data later, this is an unrecoverable error. However, the command
+        // that uses the data may have already completed previously, so the
+        // possible failure will occur during source block verification.
+        fprintf(stderr, "failed to load source blocks for stash %s\n", id.c_str());
+        return 0;
+    }
+
+    // In verify mode, save source range_set instead of stashing blocks.
+    if (!params.canwrite && usehash) {
+        stash_map[id] = src;
+        return 0;
+    }
+
+    fprintf(stderr, "stashing %zu blocks to %s\n", blocks, id.c_str());
+    return WriteStash(base, id, blocks, buffer, false, nullptr);
+}
+
+static int FreeStash(const std::string& base, const std::string& id) {
+    if (base.empty() || id.empty()) {
+        return -1;
+    }
+
+    std::string fn = GetStashFileName(base, id, "");
+    DeleteFile(fn, nullptr);
+
+    return 0;
+}
+
+static void MoveRange(std::vector<uint8_t>& dest, const RangeSet& locs,
+        const std::vector<uint8_t>& source) {
+    // source contains packed data, which we want to move to the
+    // locations given in locs in the dest buffer.  source and dest
+    // may be the same buffer.
+
+    const uint8_t* from = source.data();
+    uint8_t* to = dest.data();
+    size_t start = locs.size;
+    for (int i = locs.count-1; i >= 0; --i) {
+        size_t blocks = locs.pos[i*2+1] - locs.pos[i*2];
+        start -= blocks;
+        memmove(to + (locs.pos[i*2] * BLOCKSIZE), from + (start * BLOCKSIZE),
+                blocks * BLOCKSIZE);
+    }
+}
+
+// Do a source/target load for move/bsdiff/imgdiff in version 2.
+// We expect to parse the remainder of the parameter tokens as one of:
+//
+//    <tgt_range> <src_block_count> <src_range>
+//        (loads data from source image only)
+//
+//    <tgt_range> <src_block_count> - <[stash_id:stash_range] ...>
+//        (loads data from stashes only)
+//
+//    <tgt_range> <src_block_count> <src_range> <src_loc> <[stash_id:stash_range] ...>
+//        (loads data from both source image and stashes)
+//
+// On return, buffer is filled with the loaded source data (rearranged
+// and combined with stashed data as necessary).  buffer may be
+// reallocated if needed to accommodate the source data.  *tgt is the
+// target RangeSet.  Any stashes required are loaded using LoadStash.
+
+static int LoadSrcTgtVersion2(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
+        std::vector<uint8_t>& buffer, int fd, const std::string& stashbase, bool* overlap) {
+
+    // At least it needs to provide three parameters: <tgt_range>,
+    // <src_block_count> and "-"/<src_range>.
+    if (params.cpos + 2 >= params.tokens.size()) {
+        fprintf(stderr, "invalid parameters\n");
+        return -1;
+    }
+
+    // <tgt_range>
+    parse_range(params.tokens[params.cpos++], tgt);
+
+    // <src_block_count>
+    const std::string& token = params.tokens[params.cpos++];
+    if (!android::base::ParseUint(token.c_str(), &src_blocks)) {
+        fprintf(stderr, "invalid src_block_count \"%s\"\n", token.c_str());
+        return -1;
+    }
+
+    allocate(src_blocks * BLOCKSIZE, buffer);
+
+    // "-" or <src_range> [<src_loc>]
+    if (params.tokens[params.cpos] == "-") {
+        // no source ranges, only stashes
+        params.cpos++;
+    } else {
+        RangeSet src;
+        parse_range(params.tokens[params.cpos++], src);
+        int res = ReadBlocks(src, buffer, fd);
+
+        if (overlap) {
+            *overlap = range_overlaps(src, tgt);
+        }
+
+        if (res == -1) {
+            return -1;
+        }
+
+        if (params.cpos >= params.tokens.size()) {
+            // no stashes, only source range
+            return 0;
+        }
+
+        RangeSet locs;
+        parse_range(params.tokens[params.cpos++], locs);
+        MoveRange(buffer, locs, buffer);
+    }
+
+    // <[stash_id:stash_range]>
+    while (params.cpos < params.tokens.size()) {
+        // Each word is a an index into the stash table, a colon, and
+        // then a rangeset describing where in the source block that
+        // stashed data should go.
+        std::vector<std::string> tokens = android::base::Split(params.tokens[params.cpos++], ":");
+        if (tokens.size() != 2) {
+            fprintf(stderr, "invalid parameter\n");
+            return -1;
+        }
+
+        std::vector<uint8_t> stash;
+        int res = LoadStash(params, stashbase, tokens[0], false, nullptr, stash, true);
+
+        if (res == -1) {
+            // These source blocks will fail verification if used later, but we
+            // will let the caller decide if this is a fatal failure
+            fprintf(stderr, "failed to load stash %s\n", tokens[0].c_str());
+            continue;
+        }
+
+        RangeSet locs;
+        parse_range(tokens[1], locs);
+
+        MoveRange(buffer, locs, stash);
+    }
+
+    return 0;
+}
+
+// Do a source/target load for move/bsdiff/imgdiff in version 3.
+//
+// Parameters are the same as for LoadSrcTgtVersion2, except for 'onehash', which
+// tells the function whether to expect separate source and targe block hashes, or
+// if they are both the same and only one hash should be expected, and
+// 'isunresumable', which receives a non-zero value if block verification fails in
+// a way that the update cannot be resumed anymore.
+//
+// If the function is unable to load the necessary blocks or their contents don't
+// match the hashes, the return value is -1 and the command should be aborted.
+//
+// If the return value is 1, the command has already been completed according to
+// the contents of the target blocks, and should not be performed again.
+//
+// If the return value is 0, source blocks have expected content and the command
+// can be performed.
+
+static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
+        bool onehash, bool& overlap) {
+
+    if (params.cpos >= params.tokens.size()) {
+        fprintf(stderr, "missing source hash\n");
+        return -1;
+    }
+
+    std::string srchash = params.tokens[params.cpos++];
+    std::string tgthash;
+
+    if (onehash) {
+        tgthash = srchash;
+    } else {
+        if (params.cpos >= params.tokens.size()) {
+            fprintf(stderr, "missing target hash\n");
+            return -1;
+        }
+        tgthash = params.tokens[params.cpos++];
+    }
+
+    if (LoadSrcTgtVersion2(params, tgt, src_blocks, params.buffer, params.fd,
+                           params.stashbase, &overlap) == -1) {
+        return -1;
+    }
+
+    std::vector<uint8_t> tgtbuffer(tgt.size * BLOCKSIZE);
+
+    if (ReadBlocks(tgt, tgtbuffer, params.fd) == -1) {
+        return -1;
+    }
+
+    if (VerifyBlocks(tgthash, tgtbuffer, tgt.size, false) == 0) {
+        // Target blocks already have expected content, command should be skipped
+        return 1;
+    }
+
+    if (VerifyBlocks(srchash, params.buffer, src_blocks, true) == 0) {
+        // If source and target blocks overlap, stash the source blocks so we can
+        // resume from possible write errors. In verify mode, we can skip stashing
+        // because the source blocks won't be overwritten.
+        if (overlap && params.canwrite) {
+            fprintf(stderr, "stashing %zu overlapping blocks to %s\n", src_blocks,
+                    srchash.c_str());
+
+            bool stash_exists = false;
+            if (WriteStash(params.stashbase, srchash, src_blocks, params.buffer, true,
+                           &stash_exists) != 0) {
+                fprintf(stderr, "failed to stash overlapping source blocks\n");
+                return -1;
+            }
+
+            // Can be deleted when the write has completed
+            if (!stash_exists) {
+                params.freestash = srchash;
+            }
+        }
+
+        // Source blocks have expected content, command can proceed
+        return 0;
+    }
+
+    if (overlap && LoadStash(params, params.stashbase, srchash, true, nullptr, params.buffer,
+                             true) == 0) {
+        // Overlapping source blocks were previously stashed, command can proceed.
+        // We are recovering from an interrupted command, so we don't know if the
+        // stash can safely be deleted after this command.
+        return 0;
+    }
+
+    // Valid source data not available, update cannot be resumed
+    fprintf(stderr, "partition has unexpected contents\n");
+    params.isunresumable = true;
+
+    return -1;
+}
+
+static int PerformCommandMove(CommandParameters& params) {
+    size_t blocks = 0;
+    bool overlap = false;
+    int status = 0;
+    RangeSet tgt;
+
+    if (params.version == 1) {
+        status = LoadSrcTgtVersion1(params, tgt, blocks, params.buffer, params.fd);
+    } else if (params.version == 2) {
+        status = LoadSrcTgtVersion2(params, tgt, blocks, params.buffer, params.fd,
+                params.stashbase, nullptr);
+    } else if (params.version >= 3) {
+        status = LoadSrcTgtVersion3(params, tgt, blocks, true, overlap);
+    }
+
+    if (status == -1) {
+        fprintf(stderr, "failed to read blocks for move\n");
+        return -1;
+    }
+
+    if (status == 0) {
+        params.foundwrites = true;
+    } else if (params.foundwrites) {
+        fprintf(stderr, "warning: commands executed out of order [%s]\n", params.cmdname);
+    }
+
+    if (params.canwrite) {
+        if (status == 0) {
+            fprintf(stderr, "  moving %zu blocks\n", blocks);
+
+            if (WriteBlocks(tgt, params.buffer, params.fd) == -1) {
+                return -1;
+            }
+        } else {
+            fprintf(stderr, "skipping %zu already moved blocks\n", blocks);
+        }
+
+    }
+
+    if (!params.freestash.empty()) {
+        FreeStash(params.stashbase, params.freestash);
+        params.freestash.clear();
+    }
+
+    params.written += tgt.size;
+
+    return 0;
+}
+
+static int PerformCommandStash(CommandParameters& params) {
+    return SaveStash(params, params.stashbase, params.buffer, params.fd,
+            (params.version >= 3));
+}
+
+static int PerformCommandFree(CommandParameters& params) {
+    // <stash_id>
+    if (params.cpos >= params.tokens.size()) {
+        fprintf(stderr, "missing stash id in free command\n");
+        return -1;
+    }
+
+    const std::string& id = params.tokens[params.cpos++];
+
+    if (!params.canwrite && stash_map.find(id) != stash_map.end()) {
+        stash_map.erase(id);
+        return 0;
+    }
+
+    if (params.createdstash || params.canwrite) {
+        return FreeStash(params.stashbase, id);
+    }
+
+    return 0;
+}
+
+static int PerformCommandZero(CommandParameters& params) {
+
+    if (params.cpos >= params.tokens.size()) {
+        fprintf(stderr, "missing target blocks for zero\n");
+        return -1;
+    }
+
+    RangeSet tgt;
+    parse_range(params.tokens[params.cpos++], tgt);
+
+    fprintf(stderr, "  zeroing %zu blocks\n", tgt.size);
+
+    allocate(BLOCKSIZE, params.buffer);
+    memset(params.buffer.data(), 0, BLOCKSIZE);
+
+    if (params.canwrite) {
+        for (size_t i = 0; i < tgt.count; ++i) {
+            if (!check_lseek(params.fd, (off64_t) tgt.pos[i * 2] * BLOCKSIZE, SEEK_SET)) {
+                return -1;
+            }
+
+            for (size_t j = tgt.pos[i * 2]; j < tgt.pos[i * 2 + 1]; ++j) {
+                if (write_all(params.fd, params.buffer, BLOCKSIZE) == -1) {
+                    return -1;
+                }
+            }
+        }
+    }
+
+    if (params.cmdname[0] == 'z') {
+        // Update only for the zero command, as the erase command will call
+        // this if DEBUG_ERASE is defined.
+        params.written += tgt.size;
+    }
+
+    return 0;
+}
+
+static int PerformCommandNew(CommandParameters& params) {
+
+    if (params.cpos >= params.tokens.size()) {
+        fprintf(stderr, "missing target blocks for new\n");
+        return -1;
+    }
+
+    RangeSet tgt;
+    parse_range(params.tokens[params.cpos++], tgt);
+
+    if (params.canwrite) {
+        fprintf(stderr, " writing %zu blocks of new data\n", tgt.size);
+
+        RangeSinkState rss(tgt);
+        rss.fd = params.fd;
+        rss.p_block = 0;
+        rss.p_remain = (tgt.pos[1] - tgt.pos[0]) * BLOCKSIZE;
+
+        if (!check_lseek(params.fd, (off64_t) tgt.pos[0] * BLOCKSIZE, SEEK_SET)) {
+            return -1;
+        }
+
+        pthread_mutex_lock(&params.nti.mu);
+        params.nti.rss = &rss;
+        pthread_cond_broadcast(&params.nti.cv);
+
+        while (params.nti.rss) {
+            pthread_cond_wait(&params.nti.cv, &params.nti.mu);
+        }
+
+        pthread_mutex_unlock(&params.nti.mu);
+    }
+
+    params.written += tgt.size;
+
+    return 0;
+}
+
+static int PerformCommandDiff(CommandParameters& params) {
+
+    // <offset> <length>
+    if (params.cpos + 1 >= params.tokens.size()) {
+        fprintf(stderr, "missing patch offset or length for %s\n", params.cmdname);
+        return -1;
+    }
+
+    size_t offset;
+    if (!android::base::ParseUint(params.tokens[params.cpos++].c_str(), &offset)) {
+        fprintf(stderr, "invalid patch offset\n");
+        return -1;
+    }
+
+    size_t len;
+    if (!android::base::ParseUint(params.tokens[params.cpos++].c_str(), &len)) {
+        fprintf(stderr, "invalid patch offset\n");
+        return -1;
+    }
+
+    RangeSet tgt;
+    size_t blocks = 0;
+    bool overlap = false;
+    int status = 0;
+    if (params.version == 1) {
+        status = LoadSrcTgtVersion1(params, tgt, blocks, params.buffer, params.fd);
+    } else if (params.version == 2) {
+        status = LoadSrcTgtVersion2(params, tgt, blocks, params.buffer, params.fd,
+                params.stashbase, nullptr);
+    } else if (params.version >= 3) {
+        status = LoadSrcTgtVersion3(params, tgt, blocks, false, overlap);
+    }
+
+    if (status == -1) {
+        fprintf(stderr, "failed to read blocks for diff\n");
+        return -1;
+    }
+
+    if (status == 0) {
+        params.foundwrites = true;
+    } else if (params.foundwrites) {
+        fprintf(stderr, "warning: commands executed out of order [%s]\n", params.cmdname);
+    }
+
+    if (params.canwrite) {
+        if (status == 0) {
+            fprintf(stderr, "patching %zu blocks to %zu\n", blocks, tgt.size);
+
+            Value patch_value;
+            patch_value.type = VAL_BLOB;
+            patch_value.size = len;
+            patch_value.data = (char*) (params.patch_start + offset);
+
+            RangeSinkState rss(tgt);
+            rss.fd = params.fd;
+            rss.p_block = 0;
+            rss.p_remain = (tgt.pos[1] - tgt.pos[0]) * BLOCKSIZE;
+
+            if (!check_lseek(params.fd, (off64_t) tgt.pos[0] * BLOCKSIZE, SEEK_SET)) {
+                return -1;
+            }
+
+            if (params.cmdname[0] == 'i') {      // imgdiff
+                ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, &patch_value,
+                    &RangeSinkWrite, &rss, nullptr, nullptr);
+            } else {
+                ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, &patch_value, 0,
+                    &RangeSinkWrite, &rss, nullptr);
+            }
+
+            // We expect the output of the patcher to fill the tgt ranges exactly.
+            if (rss.p_block != tgt.count || rss.p_remain != 0) {
+                fprintf(stderr, "range sink underrun?\n");
+            }
+        } else {
+            fprintf(stderr, "skipping %zu blocks already patched to %zu [%s]\n",
+                blocks, tgt.size, params.cmdline);
+        }
+    }
+
+    if (!params.freestash.empty()) {
+        FreeStash(params.stashbase, params.freestash);
+        params.freestash.clear();
+    }
+
+    params.written += tgt.size;
+
+    return 0;
+}
+
+static int PerformCommandErase(CommandParameters& params) {
+    if (DEBUG_ERASE) {
+        return PerformCommandZero(params);
+    }
+
+    struct stat sb;
+    if (fstat(params.fd, &sb) == -1) {
+        fprintf(stderr, "failed to fstat device to erase: %s\n", strerror(errno));
+        return -1;
+    }
+
+    if (!S_ISBLK(sb.st_mode)) {
+        fprintf(stderr, "not a block device; skipping erase\n");
+        return -1;
+    }
+
+    if (params.cpos >= params.tokens.size()) {
+        fprintf(stderr, "missing target blocks for erase\n");
+        return -1;
+    }
+
+    RangeSet tgt;
+    parse_range(params.tokens[params.cpos++], tgt);
+
+    if (params.canwrite) {
+        fprintf(stderr, " erasing %zu blocks\n", tgt.size);
+
+        for (size_t i = 0; i < tgt.count; ++i) {
+            uint64_t blocks[2];
+            // offset in bytes
+            blocks[0] = tgt.pos[i * 2] * (uint64_t) BLOCKSIZE;
+            // length in bytes
+            blocks[1] = (tgt.pos[i * 2 + 1] - tgt.pos[i * 2]) * (uint64_t) BLOCKSIZE;
+
+            if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) {
+                fprintf(stderr, "BLKDISCARD ioctl failed: %s\n", strerror(errno));
+                return -1;
+            }
+        }
+    }
+
+    return 0;
+}
+
+// Definitions for transfer list command functions
+typedef int (*CommandFunction)(CommandParameters&);
+
+struct Command {
+    const char* name;
+    CommandFunction f;
+};
+
+// CompareCommands and CompareCommandNames are for the hash table
+
+static int CompareCommands(const void* c1, const void* c2) {
+    return strcmp(((const Command*) c1)->name, ((const Command*) c2)->name);
+}
+
+static int CompareCommandNames(const void* c1, const void* c2) {
+    return strcmp(((const Command*) c1)->name, (const char*) c2);
+}
+
+// HashString is used to hash command names for the hash table
+
+static unsigned int HashString(const char *s) {
+    unsigned int hash = 0;
+    if (s) {
+        while (*s) {
+            hash = hash * 33 + *s++;
+        }
+    }
+    return hash;
+}
+
+// args:
+//    - block device (or file) to modify in-place
+//    - transfer list (blob)
+//    - new data stream (filename within package.zip)
+//    - patch stream (filename within package.zip, must be uncompressed)
+
+static Value* PerformBlockImageUpdate(const char* name, State* state, int /* argc */, Expr* argv[],
+        const Command* commands, size_t cmdcount, bool dryrun) {
+    CommandParameters params = {};
+    params.canwrite = !dryrun;
+
+    fprintf(stderr, "performing %s\n", dryrun ? "verification" : "update");
+
+    Value* blockdev_filename = nullptr;
+    Value* transfer_list_value = nullptr;
+    Value* new_data_fn = nullptr;
+    Value* patch_data_fn = nullptr;
+    if (ReadValueArgs(state, argv, 4, &blockdev_filename, &transfer_list_value,
+            &new_data_fn, &patch_data_fn) < 0) {
+        return StringValue(strdup(""));
+    }
+    std::unique_ptr<Value, decltype(&FreeValue)> blockdev_filename_holder(blockdev_filename,
+            FreeValue);
+    std::unique_ptr<Value, decltype(&FreeValue)> transfer_list_value_holder(transfer_list_value,
+            FreeValue);
+    std::unique_ptr<Value, decltype(&FreeValue)> new_data_fn_holder(new_data_fn, FreeValue);
+    std::unique_ptr<Value, decltype(&FreeValue)> patch_data_fn_holder(patch_data_fn, FreeValue);
+
+    if (blockdev_filename->type != VAL_STRING) {
+        ErrorAbort(state, "blockdev_filename argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+    if (transfer_list_value->type != VAL_BLOB) {
+        ErrorAbort(state, "transfer_list argument to %s must be blob", name);
+        return StringValue(strdup(""));
+    }
+    if (new_data_fn->type != VAL_STRING) {
+        ErrorAbort(state, "new_data_fn argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+    if (patch_data_fn->type != VAL_STRING) {
+        ErrorAbort(state, "patch_data_fn argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+
+    UpdaterInfo* ui = reinterpret_cast<UpdaterInfo*>(state->cookie);
+
+    if (ui == nullptr) {
+        return StringValue(strdup(""));
+    }
+
+    FILE* cmd_pipe = ui->cmd_pipe;
+    ZipArchive* za = ui->package_zip;
+
+    if (cmd_pipe == nullptr || za == nullptr) {
+        return StringValue(strdup(""));
+    }
+
+    const ZipEntry* patch_entry = mzFindZipEntry(za, patch_data_fn->data);
+    if (patch_entry == nullptr) {
+        fprintf(stderr, "%s(): no file \"%s\" in package", name, patch_data_fn->data);
+        return StringValue(strdup(""));
+    }
+
+    params.patch_start = ui->package_zip_addr + mzGetZipEntryOffset(patch_entry);
+    const ZipEntry* new_entry = mzFindZipEntry(za, new_data_fn->data);
+    if (new_entry == nullptr) {
+        fprintf(stderr, "%s(): no file \"%s\" in package", name, new_data_fn->data);
+        return StringValue(strdup(""));
+    }
+
+    params.fd.reset(TEMP_FAILURE_RETRY(ota_open(blockdev_filename->data, O_RDWR)));
+    if (params.fd == -1) {
+        fprintf(stderr, "open \"%s\" failed: %s\n", blockdev_filename->data, strerror(errno));
+        return StringValue(strdup(""));
+    }
+
+    if (params.canwrite) {
+        params.nti.za = za;
+        params.nti.entry = new_entry;
+
+        pthread_mutex_init(&params.nti.mu, nullptr);
+        pthread_cond_init(&params.nti.cv, nullptr);
+        pthread_attr_t attr;
+        pthread_attr_init(&attr);
+        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+
+        int error = pthread_create(&params.thread, &attr, unzip_new_data, &params.nti);
+        if (error != 0) {
+            fprintf(stderr, "pthread_create failed: %s\n", strerror(error));
+            return StringValue(strdup(""));
+        }
+    }
+
+    // Copy all the lines in transfer_list_value into std::string for
+    // processing.
+    const std::string transfer_list(transfer_list_value->data, transfer_list_value->size);
+    std::vector<std::string> lines = android::base::Split(transfer_list, "\n");
+    if (lines.size() < 2) {
+        ErrorAbort(state, "too few lines in the transfer list [%zd]\n", lines.size());
+        return StringValue(strdup(""));
+    }
+
+    // First line in transfer list is the version number
+    if (!android::base::ParseInt(lines[0].c_str(), &params.version, 1, 4)) {
+        fprintf(stderr, "unexpected transfer list version [%s]\n", lines[0].c_str());
+        return StringValue(strdup(""));
+    }
+
+    fprintf(stderr, "blockimg version is %d\n", params.version);
+
+    // Second line in transfer list is the total number of blocks we expect to write
+    int total_blocks;
+    if (!android::base::ParseInt(lines[1].c_str(), &total_blocks, 0)) {
+        ErrorAbort(state, "unexpected block count [%s]\n", lines[1].c_str());
+        return StringValue(strdup(""));
+    }
+
+    if (total_blocks == 0) {
+        return StringValue(strdup("t"));
+    }
+
+    size_t start = 2;
+    if (params.version >= 2) {
+        if (lines.size() < 4) {
+            ErrorAbort(state, "too few lines in the transfer list [%zu]\n", lines.size());
+            return StringValue(strdup(""));
+        }
+
+        // Third line is how many stash entries are needed simultaneously
+        fprintf(stderr, "maximum stash entries %s\n", lines[2].c_str());
+
+        // Fourth line is the maximum number of blocks that will be stashed simultaneously
+        int stash_max_blocks;
+        if (!android::base::ParseInt(lines[3].c_str(), &stash_max_blocks, 0)) {
+            ErrorAbort(state, "unexpected maximum stash blocks [%s]\n", lines[3].c_str());
+            return StringValue(strdup(""));
+        }
+
+        int res = CreateStash(state, stash_max_blocks, blockdev_filename->data, params.stashbase);
+        if (res == -1) {
+            return StringValue(strdup(""));
+        }
+
+        params.createdstash = res;
+
+        start += 2;
+    }
+
+    // Build a hash table of the available commands
+    HashTable* cmdht = mzHashTableCreate(cmdcount, nullptr);
+    std::unique_ptr<HashTable, decltype(&mzHashTableFree)> cmdht_holder(cmdht, mzHashTableFree);
+
+    for (size_t i = 0; i < cmdcount; ++i) {
+        unsigned int cmdhash = HashString(commands[i].name);
+        mzHashTableLookup(cmdht, cmdhash, (void*) &commands[i], CompareCommands, true);
+    }
+
+    int rc = -1;
+
+    // Subsequent lines are all individual transfer commands
+    for (auto it = lines.cbegin() + start; it != lines.cend(); it++) {
+        const std::string& line_str(*it);
+        if (line_str.empty()) {
+            continue;
+        }
+
+        params.tokens = android::base::Split(line_str, " ");
+        params.cpos = 0;
+        params.cmdname = params.tokens[params.cpos++].c_str();
+        params.cmdline = line_str.c_str();
+
+        unsigned int cmdhash = HashString(params.cmdname);
+        const Command* cmd = reinterpret_cast<const Command*>(mzHashTableLookup(cmdht, cmdhash,
+                const_cast<char*>(params.cmdname), CompareCommandNames,
+                false));
+
+        if (cmd == nullptr) {
+            fprintf(stderr, "unexpected command [%s]\n", params.cmdname);
+            goto pbiudone;
+        }
+
+        if (cmd->f != nullptr && cmd->f(params) == -1) {
+            fprintf(stderr, "failed to execute command [%s]\n", line_str.c_str());
+            goto pbiudone;
+        }
+
+        if (params.canwrite) {
+            if (ota_fsync(params.fd) == -1) {
+                fprintf(stderr, "fsync failed: %s\n", strerror(errno));
+                goto pbiudone;
+            }
+            fprintf(cmd_pipe, "set_progress %.4f\n", (double) params.written / total_blocks);
+            fflush(cmd_pipe);
+        }
+    }
+
+    if (params.canwrite) {
+        pthread_join(params.thread, nullptr);
+
+        fprintf(stderr, "wrote %zu blocks; expected %d\n", params.written, total_blocks);
+        fprintf(stderr, "max alloc needed was %zu\n", params.buffer.size());
+
+        // Delete stash only after successfully completing the update, as it
+        // may contain blocks needed to complete the update later.
+        DeleteStash(params.stashbase);
+    } else {
+        fprintf(stderr, "verified partition contents; update may be resumed\n");
+    }
+
+    rc = 0;
+
+pbiudone:
+    if (ota_fsync(params.fd) == -1) {
+        fprintf(stderr, "fsync failed: %s\n", strerror(errno));
+    }
+    // params.fd will be automatically closed because it's a unique_fd.
+
+    // Only delete the stash if the update cannot be resumed, or it's
+    // a verification run and we created the stash.
+    if (params.isunresumable || (!params.canwrite && params.createdstash)) {
+        DeleteStash(params.stashbase);
+    }
+
+    return StringValue(rc == 0 ? strdup("t") : strdup(""));
+}
+
+// The transfer list is a text file containing commands to
+// transfer data from one place to another on the target
+// partition.  We parse it and execute the commands in order:
+//
+//    zero [rangeset]
+//      - fill the indicated blocks with zeros
+//
+//    new [rangeset]
+//      - fill the blocks with data read from the new_data file
+//
+//    erase [rangeset]
+//      - mark the given blocks as empty
+//
+//    move <...>
+//    bsdiff <patchstart> <patchlen> <...>
+//    imgdiff <patchstart> <patchlen> <...>
+//      - read the source blocks, apply a patch (or not in the
+//        case of move), write result to target blocks.  bsdiff or
+//        imgdiff specifies the type of patch; move means no patch
+//        at all.
+//
+//        The format of <...> differs between versions 1 and 2;
+//        see the LoadSrcTgtVersion{1,2}() functions for a
+//        description of what's expected.
+//
+//    stash <stash_id> <src_range>
+//      - (version 2+ only) load the given source range and stash
+//        the data in the given slot of the stash table.
+//
+//    free <stash_id>
+//      - (version 3+ only) free the given stash data.
+//
+// The creator of the transfer list will guarantee that no block
+// is read (ie, used as the source for a patch or move) after it
+// has been written.
+//
+// In version 2, the creator will guarantee that a given stash is
+// loaded (with a stash command) before it's used in a
+// move/bsdiff/imgdiff command.
+//
+// Within one command the source and target ranges may overlap so
+// in general we need to read the entire source into memory before
+// writing anything to the target blocks.
+//
+// All the patch data is concatenated into one patch_data file in
+// the update package.  It must be stored uncompressed because we
+// memory-map it in directly from the archive.  (Since patches are
+// already compressed, we lose very little by not compressing
+// their concatenation.)
+//
+// In version 3, commands that read data from the partition (i.e.
+// move/bsdiff/imgdiff/stash) have one or more additional hashes
+// before the range parameters, which are used to check if the
+// command has already been completed and verify the integrity of
+// the source data.
+
+Value* BlockImageVerifyFn(const char* name, State* state, int argc, Expr* argv[]) {
+    // Commands which are not tested are set to nullptr to skip them completely
+    const Command commands[] = {
+        { "bsdiff",     PerformCommandDiff  },
+        { "erase",      nullptr             },
+        { "free",       PerformCommandFree  },
+        { "imgdiff",    PerformCommandDiff  },
+        { "move",       PerformCommandMove  },
+        { "new",        nullptr             },
+        { "stash",      PerformCommandStash },
+        { "zero",       nullptr             }
+    };
+
+    // Perform a dry run without writing to test if an update can proceed
+    return PerformBlockImageUpdate(name, state, argc, argv, commands,
+                sizeof(commands) / sizeof(commands[0]), true);
+}
+
+Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]) {
+    const Command commands[] = {
+        { "bsdiff",     PerformCommandDiff  },
+        { "erase",      PerformCommandErase },
+        { "free",       PerformCommandFree  },
+        { "imgdiff",    PerformCommandDiff  },
+        { "move",       PerformCommandMove  },
+        { "new",        PerformCommandNew   },
+        { "stash",      PerformCommandStash },
+        { "zero",       PerformCommandZero  }
+    };
+
+    return PerformBlockImageUpdate(name, state, argc, argv, commands,
+                sizeof(commands) / sizeof(commands[0]), false);
+}
+
+Value* RangeSha1Fn(const char* name, State* state, int /* argc */, Expr* argv[]) {
+    Value* blockdev_filename;
+    Value* ranges;
+
+    if (ReadValueArgs(state, argv, 2, &blockdev_filename, &ranges) < 0) {
+        return StringValue(strdup(""));
+    }
+    std::unique_ptr<Value, decltype(&FreeValue)> ranges_holder(ranges, FreeValue);
+    std::unique_ptr<Value, decltype(&FreeValue)> blockdev_filename_holder(blockdev_filename,
+            FreeValue);
+
+    if (blockdev_filename->type != VAL_STRING) {
+        ErrorAbort(state, "blockdev_filename argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+    if (ranges->type != VAL_STRING) {
+        ErrorAbort(state, "ranges argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+
+    android::base::unique_fd fd(ota_open(blockdev_filename->data, O_RDWR));
+    if (fd == -1) {
+        ErrorAbort(state, "open \"%s\" failed: %s", blockdev_filename->data, strerror(errno));
+        return StringValue(strdup(""));
+    }
+
+    RangeSet rs;
+    parse_range(ranges->data, rs);
+
+    SHA_CTX ctx;
+    SHA1_Init(&ctx);
+
+    std::vector<uint8_t> buffer(BLOCKSIZE);
+    for (size_t i = 0; i < rs.count; ++i) {
+        if (!check_lseek(fd, (off64_t)rs.pos[i*2] * BLOCKSIZE, SEEK_SET)) {
+            ErrorAbort(state, "failed to seek %s: %s", blockdev_filename->data, strerror(errno));
+            return StringValue(strdup(""));
+        }
+
+        for (size_t j = rs.pos[i*2]; j < rs.pos[i*2+1]; ++j) {
+            if (read_all(fd, buffer, BLOCKSIZE) == -1) {
+                ErrorAbort(state, "failed to read %s: %s", blockdev_filename->data,
+                           strerror(errno));
+                return StringValue(strdup(""));
+            }
+
+            SHA1_Update(&ctx, buffer.data(), BLOCKSIZE);
+        }
+    }
+    uint8_t digest[SHA_DIGEST_LENGTH];
+    SHA1_Final(digest, &ctx);
+
+    return StringValue(strdup(print_sha1(digest).c_str()));
+}
+
+// This function checks if a device has been remounted R/W prior to an incremental
+// OTA update. This is an common cause of update abortion. The function reads the
+// 1st block of each partition and check for mounting time/count. It return string "t"
+// if executes successfully and an empty string otherwise.
+
+Value* CheckFirstBlockFn(const char* name, State* state, int argc, Expr* argv[]) {
+    Value* arg_filename;
+
+    if (ReadValueArgs(state, argv, 1, &arg_filename) < 0) {
+        return nullptr;
+    }
+    std::unique_ptr<Value, decltype(&FreeValue)> filename(arg_filename, FreeValue);
+
+    if (filename->type != VAL_STRING) {
+        ErrorAbort(state, "filename argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+
+    android::base::unique_fd fd(ota_open(arg_filename->data, O_RDONLY));
+    if (fd == -1) {
+        ErrorAbort(state, "open \"%s\" failed: %s", arg_filename->data, strerror(errno));
+        return StringValue(strdup(""));
+    }
+
+    RangeSet blk0 {1 /*count*/, 1/*size*/, std::vector<size_t> {0, 1}/*position*/};
+    std::vector<uint8_t> block0_buffer(BLOCKSIZE);
+
+    if (ReadBlocks(blk0, block0_buffer, fd) == -1) {
+        ErrorAbort(state, "failed to read %s: %s", arg_filename->data, strerror(errno));
+        return StringValue(strdup(""));
+    }
+
+    // https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
+    // Super block starts from block 0, offset 0x400
+    //   0x2C: len32 Mount time
+    //   0x30: len32 Write time
+    //   0x34: len16 Number of mounts since the last fsck
+    //   0x38: len16 Magic signature 0xEF53
+
+    time_t mount_time = *reinterpret_cast<uint32_t*>(&block0_buffer[0x400+0x2C]);
+    uint16_t mount_count = *reinterpret_cast<uint16_t*>(&block0_buffer[0x400+0x34]);
+
+    if (mount_count > 0) {
+        uiPrintf(state, "Device was remounted R/W %d times\n", mount_count);
+        uiPrintf(state, "Last remount happened on %s", ctime(&mount_time));
+    }
+
+    return StringValue(strdup("t"));
+}
+
+
+Value* BlockImageRecoverFn(const char* name, State* state, int argc, Expr* argv[]) {
+    Value* arg_filename;
+    Value* arg_ranges;
+
+    if (ReadValueArgs(state, argv, 2, &arg_filename, &arg_ranges) < 0) {
+        return NULL;
+    }
+
+    std::unique_ptr<Value, decltype(&FreeValue)> filename(arg_filename, FreeValue);
+    std::unique_ptr<Value, decltype(&FreeValue)> ranges(arg_ranges, FreeValue);
+
+    if (filename->type != VAL_STRING) {
+        ErrorAbort(state, "filename argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+    if (ranges->type != VAL_STRING) {
+        ErrorAbort(state, "ranges argument to %s must be string", name);
+        return StringValue(strdup(""));
+    }
+
+    // Output notice to log when recover is attempted
+    fprintf(stderr, "%s image corrupted, attempting to recover...\n", filename->data);
+
+    // When opened with O_RDWR, libfec rewrites corrupted blocks when they are read
+    fec::io fh(filename->data, O_RDWR);
+
+    if (!fh) {
+        ErrorAbort(state, "fec_open \"%s\" failed: %s", filename->data, strerror(errno));
+        return StringValue(strdup(""));
+    }
+
+    if (!fh.has_ecc() || !fh.has_verity()) {
+        ErrorAbort(state, "unable to use metadata to correct errors");
+        return StringValue(strdup(""));
+    }
+
+    fec_status status;
+
+    if (!fh.get_status(status)) {
+        ErrorAbort(state, "failed to read FEC status");
+        return StringValue(strdup(""));
+    }
+
+    RangeSet rs;
+    parse_range(ranges->data, rs);
+
+    uint8_t buffer[BLOCKSIZE];
+
+    for (size_t i = 0; i < rs.count; ++i) {
+        for (size_t j = rs.pos[i * 2]; j < rs.pos[i * 2 + 1]; ++j) {
+            // Stay within the data area, libfec validates and corrects metadata
+            if (status.data_size <= (uint64_t)j * BLOCKSIZE) {
+                continue;
+            }
+
+            if (fh.pread(buffer, BLOCKSIZE, (off64_t)j * BLOCKSIZE) != BLOCKSIZE) {
+                ErrorAbort(state, "failed to recover %s (block %zu): %s", filename->data,
+                    j, strerror(errno));
+                return StringValue(strdup(""));
+            }
+
+            // If we want to be able to recover from a situation where rewriting a corrected
+            // block doesn't guarantee the same data will be returned when re-read later, we
+            // can save a copy of corrected blocks to /cache. Note:
+            //
+            //  1. Maximum space required from /cache is the same as the maximum number of
+            //     corrupted blocks we can correct. For RS(255, 253) and a 2 GiB partition,
+            //     this would be ~16 MiB, for example.
+            //
+            //  2. To find out if this block was corrupted, call fec_get_status after each
+            //     read and check if the errors field value has increased.
+        }
+    }
+    fprintf(stderr, "...%s image recovered successfully.\n", filename->data);
+    return StringValue(strdup("t"));
+}
+
+void RegisterBlockImageFunctions() {
+    RegisterFunction("block_image_verify", BlockImageVerifyFn);
+    RegisterFunction("block_image_update", BlockImageUpdateFn);
+    RegisterFunction("block_image_recover", BlockImageRecoverFn);
+    RegisterFunction("check_first_block", CheckFirstBlockFn);
+    RegisterFunction("range_sha1", RangeSha1Fn);
+}
diff --git a/recovery/updater/blockimg.h b/recovery/updater/blockimg.h
new file mode 100644
index 0000000..2f4ad3c
--- /dev/null
+++ b/recovery/updater/blockimg.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef _UPDATER_BLOCKIMG_H_
+#define _UPDATER_BLOCKIMG_H_
+
+void RegisterBlockImageFunctions();
+
+#endif
diff --git a/recovery/updater/install.cpp b/recovery/updater/install.cpp
new file mode 100644
index 0000000..3e7e928
--- /dev/null
+++ b/recovery/updater/install.cpp
@@ -0,0 +1,1487 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#include <ftw.h>
+#include <sys/capability.h>
+#include <sys/xattr.h>
+#include <linux/xattr.h>
+#include <inttypes.h>
+
+#include <memory>
+#include <vector>
+
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <android-base/stringprintf.h>
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#include "bootloader.h"
+#include "applypatch/applypatch.h"
+#include "cutils/android_reboot.h"
+#include "cutils/misc.h"
+#include "cutils/properties.h"
+#include "edify/expr.h"
+#include "openssl/sha.h"
+#include "minzip/DirUtil.h"
+#include "mounts.h"
+#include "ota_io.h"
+#include "updater.h"
+#include "install.h"
+#include "tune2fs.h"
+
+#include "make_ext4fs.h"
+#include "wipe.h"
+
+// Send over the buffer to recovery though the command pipe.
+static void uiPrint(State* state, const std::string& buffer) {
+    UpdaterInfo* ui = reinterpret_cast<UpdaterInfo*>(state->cookie);
+
+    // "line1\nline2\n" will be split into 3 tokens: "line1", "line2" and "".
+    // So skip sending empty strings to UI.
+    std::vector<std::string> lines = android::base::Split(buffer, "\n");
+    for (auto& line: lines) {
+        if (!line.empty()) {
+            fprintf(ui->cmd_pipe, "ui_print %s\n", line.c_str());
+            fprintf(ui->cmd_pipe, "ui_print\n");
+        }
+    }
+
+    // On the updater side, we need to dump the contents to stderr (which has
+    // been redirected to the log file). Because the recovery will only print
+    // the contents to screen when processing pipe command ui_print.
+    fprintf(stderr, "%s", buffer.c_str());
+}
+
+void uiPrintf(State* _Nonnull state, const char* _Nonnull format, ...) {
+    std::string error_msg;
+
+    va_list ap;
+    va_start(ap, format);
+    android::base::StringAppendV(&error_msg, format, ap);
+    va_end(ap);
+
+    uiPrint(state, error_msg);
+}
+
+// Take a sha-1 digest and return it as a newly-allocated hex string.
+char* PrintSha1(const uint8_t* digest) {
+    char* buffer = reinterpret_cast<char*>(malloc(SHA_DIGEST_LENGTH*2 + 1));
+    const char* alphabet = "0123456789abcdef";
+    size_t i;
+    for (i = 0; i < SHA_DIGEST_LENGTH; ++i) {
+        buffer[i*2] = alphabet[(digest[i] >> 4) & 0xf];
+        buffer[i*2+1] = alphabet[digest[i] & 0xf];
+    }
+    buffer[i*2] = '\0';
+    return buffer;
+}
+
+// mount(fs_type, partition_type, location, mount_point)
+//
+//    fs_type="ext4"   partition_type="EMMC"    location=device
+Value* MountFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    if (argc != 4 && argc != 5) {
+        return ErrorAbort(state, "%s() expects 4-5 args, got %d", name, argc);
+    }
+    char* fs_type;
+    char* partition_type;
+    char* location;
+    char* mount_point;
+    char* mount_options;
+    bool has_mount_options;
+    if (argc == 5) {
+        has_mount_options = true;
+        if (ReadArgs(state, argv, 5, &fs_type, &partition_type,
+                 &location, &mount_point, &mount_options) < 0) {
+            return NULL;
+        }
+    } else {
+        has_mount_options = false;
+        if (ReadArgs(state, argv, 4, &fs_type, &partition_type,
+                 &location, &mount_point) < 0) {
+            return NULL;
+        }
+    }
+
+    if (strlen(fs_type) == 0) {
+        ErrorAbort(state, "fs_type argument to %s() can't be empty", name);
+        goto done;
+    }
+    if (strlen(partition_type) == 0) {
+        ErrorAbort(state, "partition_type argument to %s() can't be empty",
+                   name);
+        goto done;
+    }
+    if (strlen(location) == 0) {
+        ErrorAbort(state, "location argument to %s() can't be empty", name);
+        goto done;
+    }
+    if (strlen(mount_point) == 0) {
+        ErrorAbort(state, "mount_point argument to %s() can't be empty", name);
+        goto done;
+    }
+
+    {
+        char *secontext = NULL;
+
+        if (sehandle) {
+            selabel_lookup(sehandle, &secontext, mount_point, 0755);
+            setfscreatecon(secontext);
+        }
+
+        mkdir(mount_point, 0755);
+
+        if (secontext) {
+            freecon(secontext);
+            setfscreatecon(NULL);
+        }
+    }
+
+    if (mount(location, mount_point, fs_type,
+              MS_NOATIME | MS_NODEV | MS_NODIRATIME,
+              has_mount_options ? mount_options : "") < 0) {
+        uiPrintf(state, "%s: failed to mount %s at %s: %s\n",
+                 name, location, mount_point, strerror(errno));
+        result = strdup("");
+    } else {
+        result = mount_point;
+    }
+
+done:
+    free(fs_type);
+    free(partition_type);
+    free(location);
+    if (result != mount_point) free(mount_point);
+    if (has_mount_options) free(mount_options);
+    return StringValue(result);
+}
+
+
+// is_mounted(mount_point)
+Value* IsMountedFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+    char* mount_point;
+    if (ReadArgs(state, argv, 1, &mount_point) < 0) {
+        return NULL;
+    }
+    if (strlen(mount_point) == 0) {
+        ErrorAbort(state, "mount_point argument to unmount() can't be empty");
+        goto done;
+    }
+
+    scan_mounted_volumes();
+    {
+        MountedVolume* vol = find_mounted_volume_by_mount_point(mount_point);
+        if (vol == NULL) {
+            result = strdup("");
+        } else {
+            result = mount_point;
+        }
+    }
+
+done:
+    if (result != mount_point) free(mount_point);
+    return StringValue(result);
+}
+
+
+Value* UnmountFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+    char* mount_point;
+    if (ReadArgs(state, argv, 1, &mount_point) < 0) {
+        return NULL;
+    }
+    if (strlen(mount_point) == 0) {
+        ErrorAbort(state, "mount_point argument to unmount() can't be empty");
+        goto done;
+    }
+
+    scan_mounted_volumes();
+    {
+        MountedVolume* vol = find_mounted_volume_by_mount_point(mount_point);
+        if (vol == NULL) {
+            uiPrintf(state, "unmount of %s failed; no such volume\n", mount_point);
+            result = strdup("");
+        } else {
+            int ret = unmount_mounted_volume(vol);
+            if (ret != 0) {
+                uiPrintf(state, "unmount of %s failed (%d): %s\n",
+                         mount_point, ret, strerror(errno));
+            }
+            result = mount_point;
+        }
+    }
+
+done:
+    if (result != mount_point) free(mount_point);
+    return StringValue(result);
+}
+
+static int exec_cmd(const char* path, char* const argv[]) {
+    int status;
+    pid_t child;
+    if ((child = vfork()) == 0) {
+        execv(path, argv);
+        _exit(-1);
+    }
+    waitpid(child, &status, 0);
+    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+        printf("%s failed with status %d\n", path, WEXITSTATUS(status));
+    }
+    return WEXITSTATUS(status);
+}
+
+
+// format(fs_type, partition_type, location, fs_size, mount_point)
+//
+//    fs_type="ext4"   partition_type="EMMC"    location=device    fs_size=<bytes> mount_point=<location>
+//    fs_type="f2fs"   partition_type="EMMC"    location=device    fs_size=<bytes> mount_point=<location>
+//    if fs_size == 0, then make fs uses the entire partition.
+//    if fs_size > 0, that is the size to use
+//    if fs_size < 0, then reserve that many bytes at the end of the partition (not for "f2fs")
+Value* FormatFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    if (argc != 5) {
+        return ErrorAbort(state, "%s() expects 5 args, got %d", name, argc);
+    }
+    char* fs_type;
+    char* partition_type;
+    char* location;
+    char* fs_size;
+    char* mount_point;
+
+    if (ReadArgs(state, argv, 5, &fs_type, &partition_type, &location, &fs_size, &mount_point) < 0) {
+        return NULL;
+    }
+
+    if (strlen(fs_type) == 0) {
+        ErrorAbort(state, "fs_type argument to %s() can't be empty", name);
+        goto done;
+    }
+    if (strlen(partition_type) == 0) {
+        ErrorAbort(state, "partition_type argument to %s() can't be empty",
+                   name);
+        goto done;
+    }
+    if (strlen(location) == 0) {
+        ErrorAbort(state, "location argument to %s() can't be empty", name);
+        goto done;
+    }
+
+    if (strlen(mount_point) == 0) {
+        ErrorAbort(state, "mount_point argument to %s() can't be empty", name);
+        goto done;
+    }
+
+    if (strcmp(fs_type, "ext4") == 0) {
+        int status = make_ext4fs(location, atoll(fs_size), mount_point, sehandle);
+        if (status != 0) {
+            printf("%s: make_ext4fs failed (%d) on %s",
+                    name, status, location);
+            result = strdup("");
+            goto done;
+        }
+        result = location;
+    } else if (strcmp(fs_type, "f2fs") == 0) {
+        char *num_sectors;
+        if (asprintf(&num_sectors, "%lld", atoll(fs_size) / 512) <= 0) {
+            printf("format_volume: failed to create %s command for %s\n", fs_type, location);
+            result = strdup("");
+            goto done;
+        }
+        const char *f2fs_path = "/sbin/mkfs.f2fs";
+        const char* const f2fs_argv[] = {"mkfs.f2fs", "-t", "-d1", location, num_sectors, NULL};
+        int status = exec_cmd(f2fs_path, (char* const*)f2fs_argv);
+        free(num_sectors);
+        if (status != 0) {
+            printf("%s: mkfs.f2fs failed (%d) on %s",
+                    name, status, location);
+            result = strdup("");
+            goto done;
+        }
+        result = location;
+    } else {
+        printf("%s: unsupported fs_type \"%s\" partition_type \"%s\"",
+                name, fs_type, partition_type);
+    }
+
+done:
+    free(fs_type);
+    free(partition_type);
+    if (result != location) free(location);
+    return StringValue(result);
+}
+
+Value* RenameFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+
+    char* src_name;
+    char* dst_name;
+
+    if (ReadArgs(state, argv, 2, &src_name, &dst_name) < 0) {
+        return NULL;
+    }
+    if (strlen(src_name) == 0) {
+        ErrorAbort(state, "src_name argument to %s() can't be empty", name);
+        goto done;
+    }
+    if (strlen(dst_name) == 0) {
+        ErrorAbort(state, "dst_name argument to %s() can't be empty", name);
+        goto done;
+    }
+    if (make_parents(dst_name) != 0) {
+        ErrorAbort(state, "Creating parent of %s failed, error %s",
+          dst_name, strerror(errno));
+    } else if (access(dst_name, F_OK) == 0 && access(src_name, F_OK) != 0) {
+        // File was already moved
+        result = dst_name;
+    } else if (rename(src_name, dst_name) != 0) {
+        ErrorAbort(state, "Rename of %s to %s failed, error %s",
+          src_name, dst_name, strerror(errno));
+    } else {
+        result = dst_name;
+    }
+
+done:
+    free(src_name);
+    if (result != dst_name) free(dst_name);
+    return StringValue(result);
+}
+
+Value* DeleteFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char** paths = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
+    for (int i = 0; i < argc; ++i) {
+        paths[i] = Evaluate(state, argv[i]);
+        if (paths[i] == NULL) {
+            for (int j = 0; j < i; ++j) {
+                free(paths[j]);
+            }
+            free(paths);
+            return NULL;
+        }
+    }
+
+    bool recursive = (strcmp(name, "delete_recursive") == 0);
+
+    int success = 0;
+    for (int i = 0; i < argc; ++i) {
+        if ((recursive ? dirUnlinkHierarchy(paths[i]) : unlink(paths[i])) == 0)
+            ++success;
+        free(paths[i]);
+    }
+    free(paths);
+
+    char buffer[10];
+    sprintf(buffer, "%d", success);
+    return StringValue(strdup(buffer));
+}
+
+
+Value* ShowProgressFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+    char* frac_str;
+    char* sec_str;
+    if (ReadArgs(state, argv, 2, &frac_str, &sec_str) < 0) {
+        return NULL;
+    }
+
+    double frac = strtod(frac_str, NULL);
+    int sec;
+    android::base::ParseInt(sec_str, &sec);
+
+    UpdaterInfo* ui = (UpdaterInfo*)(state->cookie);
+    fprintf(ui->cmd_pipe, "progress %f %d\n", frac, sec);
+
+    free(sec_str);
+    return StringValue(frac_str);
+}
+
+Value* SetProgressFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+    char* frac_str;
+    if (ReadArgs(state, argv, 1, &frac_str) < 0) {
+        return NULL;
+    }
+
+    double frac = strtod(frac_str, NULL);
+
+    UpdaterInfo* ui = (UpdaterInfo*)(state->cookie);
+    fprintf(ui->cmd_pipe, "set_progress %f\n", frac);
+
+    return StringValue(frac_str);
+}
+
+// package_extract_dir(package_path, destination_path)
+Value* PackageExtractDirFn(const char* name, State* state,
+                          int argc, Expr* argv[]) {
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+    char* zip_path;
+    char* dest_path;
+    if (ReadArgs(state, argv, 2, &zip_path, &dest_path) < 0) return NULL;
+
+    ZipArchive* za = ((UpdaterInfo*)(state->cookie))->package_zip;
+
+    // To create a consistent system image, never use the clock for timestamps.
+    struct utimbuf timestamp = { 1217592000, 1217592000 };  // 8/1/2008 default
+
+    bool success = mzExtractRecursive(za, zip_path, dest_path,
+                                      &timestamp,
+                                      NULL, NULL, sehandle);
+    free(zip_path);
+    free(dest_path);
+    return StringValue(strdup(success ? "t" : ""));
+}
+
+
+// package_extract_file(package_path, destination_path)
+//   or
+// package_extract_file(package_path)
+//   to return the entire contents of the file as the result of this
+//   function (the char* returned is actually a FileContents*).
+Value* PackageExtractFileFn(const char* name, State* state,
+                           int argc, Expr* argv[]) {
+    if (argc < 1 || argc > 2) {
+        return ErrorAbort(state, "%s() expects 1 or 2 args, got %d",
+                          name, argc);
+    }
+    bool success = false;
+
+    if (argc == 2) {
+        // The two-argument version extracts to a file.
+
+        ZipArchive* za = ((UpdaterInfo*)(state->cookie))->package_zip;
+
+        char* zip_path;
+        char* dest_path;
+        if (ReadArgs(state, argv, 2, &zip_path, &dest_path) < 0) return NULL;
+
+        const ZipEntry* entry = mzFindZipEntry(za, zip_path);
+        if (entry == NULL) {
+            printf("%s: no %s in package\n", name, zip_path);
+            goto done2;
+        }
+
+        {
+            int fd = TEMP_FAILURE_RETRY(ota_open(dest_path, O_WRONLY | O_CREAT | O_TRUNC,
+                  S_IRUSR | S_IWUSR));
+            if (fd == -1) {
+                printf("%s: can't open %s for write: %s\n", name, dest_path, strerror(errno));
+                goto done2;
+            }
+            success = mzExtractZipEntryToFile(za, entry, fd);
+            if (ota_fsync(fd) == -1) {
+                printf("fsync of \"%s\" failed: %s\n", dest_path, strerror(errno));
+                success = false;
+            }
+            if (ota_close(fd) == -1) {
+                printf("close of \"%s\" failed: %s\n", dest_path, strerror(errno));
+                success = false;
+            }
+        }
+
+      done2:
+        free(zip_path);
+        free(dest_path);
+        return StringValue(strdup(success ? "t" : ""));
+    } else {
+        // The one-argument version returns the contents of the file
+        // as the result.
+
+        char* zip_path;
+        if (ReadArgs(state, argv, 1, &zip_path) < 0) return NULL;
+
+        Value* v = reinterpret_cast<Value*>(malloc(sizeof(Value)));
+        v->type = VAL_BLOB;
+        v->size = -1;
+        v->data = NULL;
+
+        ZipArchive* za = ((UpdaterInfo*)(state->cookie))->package_zip;
+        const ZipEntry* entry = mzFindZipEntry(za, zip_path);
+        if (entry == NULL) {
+            printf("%s: no %s in package\n", name, zip_path);
+            goto done1;
+        }
+
+        v->size = mzGetZipEntryUncompLen(entry);
+        v->data = reinterpret_cast<char*>(malloc(v->size));
+        if (v->data == NULL) {
+            printf("%s: failed to allocate %zd bytes for %s\n",
+                    name, v->size, zip_path);
+            goto done1;
+        }
+
+        success = mzExtractZipEntryToBuffer(za, entry,
+                                            (unsigned char *)v->data);
+
+      done1:
+        free(zip_path);
+        if (!success) {
+            free(v->data);
+            v->data = NULL;
+            v->size = -1;
+        }
+        return v;
+    }
+}
+
+// Create all parent directories of name, if necessary.
+static int make_parents(char* name) {
+    char* p;
+    for (p = name + (strlen(name)-1); p > name; --p) {
+        if (*p != '/') continue;
+        *p = '\0';
+        if (make_parents(name) < 0) return -1;
+        int result = mkdir(name, 0700);
+        if (result == 0) printf("created [%s]\n", name);
+        *p = '/';
+        if (result == 0 || errno == EEXIST) {
+            // successfully created or already existed; we're done
+            return 0;
+        } else {
+            printf("failed to mkdir %s: %s\n", name, strerror(errno));
+            return -1;
+        }
+    }
+    return 0;
+}
+
+// symlink target src1 src2 ...
+//    unlinks any previously existing src1, src2, etc before creating symlinks.
+Value* SymlinkFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc == 0) {
+        return ErrorAbort(state, "%s() expects 1+ args, got %d", name, argc);
+    }
+    char* target;
+    target = Evaluate(state, argv[0]);
+    if (target == NULL) return NULL;
+
+    char** srcs = ReadVarArgs(state, argc-1, argv+1);
+    if (srcs == NULL) {
+        free(target);
+        return NULL;
+    }
+
+    int bad = 0;
+    int i;
+    for (i = 0; i < argc-1; ++i) {
+        if (unlink(srcs[i]) < 0) {
+            if (errno != ENOENT) {
+                printf("%s: failed to remove %s: %s\n",
+                        name, srcs[i], strerror(errno));
+                ++bad;
+            }
+        }
+        if (make_parents(srcs[i])) {
+            printf("%s: failed to symlink %s to %s: making parents failed\n",
+                    name, srcs[i], target);
+            ++bad;
+        }
+        if (symlink(target, srcs[i]) < 0) {
+            printf("%s: failed to symlink %s to %s: %s\n",
+                    name, srcs[i], target, strerror(errno));
+            ++bad;
+        }
+        free(srcs[i]);
+    }
+    free(srcs);
+    if (bad) {
+        return ErrorAbort(state, "%s: some symlinks failed", name);
+    }
+    return StringValue(strdup(""));
+}
+
+struct perm_parsed_args {
+    bool has_uid;
+    uid_t uid;
+    bool has_gid;
+    gid_t gid;
+    bool has_mode;
+    mode_t mode;
+    bool has_fmode;
+    mode_t fmode;
+    bool has_dmode;
+    mode_t dmode;
+    bool has_selabel;
+    char* selabel;
+    bool has_capabilities;
+    uint64_t capabilities;
+};
+
+static struct perm_parsed_args ParsePermArgs(State * state, int argc, char** args) {
+    int i;
+    struct perm_parsed_args parsed;
+    int bad = 0;
+    static int max_warnings = 20;
+
+    memset(&parsed, 0, sizeof(parsed));
+
+    for (i = 1; i < argc; i += 2) {
+        if (strcmp("uid", args[i]) == 0) {
+            int64_t uid;
+            if (sscanf(args[i+1], "%" SCNd64, &uid) == 1) {
+                parsed.uid = uid;
+                parsed.has_uid = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid UID \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("gid", args[i]) == 0) {
+            int64_t gid;
+            if (sscanf(args[i+1], "%" SCNd64, &gid) == 1) {
+                parsed.gid = gid;
+                parsed.has_gid = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid GID \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("mode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.mode = mode;
+                parsed.has_mode = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid mode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("dmode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.dmode = mode;
+                parsed.has_dmode = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid dmode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("fmode", args[i]) == 0) {
+            int32_t mode;
+            if (sscanf(args[i+1], "%" SCNi32, &mode) == 1) {
+                parsed.fmode = mode;
+                parsed.has_fmode = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid fmode \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("capabilities", args[i]) == 0) {
+            int64_t capabilities;
+            if (sscanf(args[i+1], "%" SCNi64, &capabilities) == 1) {
+                parsed.capabilities = capabilities;
+                parsed.has_capabilities = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid capabilities \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (strcmp("selabel", args[i]) == 0) {
+            if (args[i+1][0] != '\0') {
+                parsed.selabel = args[i+1];
+                parsed.has_selabel = true;
+            } else {
+                uiPrintf(state, "ParsePermArgs: invalid selabel \"%s\"\n", args[i + 1]);
+                bad++;
+            }
+            continue;
+        }
+        if (max_warnings != 0) {
+            printf("ParsedPermArgs: unknown key \"%s\", ignoring\n", args[i]);
+            max_warnings--;
+            if (max_warnings == 0) {
+                printf("ParsedPermArgs: suppressing further warnings\n");
+            }
+        }
+    }
+    return parsed;
+}
+
+static int ApplyParsedPerms(
+        State * state,
+        const char* filename,
+        const struct stat *statptr,
+        struct perm_parsed_args parsed)
+{
+    int bad = 0;
+
+    if (parsed.has_selabel) {
+        if (lsetfilecon(filename, parsed.selabel) != 0) {
+            uiPrintf(state, "ApplyParsedPerms: lsetfilecon of %s to %s failed: %s\n",
+                    filename, parsed.selabel, strerror(errno));
+            bad++;
+        }
+    }
+
+    /* ignore symlinks */
+    if (S_ISLNK(statptr->st_mode)) {
+        return bad;
+    }
+
+    if (parsed.has_uid) {
+        if (chown(filename, parsed.uid, -1) < 0) {
+            uiPrintf(state, "ApplyParsedPerms: chown of %s to %d failed: %s\n",
+                    filename, parsed.uid, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_gid) {
+        if (chown(filename, -1, parsed.gid) < 0) {
+            uiPrintf(state, "ApplyParsedPerms: chgrp of %s to %d failed: %s\n",
+                    filename, parsed.gid, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_mode) {
+        if (chmod(filename, parsed.mode) < 0) {
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                    filename, parsed.mode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_dmode && S_ISDIR(statptr->st_mode)) {
+        if (chmod(filename, parsed.dmode) < 0) {
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                    filename, parsed.dmode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_fmode && S_ISREG(statptr->st_mode)) {
+        if (chmod(filename, parsed.fmode) < 0) {
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                   filename, parsed.fmode, strerror(errno));
+            bad++;
+        }
+    }
+
+    if (parsed.has_capabilities && S_ISREG(statptr->st_mode)) {
+        if (parsed.capabilities == 0) {
+            if ((removexattr(filename, XATTR_NAME_CAPS) == -1) && (errno != ENODATA)) {
+                // Report failure unless it's ENODATA (attribute not set)
+                uiPrintf(state, "ApplyParsedPerms: removexattr of %s to %" PRIx64 " failed: %s\n",
+                       filename, parsed.capabilities, strerror(errno));
+                bad++;
+            }
+        } else {
+            struct vfs_cap_data cap_data;
+            memset(&cap_data, 0, sizeof(cap_data));
+            cap_data.magic_etc = VFS_CAP_REVISION | VFS_CAP_FLAGS_EFFECTIVE;
+            cap_data.data[0].permitted = (uint32_t) (parsed.capabilities & 0xffffffff);
+            cap_data.data[0].inheritable = 0;
+            cap_data.data[1].permitted = (uint32_t) (parsed.capabilities >> 32);
+            cap_data.data[1].inheritable = 0;
+            if (setxattr(filename, XATTR_NAME_CAPS, &cap_data, sizeof(cap_data), 0) < 0) {
+                uiPrintf(state, "ApplyParsedPerms: setcap of %s to %" PRIx64 " failed: %s\n",
+                        filename, parsed.capabilities, strerror(errno));
+                bad++;
+            }
+        }
+    }
+
+    return bad;
+}
+
+// nftw doesn't allow us to pass along context, so we need to use
+// global variables.  *sigh*
+static struct perm_parsed_args recursive_parsed_args;
+static State* recursive_state;
+
+static int do_SetMetadataRecursive(const char* filename, const struct stat *statptr,
+        int fileflags, struct FTW *pfwt) {
+    return ApplyParsedPerms(recursive_state, filename, statptr, recursive_parsed_args);
+}
+
+static Value* SetMetadataFn(const char* name, State* state, int argc, Expr* argv[]) {
+    int bad = 0;
+    struct stat sb;
+    Value* result = NULL;
+
+    bool recursive = (strcmp(name, "set_metadata_recursive") == 0);
+
+    if ((argc % 2) != 1) {
+        return ErrorAbort(state, "%s() expects an odd number of arguments, got %d", name, argc);
+    }
+
+    char** args = ReadVarArgs(state, argc, argv);
+    if (args == NULL) return NULL;
+
+    if (lstat(args[0], &sb) == -1) {
+        result = ErrorAbort(state, "%s: Error on lstat of \"%s\": %s", name, args[0], strerror(errno));
+        goto done;
+    }
+
+    {
+        struct perm_parsed_args parsed = ParsePermArgs(state, argc, args);
+
+        if (recursive) {
+            recursive_parsed_args = parsed;
+            recursive_state = state;
+            bad += nftw(args[0], do_SetMetadataRecursive, 30, FTW_CHDIR | FTW_DEPTH | FTW_PHYS);
+            memset(&recursive_parsed_args, 0, sizeof(recursive_parsed_args));
+            recursive_state = NULL;
+        } else {
+            bad += ApplyParsedPerms(state, args[0], &sb, parsed);
+        }
+    }
+
+done:
+    for (int i = 0; i < argc; ++i) {
+        free(args[i]);
+    }
+    free(args);
+
+    if (result != NULL) {
+        return result;
+    }
+
+    if (bad > 0) {
+        return ErrorAbort(state, "%s: some changes failed", name);
+    }
+
+    return StringValue(strdup(""));
+}
+
+Value* GetPropFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+    char* key = Evaluate(state, argv[0]);
+    if (key == NULL) return NULL;
+
+    char value[PROPERTY_VALUE_MAX];
+    property_get(key, value, "");
+    free(key);
+
+    return StringValue(strdup(value));
+}
+
+
+// file_getprop(file, key)
+//
+//   interprets 'file' as a getprop-style file (key=value pairs, one
+//   per line. # comment lines,blank lines, lines without '=' ignored),
+//   and returns the value for 'key' (or "" if it isn't defined).
+Value* FileGetPropFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char* result = NULL;
+    char* buffer = NULL;
+    char* filename;
+    char* key;
+    if (ReadArgs(state, argv, 2, &filename, &key) < 0) {
+        return NULL;
+    }
+
+    struct stat st;
+    if (stat(filename, &st) < 0) {
+        ErrorAbort(state, "%s: failed to stat \"%s\": %s", name, filename, strerror(errno));
+        goto done;
+    }
+
+#define MAX_FILE_GETPROP_SIZE    65536
+
+    if (st.st_size > MAX_FILE_GETPROP_SIZE) {
+        ErrorAbort(state, "%s too large for %s (max %d)", filename, name, MAX_FILE_GETPROP_SIZE);
+        goto done;
+    }
+
+    buffer = reinterpret_cast<char*>(malloc(st.st_size+1));
+    if (buffer == NULL) {
+        ErrorAbort(state, "%s: failed to alloc %zu bytes", name,
+                   static_cast<size_t>(st.st_size+1));
+        goto done;
+    }
+
+    FILE* f;
+    f = ota_fopen(filename, "rb");
+    if (f == NULL) {
+        ErrorAbort(state, "%s: failed to open %s: %s", name, filename, strerror(errno));
+        goto done;
+    }
+
+    if (ota_fread(buffer, 1, st.st_size, f) != static_cast<size_t>(st.st_size)) {
+        ErrorAbort(state, "%s: failed to read %zu bytes from %s",
+                   name, static_cast<size_t>(st.st_size), filename);
+        ota_fclose(f);
+        goto done;
+    }
+    buffer[st.st_size] = '\0';
+
+    ota_fclose(f);
+
+    char* line;
+    line = strtok(buffer, "\n");
+    do {
+        // skip whitespace at start of line
+        while (*line && isspace(*line)) ++line;
+
+        // comment or blank line: skip to next line
+        if (*line == '\0' || *line == '#') continue;
+
+        char* equal = strchr(line, '=');
+        if (equal == NULL) {
+            continue;
+        }
+
+        // trim whitespace between key and '='
+        char* key_end = equal-1;
+        while (key_end > line && isspace(*key_end)) --key_end;
+        key_end[1] = '\0';
+
+        // not the key we're looking for
+        if (strcmp(key, line) != 0) continue;
+
+        // skip whitespace after the '=' to the start of the value
+        char* val_start = equal+1;
+        while(*val_start && isspace(*val_start)) ++val_start;
+
+        // trim trailing whitespace
+        char* val_end = val_start + strlen(val_start)-1;
+        while (val_end > val_start && isspace(*val_end)) --val_end;
+        val_end[1] = '\0';
+
+        result = strdup(val_start);
+        break;
+
+    } while ((line = strtok(NULL, "\n")));
+
+    if (result == NULL) result = strdup("");
+
+  done:
+    free(filename);
+    free(key);
+    free(buffer);
+    return StringValue(result);
+}
+
+// apply_patch_space(bytes)
+Value* ApplyPatchSpaceFn(const char* name, State* state,
+                         int argc, Expr* argv[]) {
+    char* bytes_str;
+    if (ReadArgs(state, argv, 1, &bytes_str) < 0) {
+        return NULL;
+    }
+
+    size_t bytes;
+    if (!android::base::ParseUint(bytes_str, &bytes)) {
+        ErrorAbort(state, "%s(): can't parse \"%s\" as byte count\n\n", name, bytes_str);
+        free(bytes_str);
+        return nullptr;
+    }
+
+    return StringValue(strdup(CacheSizeCheck(bytes) ? "" : "t"));
+}
+
+// apply_patch(file, size, init_sha1, tgt_sha1, patch)
+
+Value* ApplyPatchFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc < 6 || (argc % 2) == 1) {
+        return ErrorAbort(state, "%s(): expected at least 6 args and an "
+                                 "even number, got %d",
+                          name, argc);
+    }
+
+    char* source_filename;
+    char* target_filename;
+    char* target_sha1;
+    char* target_size_str;
+    if (ReadArgs(state, argv, 4, &source_filename, &target_filename,
+                 &target_sha1, &target_size_str) < 0) {
+        return NULL;
+    }
+
+    size_t target_size;
+    if (!android::base::ParseUint(target_size_str, &target_size)) {
+        ErrorAbort(state, "%s(): can't parse \"%s\" as byte count", name, target_size_str);
+        free(source_filename);
+        free(target_filename);
+        free(target_sha1);
+        free(target_size_str);
+        return nullptr;
+    }
+
+    int patchcount = (argc-4) / 2;
+    std::unique_ptr<Value*, decltype(&free)> arg_values(ReadValueVarArgs(state, argc-4, argv+4),
+                                                        free);
+    if (!arg_values) {
+        return nullptr;
+    }
+    std::vector<std::unique_ptr<Value, decltype(&FreeValue)>> patch_shas;
+    std::vector<std::unique_ptr<Value, decltype(&FreeValue)>> patches;
+    // Protect values by unique_ptrs first to get rid of memory leak.
+    for (int i = 0; i < patchcount * 2; i += 2) {
+        patch_shas.emplace_back(arg_values.get()[i], FreeValue);
+        patches.emplace_back(arg_values.get()[i+1], FreeValue);
+    }
+
+    for (int i = 0; i < patchcount; ++i) {
+        if (patch_shas[i]->type != VAL_STRING) {
+            ErrorAbort(state, "%s(): sha-1 #%d is not string", name, i);
+            return nullptr;
+        }
+        if (patches[i]->type != VAL_BLOB) {
+            ErrorAbort(state, "%s(): patch #%d is not blob", name, i);
+            return nullptr;
+        }
+    }
+
+    std::vector<char*> patch_sha_str;
+    std::vector<Value*> patch_ptrs;
+    for (int i = 0; i < patchcount; ++i) {
+        patch_sha_str.push_back(patch_shas[i]->data);
+        patch_ptrs.push_back(patches[i].get());
+    }
+
+    int result = applypatch(source_filename, target_filename,
+                            target_sha1, target_size,
+                            patchcount, patch_sha_str.data(), patch_ptrs.data(), NULL);
+
+    return StringValue(strdup(result == 0 ? "t" : ""));
+}
+
+// apply_patch_check(file, [sha1_1, ...])
+Value* ApplyPatchCheckFn(const char* name, State* state,
+                         int argc, Expr* argv[]) {
+    if (argc < 1) {
+        return ErrorAbort(state, "%s(): expected at least 1 arg, got %d",
+                          name, argc);
+    }
+
+    char* filename;
+    if (ReadArgs(state, argv, 1, &filename) < 0) {
+        return NULL;
+    }
+
+    int patchcount = argc-1;
+    char** sha1s = ReadVarArgs(state, argc-1, argv+1);
+
+    int result = applypatch_check(filename, patchcount, sha1s);
+
+    int i;
+    for (i = 0; i < patchcount; ++i) {
+        free(sha1s[i]);
+    }
+    free(sha1s);
+
+    return StringValue(strdup(result == 0 ? "t" : ""));
+}
+
+// This is the updater side handler for ui_print() in edify script. Contents
+// will be sent over to the recovery side for on-screen display.
+Value* UIPrintFn(const char* name, State* state, int argc, Expr* argv[]) {
+    char** args = ReadVarArgs(state, argc, argv);
+    if (args == NULL) {
+        return NULL;
+    }
+
+    std::string buffer;
+    for (int i = 0; i < argc; ++i) {
+        buffer += args[i];
+        free(args[i]);
+    }
+    free(args);
+
+    buffer += "\n";
+    uiPrint(state, buffer);
+    return StringValue(strdup(buffer.c_str()));
+}
+
+Value* WipeCacheFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 0) {
+        return ErrorAbort(state, "%s() expects no args, got %d", name, argc);
+    }
+    fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe, "wipe_cache\n");
+    return StringValue(strdup("t"));
+}
+
+Value* RunProgramFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc < 1) {
+        return ErrorAbort(state, "%s() expects at least 1 arg", name);
+    }
+    char** args = ReadVarArgs(state, argc, argv);
+    if (args == NULL) {
+        return NULL;
+    }
+
+    char** args2 = reinterpret_cast<char**>(malloc(sizeof(char*) * (argc+1)));
+    memcpy(args2, args, sizeof(char*) * argc);
+    args2[argc] = NULL;
+
+    printf("about to run program [%s] with %d args\n", args2[0], argc);
+
+    pid_t child = fork();
+    if (child == 0) {
+        execv(args2[0], args2);
+        printf("run_program: execv failed: %s\n", strerror(errno));
+        _exit(1);
+    }
+    int status;
+    waitpid(child, &status, 0);
+    if (WIFEXITED(status)) {
+        if (WEXITSTATUS(status) != 0) {
+            printf("run_program: child exited with status %d\n",
+                    WEXITSTATUS(status));
+        }
+    } else if (WIFSIGNALED(status)) {
+        printf("run_program: child terminated by signal %d\n",
+                WTERMSIG(status));
+    }
+
+    int i;
+    for (i = 0; i < argc; ++i) {
+        free(args[i]);
+    }
+    free(args);
+    free(args2);
+
+    char buffer[20];
+    sprintf(buffer, "%d", status);
+
+    return StringValue(strdup(buffer));
+}
+
+// sha1_check(data)
+//    to return the sha1 of the data (given in the format returned by
+//    read_file).
+//
+// sha1_check(data, sha1_hex, [sha1_hex, ...])
+//    returns the sha1 of the file if it matches any of the hex
+//    strings passed, or "" if it does not equal any of them.
+//
+Value* Sha1CheckFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc < 1) {
+        return ErrorAbort(state, "%s() expects at least 1 arg", name);
+    }
+
+    std::unique_ptr<Value*, decltype(&free)> arg_values(ReadValueVarArgs(state, argc, argv), free);
+    if (arg_values == nullptr) {
+        return nullptr;
+    }
+    std::vector<std::unique_ptr<Value, decltype(&FreeValue)>> args;
+    for (int i = 0; i < argc; ++i) {
+        args.emplace_back(arg_values.get()[i], FreeValue);
+    }
+
+    if (args[0]->size < 0) {
+        return StringValue(strdup(""));
+    }
+    uint8_t digest[SHA_DIGEST_LENGTH];
+    SHA1(reinterpret_cast<uint8_t*>(args[0]->data), args[0]->size, digest);
+
+    if (argc == 1) {
+        return StringValue(PrintSha1(digest));
+    }
+
+    int i;
+    uint8_t arg_digest[SHA_DIGEST_LENGTH];
+    for (i = 1; i < argc; ++i) {
+        if (args[i]->type != VAL_STRING) {
+            printf("%s(): arg %d is not a string; skipping",
+                    name, i);
+        } else if (ParseSha1(args[i]->data, arg_digest) != 0) {
+            // Warn about bad args and skip them.
+            printf("%s(): error parsing \"%s\" as sha-1; skipping",
+                   name, args[i]->data);
+        } else if (memcmp(digest, arg_digest, SHA_DIGEST_LENGTH) == 0) {
+            break;
+        }
+    }
+    if (i >= argc) {
+        // Didn't match any of the hex strings; return false.
+        return StringValue(strdup(""));
+    }
+    // Found a match.
+    return args[i].release();
+}
+
+// Read a local file and return its contents (the Value* returned
+// is actually a FileContents*).
+Value* ReadFileFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+    char* filename;
+    if (ReadArgs(state, argv, 1, &filename) < 0) return NULL;
+
+    Value* v = static_cast<Value*>(malloc(sizeof(Value)));
+    if (v == nullptr) {
+        return nullptr;
+    }
+    v->type = VAL_BLOB;
+    v->size = -1;
+    v->data = nullptr;
+
+    FileContents fc;
+    if (LoadFileContents(filename, &fc) == 0) {
+        v->data = static_cast<char*>(malloc(fc.data.size()));
+        if (v->data != nullptr) {
+            memcpy(v->data, fc.data.data(), fc.data.size());
+            v->size = fc.data.size();
+        }
+    }
+    free(filename);
+    return v;
+}
+
+// Immediately reboot the device.  Recovery is not finished normally,
+// so if you reboot into recovery it will re-start applying the
+// current package (because nothing has cleared the copy of the
+// arguments stored in the BCB).
+//
+// The argument is the partition name passed to the android reboot
+// property.  It can be "recovery" to boot from the recovery
+// partition, or "" (empty string) to boot from the regular boot
+// partition.
+Value* RebootNowFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+
+    char* filename;
+    char* property;
+    if (ReadArgs(state, argv, 2, &filename, &property) < 0) return NULL;
+
+    char buffer[80];
+
+    // zero out the 'command' field of the bootloader message.
+    memset(buffer, 0, sizeof(((struct bootloader_message*)0)->command));
+    FILE* f = ota_fopen(filename, "r+b");
+    fseek(f, offsetof(struct bootloader_message, command), SEEK_SET);
+    ota_fwrite(buffer, sizeof(((struct bootloader_message*)0)->command), 1, f);
+    ota_fclose(f);
+    free(filename);
+
+    strcpy(buffer, "reboot,");
+    if (property != NULL) {
+        strncat(buffer, property, sizeof(buffer)-10);
+    }
+
+    property_set(ANDROID_RB_PROPERTY, buffer);
+
+    sleep(5);
+    free(property);
+    ErrorAbort(state, "%s() failed to reboot", name);
+    return NULL;
+}
+
+// Store a string value somewhere that future invocations of recovery
+// can access it.  This value is called the "stage" and can be used to
+// drive packages that need to do reboots in the middle of
+// installation and keep track of where they are in the multi-stage
+// install.
+//
+// The first argument is the block device for the misc partition
+// ("/misc" in the fstab), which is where this value is stored.  The
+// second argument is the string to store; it should not exceed 31
+// bytes.
+Value* SetStageFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+
+    char* filename;
+    char* stagestr;
+    if (ReadArgs(state, argv, 2, &filename, &stagestr) < 0) return NULL;
+
+    // Store this value in the misc partition, immediately after the
+    // bootloader message that the main recovery uses to save its
+    // arguments in case of the device restarting midway through
+    // package installation.
+    FILE* f = ota_fopen(filename, "r+b");
+    fseek(f, offsetof(struct bootloader_message, stage), SEEK_SET);
+    int to_write = strlen(stagestr)+1;
+    int max_size = sizeof(((struct bootloader_message*)0)->stage);
+    if (to_write > max_size) {
+        to_write = max_size;
+        stagestr[max_size-1] = 0;
+    }
+    ota_fwrite(stagestr, to_write, 1, f);
+    ota_fclose(f);
+
+    free(stagestr);
+    return StringValue(filename);
+}
+
+// Return the value most recently saved with SetStageFn.  The argument
+// is the block device for the misc partition.
+Value* GetStageFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 1) {
+        return ErrorAbort(state, "%s() expects 1 arg, got %d", name, argc);
+    }
+
+    char* filename;
+    if (ReadArgs(state, argv, 1, &filename) < 0) return NULL;
+
+    char buffer[sizeof(((struct bootloader_message*)0)->stage)];
+    FILE* f = ota_fopen(filename, "rb");
+    fseek(f, offsetof(struct bootloader_message, stage), SEEK_SET);
+    ota_fread(buffer, sizeof(buffer), 1, f);
+    ota_fclose(f);
+    buffer[sizeof(buffer)-1] = '\0';
+
+    return StringValue(strdup(buffer));
+}
+
+Value* WipeBlockDeviceFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 2) {
+        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
+    }
+
+    char* filename;
+    char* len_str;
+    if (ReadArgs(state, argv, 2, &filename, &len_str) < 0) return NULL;
+
+    size_t len;
+    android::base::ParseUint(len_str, &len);
+    int fd = ota_open(filename, O_WRONLY, 0644);
+    int success = wipe_block_device(fd, len);
+
+    free(filename);
+    free(len_str);
+
+    ota_close(fd);
+
+    return StringValue(strdup(success ? "t" : ""));
+}
+
+Value* EnableRebootFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc != 0) {
+        return ErrorAbort(state, "%s() expects no args, got %d", name, argc);
+    }
+    UpdaterInfo* ui = (UpdaterInfo*)(state->cookie);
+    fprintf(ui->cmd_pipe, "enable_reboot\n");
+    return StringValue(strdup("t"));
+}
+
+Value* Tune2FsFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc == 0) {
+        return ErrorAbort(state, "%s() expects args, got %d", name, argc);
+    }
+
+    char** args = ReadVarArgs(state, argc, argv);
+    if (args == NULL) {
+        return ErrorAbort(state, "%s() could not read args", name);
+    }
+
+    char** args2 = reinterpret_cast<char**>(malloc(sizeof(char*) * (argc+1)));
+    // Tune2fs expects the program name as its args[0]
+    args2[0] = strdup(name);
+    for (int i = 0; i < argc; ++i) {
+       args2[i + 1] = args[i];
+    }
+    int result = tune2fs_main(argc + 1, args2);
+    for (int i = 0; i < argc; ++i) {
+        free(args[i]);
+    }
+    free(args);
+
+    free(args2[0]);
+    free(args2);
+    if (result != 0) {
+        return ErrorAbort(state, "%s() returned error code %d", name, result);
+    }
+    return StringValue(strdup("t"));
+}
+
+void RegisterInstallFunctions() {
+    RegisterFunction("mount", MountFn);
+    RegisterFunction("is_mounted", IsMountedFn);
+    RegisterFunction("unmount", UnmountFn);
+    RegisterFunction("format", FormatFn);
+    RegisterFunction("show_progress", ShowProgressFn);
+    RegisterFunction("set_progress", SetProgressFn);
+    RegisterFunction("delete", DeleteFn);
+    RegisterFunction("delete_recursive", DeleteFn);
+    RegisterFunction("package_extract_dir", PackageExtractDirFn);
+    RegisterFunction("package_extract_file", PackageExtractFileFn);
+    RegisterFunction("symlink", SymlinkFn);
+
+    // Usage:
+    //   set_metadata("filename", "key1", "value1", "key2", "value2", ...)
+    // Example:
+    //   set_metadata("/system/bin/netcfg", "uid", 0, "gid", 3003, "mode", 02750, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
+    RegisterFunction("set_metadata", SetMetadataFn);
+
+    // Usage:
+    //   set_metadata_recursive("dirname", "key1", "value1", "key2", "value2", ...)
+    // Example:
+    //   set_metadata_recursive("/system", "uid", 0, "gid", 0, "fmode", 0644, "dmode", 0755, "selabel", "u:object_r:system_file:s0", "capabilities", 0x0);
+    RegisterFunction("set_metadata_recursive", SetMetadataFn);
+
+    RegisterFunction("getprop", GetPropFn);
+    RegisterFunction("file_getprop", FileGetPropFn);
+
+    RegisterFunction("apply_patch", ApplyPatchFn);
+    RegisterFunction("apply_patch_check", ApplyPatchCheckFn);
+    RegisterFunction("apply_patch_space", ApplyPatchSpaceFn);
+
+    RegisterFunction("wipe_block_device", WipeBlockDeviceFn);
+
+    RegisterFunction("read_file", ReadFileFn);
+    RegisterFunction("sha1_check", Sha1CheckFn);
+    RegisterFunction("rename", RenameFn);
+
+    RegisterFunction("wipe_cache", WipeCacheFn);
+
+    RegisterFunction("ui_print", UIPrintFn);
+
+    RegisterFunction("run_program", RunProgramFn);
+
+    RegisterFunction("reboot_now", RebootNowFn);
+    RegisterFunction("get_stage", GetStageFn);
+    RegisterFunction("set_stage", SetStageFn);
+
+    RegisterFunction("enable_reboot", EnableRebootFn);
+    RegisterFunction("tune2fs", Tune2FsFn);
+}
diff --git a/recovery/updater/install.h b/recovery/updater/install.h
new file mode 100644
index 0000000..b3b8a4d
--- /dev/null
+++ b/recovery/updater/install.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#ifndef _UPDATER_INSTALL_H_
+#define _UPDATER_INSTALL_H_
+
+void RegisterInstallFunctions();
+
+// uiPrintf function prints msg to screen as well as logs
+void uiPrintf(State* _Nonnull state, const char* _Nonnull format, ...) __attribute__((__format__(printf, 2, 3)));
+
+static int make_parents(char* _Nonnull name);
+
+#endif
diff --git a/recovery/updater/updater.cpp b/recovery/updater/updater.cpp
new file mode 100644
index 0000000..0497d6a
--- /dev/null
+++ b/recovery/updater/updater.cpp
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "edify/expr.h"
+#include "updater.h"
+#include "install.h"
+#include "blockimg.h"
+#include "minzip/Zip.h"
+#include "minzip/SysUtil.h"
+#include "config.h"
+
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+// Generated by the makefile, this function defines the
+// RegisterDeviceExtensions() function, which calls all the
+// registration functions for device-specific extensions.
+#include "register.inc"
+
+// Where in the package we expect to find the edify script to execute.
+// (Note it's "updateR-script", not the older "update-script".)
+#define SCRIPT_NAME "META-INF/com/google/android/updater-script"
+
+extern bool have_eio_error;
+
+struct selabel_handle *sehandle;
+
+int main(int argc, char** argv) {
+    // Various things log information to stdout or stderr more or less
+    // at random (though we've tried to standardize on stdout).  The
+    // log file makes more sense if buffering is turned off so things
+    // appear in the right order.
+    setbuf(stdout, NULL);
+    setbuf(stderr, NULL);
+
+    if (argc != 4) {
+        printf("unexpected number of arguments (%d)\n", argc);
+        return 1;
+    }
+
+    char* version = argv[1];
+    if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||
+        version[1] != '\0') {
+        // We support version 1, 2, or 3.
+        printf("wrong updater binary API; expected 1, 2, or 3; "
+                        "got %s\n",
+                argv[1]);
+        return 2;
+    }
+
+    // Set up the pipe for sending commands back to the parent process.
+
+    int fd = atoi(argv[2]);
+    FILE* cmd_pipe = fdopen(fd, "wb");
+    setlinebuf(cmd_pipe);
+
+    // Extract the script from the package.
+
+    const char* package_filename = argv[3];
+    MemMapping map;
+    if (sysMapFile(package_filename, &map) != 0) {
+        printf("failed to map package %s\n", argv[3]);
+        return 3;
+    }
+    ZipArchive za;
+    int err;
+    err = mzOpenZipArchive(map.addr, map.length, &za);
+    if (err != 0) {
+        printf("failed to open package %s: %s\n",
+               argv[3], strerror(err));
+        return 3;
+    }
+    ota_io_init(&za);
+
+    const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME);
+    if (script_entry == NULL) {
+        printf("failed to find %s in %s\n", SCRIPT_NAME, package_filename);
+        return 4;
+    }
+
+    char* script = reinterpret_cast<char*>(malloc(script_entry->uncompLen+1));
+    if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) {
+        printf("failed to read script from package\n");
+        return 5;
+    }
+    script[script_entry->uncompLen] = '\0';
+
+    // Configure edify's functions.
+
+    RegisterBuiltins();
+    RegisterInstallFunctions();
+    RegisterBlockImageFunctions();
+    RegisterDeviceExtensions();
+    FinishRegistration();
+
+    // Parse the script.
+
+    Expr* root;
+    int error_count = 0;
+    int error = parse_string(script, &root, &error_count);
+    if (error != 0 || error_count > 0) {
+        printf("%d parse errors\n", error_count);
+        return 6;
+    }
+
+    struct selinux_opt seopts[] = {
+      { SELABEL_OPT_PATH, "/file_contexts" }
+    };
+
+    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);
+
+    if (!sehandle) {
+        fprintf(cmd_pipe, "ui_print Warning: No file_contexts\n");
+    }
+
+    // Evaluate the parsed script.
+
+    UpdaterInfo updater_info;
+    updater_info.cmd_pipe = cmd_pipe;
+    updater_info.package_zip = &za;
+    updater_info.version = atoi(version);
+    updater_info.package_zip_addr = map.addr;
+    updater_info.package_zip_len = map.length;
+
+    State state;
+    state.cookie = &updater_info;
+    state.script = script;
+    state.errmsg = NULL;
+
+    char* result = Evaluate(&state, root);
+
+    if (have_eio_error) {
+        fprintf(cmd_pipe, "retry_update\n");
+    }
+
+    if (result == NULL) {
+        if (state.errmsg == NULL) {
+            printf("script aborted (no error message)\n");
+            fprintf(cmd_pipe, "ui_print script aborted (no error message)\n");
+        } else {
+            printf("script aborted: %s\n", state.errmsg);
+            char* line = strtok(state.errmsg, "\n");
+            while (line) {
+                fprintf(cmd_pipe, "ui_print %s\n", line);
+                line = strtok(NULL, "\n");
+            }
+            fprintf(cmd_pipe, "ui_print\n");
+        }
+        free(state.errmsg);
+        return 7;
+    } else {
+        fprintf(cmd_pipe, "ui_print script succeeded: result was [%s]\n", result);
+        free(result);
+    }
+
+    if (updater_info.package_zip) {
+        mzCloseZipArchive(updater_info.package_zip);
+    }
+    sysReleaseMap(&map);
+    free(script);
+
+    return 0;
+}
diff --git a/recovery/updater/updater.h b/recovery/updater/updater.h
new file mode 100644
index 0000000..d3a09b9
--- /dev/null
+++ b/recovery/updater/updater.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#ifndef _UPDATER_UPDATER_H_
+#define _UPDATER_UPDATER_H_
+
+#include <stdio.h>
+#include "minzip/Zip.h"
+
+typedef struct {
+    FILE* cmd_pipe;
+    ZipArchive* package_zip;
+    int version;
+
+    uint8_t* package_zip_addr;
+    size_t package_zip_len;
+} UpdaterInfo;
+
+struct selabel_handle;
+extern struct selabel_handle *sehandle;
+
+#endif
diff --git a/recovery/verifier.cpp b/recovery/verifier.cpp
new file mode 100644
index 0000000..1d6cf81
--- /dev/null
+++ b/recovery/verifier.cpp
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#include <errno.h>
+#include <malloc.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <algorithm>
+#include <memory>
+
+#include <openssl/bn.h>
+#include <openssl/ecdsa.h>
+#include <openssl/obj_mac.h>
+
+#include "asn1_decoder.h"
+#include "common.h"
+#include "ui.h"
+#include "verifier.h"
+
+extern RecoveryUI* ui;
+
+/*
+ * Simple version of PKCS#7 SignedData extraction. This extracts the
+ * signature OCTET STRING to be used for signature verification.
+ *
+ * For full details, see http://www.ietf.org/rfc/rfc3852.txt
+ *
+ * The PKCS#7 structure looks like:
+ *
+ *   SEQUENCE (ContentInfo)
+ *     OID (ContentType)
+ *     [0] (content)
+ *       SEQUENCE (SignedData)
+ *         INTEGER (version CMSVersion)
+ *         SET (DigestAlgorithmIdentifiers)
+ *         SEQUENCE (EncapsulatedContentInfo)
+ *         [0] (CertificateSet OPTIONAL)
+ *         [1] (RevocationInfoChoices OPTIONAL)
+ *         SET (SignerInfos)
+ *           SEQUENCE (SignerInfo)
+ *             INTEGER (CMSVersion)
+ *             SEQUENCE (SignerIdentifier)
+ *             SEQUENCE (DigestAlgorithmIdentifier)
+ *             SEQUENCE (SignatureAlgorithmIdentifier)
+ *             OCTET STRING (SignatureValue)
+ */
+static bool read_pkcs7(uint8_t* pkcs7_der, size_t pkcs7_der_len, uint8_t** sig_der,
+        size_t* sig_der_length) {
+    asn1_context_t* ctx = asn1_context_new(pkcs7_der, pkcs7_der_len);
+    if (ctx == NULL) {
+        return false;
+    }
+
+    asn1_context_t* pkcs7_seq = asn1_sequence_get(ctx);
+    if (pkcs7_seq != NULL && asn1_sequence_next(pkcs7_seq)) {
+        asn1_context_t *signed_data_app = asn1_constructed_get(pkcs7_seq);
+        if (signed_data_app != NULL) {
+            asn1_context_t* signed_data_seq = asn1_sequence_get(signed_data_app);
+            if (signed_data_seq != NULL
+                    && asn1_sequence_next(signed_data_seq)
+                    && asn1_sequence_next(signed_data_seq)
+                    && asn1_sequence_next(signed_data_seq)
+                    && asn1_constructed_skip_all(signed_data_seq)) {
+                asn1_context_t *sig_set = asn1_set_get(signed_data_seq);
+                if (sig_set != NULL) {
+                    asn1_context_t* sig_seq = asn1_sequence_get(sig_set);
+                    if (sig_seq != NULL
+                            && asn1_sequence_next(sig_seq)
+                            && asn1_sequence_next(sig_seq)
+                            && asn1_sequence_next(sig_seq)
+                            && asn1_sequence_next(sig_seq)) {
+                        uint8_t* sig_der_ptr;
+                        if (asn1_octet_string_get(sig_seq, &sig_der_ptr, sig_der_length)) {
+                            *sig_der = (uint8_t*) malloc(*sig_der_length);
+                            if (*sig_der != NULL) {
+                                memcpy(*sig_der, sig_der_ptr, *sig_der_length);
+                            }
+                        }
+                        asn1_context_free(sig_seq);
+                    }
+                    asn1_context_free(sig_set);
+                }
+                asn1_context_free(signed_data_seq);
+            }
+            asn1_context_free(signed_data_app);
+        }
+        asn1_context_free(pkcs7_seq);
+    }
+    asn1_context_free(ctx);
+
+    return *sig_der != NULL;
+}
+
+// Look for an RSA signature embedded in the .ZIP file comment given
+// the path to the zip.  Verify it matches one of the given public
+// keys.
+//
+// Return VERIFY_SUCCESS, VERIFY_FAILURE (if any error is encountered
+// or no key matches the signature).
+
+int verify_file(unsigned char* addr, size_t length,
+                const std::vector<Certificate>& keys) {
+    ui->SetProgress(0.0);
+
+    // An archive with a whole-file signature will end in six bytes:
+    //
+    //   (2-byte signature start) $ff $ff (2-byte comment size)
+    //
+    // (As far as the ZIP format is concerned, these are part of the
+    // archive comment.)  We start by reading this footer, this tells
+    // us how far back from the end we have to start reading to find
+    // the whole comment.
+
+#define FOOTER_SIZE 6
+
+    if (length < FOOTER_SIZE) {
+        LOGE("not big enough to contain footer\n");
+        return VERIFY_FAILURE;
+    }
+
+    unsigned char* footer = addr + length - FOOTER_SIZE;
+
+    if (footer[2] != 0xff || footer[3] != 0xff) {
+        LOGE("footer is wrong\n");
+        return VERIFY_FAILURE;
+    }
+
+    size_t comment_size = footer[4] + (footer[5] << 8);
+    size_t signature_start = footer[0] + (footer[1] << 8);
+    LOGI("comment is %zu bytes; signature %zu bytes from end\n",
+         comment_size, signature_start);
+
+    if (signature_start <= FOOTER_SIZE) {
+        LOGE("Signature start is in the footer");
+        return VERIFY_FAILURE;
+    }
+
+#define EOCD_HEADER_SIZE 22
+
+    // The end-of-central-directory record is 22 bytes plus any
+    // comment length.
+    size_t eocd_size = comment_size + EOCD_HEADER_SIZE;
+
+    if (length < eocd_size) {
+        LOGE("not big enough to contain EOCD\n");
+        return VERIFY_FAILURE;
+    }
+
+    // Determine how much of the file is covered by the signature.
+    // This is everything except the signature data and length, which
+    // includes all of the EOCD except for the comment length field (2
+    // bytes) and the comment data.
+    size_t signed_len = length - eocd_size + EOCD_HEADER_SIZE - 2;
+
+    unsigned char* eocd = addr + length - eocd_size;
+
+    // If this is really is the EOCD record, it will begin with the
+    // magic number $50 $4b $05 $06.
+    if (eocd[0] != 0x50 || eocd[1] != 0x4b ||
+        eocd[2] != 0x05 || eocd[3] != 0x06) {
+        LOGE("signature length doesn't match EOCD marker\n");
+        return VERIFY_FAILURE;
+    }
+
+    for (size_t i = 4; i < eocd_size-3; ++i) {
+        if (eocd[i  ] == 0x50 && eocd[i+1] == 0x4b &&
+            eocd[i+2] == 0x05 && eocd[i+3] == 0x06) {
+            // if the sequence $50 $4b $05 $06 appears anywhere after
+            // the real one, minzip will find the later (wrong) one,
+            // which could be exploitable.  Fail verification if
+            // this sequence occurs anywhere after the real one.
+            LOGE("EOCD marker occurs after start of EOCD\n");
+            return VERIFY_FAILURE;
+        }
+    }
+
+#define BUFFER_SIZE 4096
+
+    bool need_sha1 = false;
+    bool need_sha256 = false;
+    for (const auto& key : keys) {
+        switch (key.hash_len) {
+            case SHA_DIGEST_LENGTH: need_sha1 = true; break;
+            case SHA256_DIGEST_LENGTH: need_sha256 = true; break;
+        }
+    }
+
+    SHA_CTX sha1_ctx;
+    SHA256_CTX sha256_ctx;
+    SHA1_Init(&sha1_ctx);
+    SHA256_Init(&sha256_ctx);
+
+    double frac = -1.0;
+    size_t so_far = 0;
+    while (so_far < signed_len) {
+        size_t size = signed_len - so_far;
+        if (size > BUFFER_SIZE) size = BUFFER_SIZE;
+
+        if (need_sha1) SHA1_Update(&sha1_ctx, addr + so_far, size);
+        if (need_sha256) SHA256_Update(&sha256_ctx, addr + so_far, size);
+        so_far += size;
+
+        double f = so_far / (double)signed_len;
+        if (f > frac + 0.02 || size == so_far) {
+            ui->SetProgress(f);
+            frac = f;
+        }
+    }
+
+    uint8_t sha1[SHA_DIGEST_LENGTH];
+    SHA1_Final(sha1, &sha1_ctx);
+    uint8_t sha256[SHA256_DIGEST_LENGTH];
+    SHA256_Final(sha256, &sha256_ctx);
+
+    uint8_t* sig_der = nullptr;
+    size_t sig_der_length = 0;
+
+    size_t signature_size = signature_start - FOOTER_SIZE;
+    if (!read_pkcs7(eocd + eocd_size - signature_start, signature_size, &sig_der,
+            &sig_der_length)) {
+        LOGE("Could not find signature DER block\n");
+        return VERIFY_FAILURE;
+    }
+
+    /*
+     * Check to make sure at least one of the keys matches the signature. Since
+     * any key can match, we need to try each before determining a verification
+     * failure has happened.
+     */
+    size_t i = 0;
+    for (const auto& key : keys) {
+        const uint8_t* hash;
+        int hash_nid;
+        switch (key.hash_len) {
+            case SHA_DIGEST_LENGTH:
+                hash = sha1;
+                hash_nid = NID_sha1;
+                break;
+            case SHA256_DIGEST_LENGTH:
+                hash = sha256;
+                hash_nid = NID_sha256;
+                break;
+            default:
+                continue;
+        }
+
+        // The 6 bytes is the "(signature_start) $ff $ff (comment_size)" that
+        // the signing tool appends after the signature itself.
+        if (key.key_type == Certificate::KEY_TYPE_RSA) {
+            if (!RSA_verify(hash_nid, hash, key.hash_len, sig_der,
+                            sig_der_length, key.rsa.get())) {
+                LOGI("failed to verify against RSA key %zu\n", i);
+                continue;
+            }
+
+            LOGI("whole-file signature verified against RSA key %zu\n", i);
+            free(sig_der);
+            return VERIFY_SUCCESS;
+        } else if (key.key_type == Certificate::KEY_TYPE_EC
+                && key.hash_len == SHA256_DIGEST_LENGTH) {
+            if (!ECDSA_verify(0, hash, key.hash_len, sig_der,
+                              sig_der_length, key.ec.get())) {
+                LOGI("failed to verify against EC key %zu\n", i);
+                continue;
+            }
+
+            LOGI("whole-file signature verified against EC key %zu\n", i);
+            free(sig_der);
+            return VERIFY_SUCCESS;
+        } else {
+            LOGI("Unknown key type %d\n", key.key_type);
+        }
+        i++;
+    }
+    free(sig_der);
+    LOGE("failed to verify whole-file signature\n");
+    return VERIFY_FAILURE;
+}
+
+std::unique_ptr<RSA, RSADeleter> parse_rsa_key(FILE* file, uint32_t exponent) {
+    // Read key length in words and n0inv. n0inv is a precomputed montgomery
+    // parameter derived from the modulus and can be used to speed up
+    // verification. n0inv is 32 bits wide here, assuming the verification logic
+    // uses 32 bit arithmetic. However, BoringSSL may use a word size of 64 bits
+    // internally, in which case we don't have a valid n0inv. Thus, we just
+    // ignore the montgomery parameters and have BoringSSL recompute them
+    // internally. If/When the speedup from using the montgomery parameters
+    // becomes relevant, we can add more sophisticated code here to obtain a
+    // 64-bit n0inv and initialize the montgomery parameters in the key object.
+    uint32_t key_len_words = 0;
+    uint32_t n0inv = 0;
+    if (fscanf(file, " %i , 0x%x", &key_len_words, &n0inv) != 2) {
+        return nullptr;
+    }
+
+    if (key_len_words > 8192 / 32) {
+        LOGE("key length (%d) too large\n", key_len_words);
+        return nullptr;
+    }
+
+    // Read the modulus.
+    std::unique_ptr<uint32_t[]> modulus(new uint32_t[key_len_words]);
+    if (fscanf(file, " , { %u", &modulus[0]) != 1) {
+        return nullptr;
+    }
+    for (uint32_t i = 1; i < key_len_words; ++i) {
+        if (fscanf(file, " , %u", &modulus[i]) != 1) {
+            return nullptr;
+        }
+    }
+
+    // Cconvert from little-endian array of little-endian words to big-endian
+    // byte array suitable as input for BN_bin2bn.
+    std::reverse((uint8_t*)modulus.get(),
+                 (uint8_t*)(modulus.get() + key_len_words));
+
+    // The next sequence of values is the montgomery parameter R^2. Since we
+    // generally don't have a valid |n0inv|, we ignore this (see comment above).
+    uint32_t rr_value;
+    if (fscanf(file, " } , { %u", &rr_value) != 1) {
+        return nullptr;
+    }
+    for (uint32_t i = 1; i < key_len_words; ++i) {
+        if (fscanf(file, " , %u", &rr_value) != 1) {
+            return nullptr;
+        }
+    }
+    if (fscanf(file, " } } ") != 0) {
+        return nullptr;
+    }
+
+    // Initialize the key.
+    std::unique_ptr<RSA, RSADeleter> key(RSA_new());
+    if (!key) {
+      return nullptr;
+    }
+
+    key->n = BN_bin2bn((uint8_t*)modulus.get(),
+                       key_len_words * sizeof(uint32_t), NULL);
+    if (!key->n) {
+      return nullptr;
+    }
+
+    key->e = BN_new();
+    if (!key->e || !BN_set_word(key->e, exponent)) {
+      return nullptr;
+    }
+
+    return key;
+}
+
+struct BNDeleter {
+  void operator()(BIGNUM* bn) {
+    BN_free(bn);
+  }
+};
+
+std::unique_ptr<EC_KEY, ECKEYDeleter> parse_ec_key(FILE* file) {
+    uint32_t key_len_bytes = 0;
+    if (fscanf(file, " %i", &key_len_bytes) != 1) {
+        return nullptr;
+    }
+
+    std::unique_ptr<EC_GROUP, void (*)(EC_GROUP*)> group(
+        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1), EC_GROUP_free);
+    if (!group) {
+        return nullptr;
+    }
+
+    // Verify that |key_len| matches the group order.
+    if (key_len_bytes != BN_num_bytes(EC_GROUP_get0_order(group.get()))) {
+        return nullptr;
+    }
+
+    // Read the public key coordinates. Note that the byte order in the file is
+    // little-endian, so we convert to big-endian here.
+    std::unique_ptr<uint8_t[]> bytes(new uint8_t[key_len_bytes]);
+    std::unique_ptr<BIGNUM, BNDeleter> point[2];
+    for (int i = 0; i < 2; ++i) {
+        unsigned int byte = 0;
+        if (fscanf(file, " , { %u", &byte) != 1) {
+            return nullptr;
+        }
+        bytes[key_len_bytes - 1] = byte;
+
+        for (size_t i = 1; i < key_len_bytes; ++i) {
+            if (fscanf(file, " , %u", &byte) != 1) {
+                return nullptr;
+            }
+            bytes[key_len_bytes - i - 1] = byte;
+        }
+
+        point[i].reset(BN_bin2bn(bytes.get(), key_len_bytes, nullptr));
+        if (!point[i]) {
+            return nullptr;
+        }
+
+        if (fscanf(file, " }") != 0) {
+            return nullptr;
+        }
+    }
+
+    if (fscanf(file, " } ") != 0) {
+        return nullptr;
+    }
+
+    // Create and initialize the key.
+    std::unique_ptr<EC_KEY, ECKEYDeleter> key(EC_KEY_new());
+    if (!key || !EC_KEY_set_group(key.get(), group.get()) ||
+        !EC_KEY_set_public_key_affine_coordinates(key.get(), point[0].get(),
+                                                  point[1].get())) {
+        return nullptr;
+    }
+
+    return key;
+}
+
+// Reads a file containing one or more public keys as produced by
+// DumpPublicKey:  this is an RSAPublicKey struct as it would appear
+// as a C source literal, eg:
+//
+//  "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"
+//
+// For key versions newer than the original 2048-bit e=3 keys
+// supported by Android, the string is preceded by a version
+// identifier, eg:
+//
+//  "v2 {64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"
+//
+// (Note that the braces and commas in this example are actual
+// characters the parser expects to find in the file; the ellipses
+// indicate more numbers omitted from this example.)
+//
+// The file may contain multiple keys in this format, separated by
+// commas.  The last key must not be followed by a comma.
+//
+// A Certificate is a pair of an RSAPublicKey and a particular hash
+// (we support SHA-1 and SHA-256; we store the hash length to signify
+// which is being used).  The hash used is implied by the version number.
+//
+//       1: 2048-bit RSA key with e=3 and SHA-1 hash
+//       2: 2048-bit RSA key with e=65537 and SHA-1 hash
+//       3: 2048-bit RSA key with e=3 and SHA-256 hash
+//       4: 2048-bit RSA key with e=65537 and SHA-256 hash
+//       5: 256-bit EC key using the NIST P-256 curve parameters and SHA-256 hash
+//
+// Returns true on success, and appends the found keys (at least one) to certs.
+// Otherwise returns false if the file failed to parse, or if it contains zero
+// keys. The contents in certs would be unspecified on failure.
+bool load_keys(const char* filename, std::vector<Certificate>& certs) {
+    std::unique_ptr<FILE, decltype(&fclose)> f(fopen(filename, "r"), fclose);
+    if (!f) {
+        LOGE("opening %s: %s\n", filename, strerror(errno));
+        return false;
+    }
+
+    while (true) {
+        certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+        Certificate& cert = certs.back();
+        uint32_t exponent = 0;
+
+        char start_char;
+        if (fscanf(f.get(), " %c", &start_char) != 1) return false;
+        if (start_char == '{') {
+            // a version 1 key has no version specifier.
+            cert.key_type = Certificate::KEY_TYPE_RSA;
+            exponent = 3;
+            cert.hash_len = SHA_DIGEST_LENGTH;
+        } else if (start_char == 'v') {
+            int version;
+            if (fscanf(f.get(), "%d {", &version) != 1) return false;
+            switch (version) {
+                case 2:
+                    cert.key_type = Certificate::KEY_TYPE_RSA;
+                    exponent = 65537;
+                    cert.hash_len = SHA_DIGEST_LENGTH;
+                    break;
+                case 3:
+                    cert.key_type = Certificate::KEY_TYPE_RSA;
+                    exponent = 3;
+                    cert.hash_len = SHA256_DIGEST_LENGTH;
+                    break;
+                case 4:
+                    cert.key_type = Certificate::KEY_TYPE_RSA;
+                    exponent = 65537;
+                    cert.hash_len = SHA256_DIGEST_LENGTH;
+                    break;
+                case 5:
+                    cert.key_type = Certificate::KEY_TYPE_EC;
+                    cert.hash_len = SHA256_DIGEST_LENGTH;
+                    break;
+                default:
+                    return false;
+            }
+        }
+
+        if (cert.key_type == Certificate::KEY_TYPE_RSA) {
+            cert.rsa = parse_rsa_key(f.get(), exponent);
+            if (!cert.rsa) {
+              return false;
+            }
+
+            LOGI("read key e=%d hash=%d\n", exponent, cert.hash_len);
+        } else if (cert.key_type == Certificate::KEY_TYPE_EC) {
+            cert.ec = parse_ec_key(f.get());
+            if (!cert.ec) {
+              return false;
+            }
+        } else {
+            LOGE("Unknown key type %d\n", cert.key_type);
+            return false;
+        }
+
+        // if the line ends in a comma, this file has more keys.
+        int ch = fgetc(f.get());
+        if (ch == ',') {
+            // more keys to come.
+            continue;
+        } else if (ch == EOF) {
+            break;
+        } else {
+            LOGE("unexpected character between keys\n");
+            return false;
+        }
+    }
+
+    return true;
+}
diff --git a/recovery/verifier.h b/recovery/verifier.h
new file mode 100644
index 0000000..58083fe
--- /dev/null
+++ b/recovery/verifier.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+#ifndef _RECOVERY_VERIFIER_H
+#define _RECOVERY_VERIFIER_H
+
+#include <memory>
+#include <vector>
+
+#include <openssl/ec_key.h>
+#include <openssl/rsa.h>
+#include <openssl/sha.h>
+
+struct RSADeleter {
+  void operator()(RSA* rsa) {
+    RSA_free(rsa);
+  }
+};
+
+struct ECKEYDeleter {
+  void operator()(EC_KEY* ec_key) {
+    EC_KEY_free(ec_key);
+  }
+};
+
+struct Certificate {
+    typedef enum {
+        KEY_TYPE_RSA,
+        KEY_TYPE_EC,
+    } KeyType;
+
+    Certificate(int hash_len_,
+                KeyType key_type_,
+                std::unique_ptr<RSA, RSADeleter>&& rsa_,
+                std::unique_ptr<EC_KEY, ECKEYDeleter>&& ec_)
+        : hash_len(hash_len_),
+          key_type(key_type_),
+          rsa(std::move(rsa_)),
+          ec(std::move(ec_)) {}
+
+    // SHA_DIGEST_LENGTH (SHA-1) or SHA256_DIGEST_LENGTH (SHA-256)
+    int hash_len;
+    KeyType key_type;
+    std::unique_ptr<RSA, RSADeleter> rsa;
+    std::unique_ptr<EC_KEY, ECKEYDeleter> ec;
+};
+
+/* addr and length define a an update package file that has been
+ * loaded (or mmap'ed, or whatever) into memory.  Verify that the file
+ * is signed and the signature matches one of the given keys.  Return
+ * one of the constants below.
+ */
+int verify_file(unsigned char* addr, size_t length,
+                const std::vector<Certificate>& keys);
+
+bool load_keys(const char* filename, std::vector<Certificate>& certs);
+
+#define VERIFY_SUCCESS        0
+#define VERIFY_FAILURE        1
+
+#endif  /* _RECOVERY_VERIFIER_H */
diff --git a/recovery/wear_ui.cpp b/recovery/wear_ui.cpp
new file mode 100644
index 0000000..48278ff
--- /dev/null
+++ b/recovery/wear_ui.cpp
@@ -0,0 +1,655 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include "common.h"
+#include "device.h"
+#include "minui/minui.h"
+#include "wear_ui.h"
+#include "ui.h"
+#include "cutils/properties.h"
+#include "android-base/strings.h"
+
+static int char_width;
+static int char_height;
+
+// There's only (at most) one of these objects, and global callbacks
+// (for pthread_create, and the input event system) need to find it,
+// so use a global variable.
+static WearRecoveryUI* self = NULL;
+
+// Return the current time as a double (including fractions of a second).
+static double now() {
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return tv.tv_sec + tv.tv_usec / 1000000.0;
+}
+
+WearRecoveryUI::WearRecoveryUI() :
+    progress_bar_height(3),
+    progress_bar_width(200),
+    progress_bar_y(259),
+    outer_height(0),
+    outer_width(0),
+    menu_unusable_rows(0),
+    intro_frames(22),
+    loop_frames(60),
+    animation_fps(30),
+    currentIcon(NONE),
+    intro_done(false),
+    current_frame(0),
+    rtl_locale(false),
+    progressBarType(EMPTY),
+    progressScopeStart(0),
+    progressScopeSize(0),
+    progress(0),
+    text_cols(0),
+    text_rows(0),
+    text_col(0),
+    text_row(0),
+    text_top(0),
+    show_text(false),
+    show_text_ever(false),
+    show_menu(false),
+    menu_items(0),
+    menu_sel(0) {
+
+    for (size_t i = 0; i < 5; i++)
+        backgroundIcon[i] = NULL;
+
+    pthread_mutex_init(&updateMutex, NULL);
+    self = this;
+}
+
+// Draw background frame on the screen.  Does not flip pages.
+// Should only be called with updateMutex locked.
+void WearRecoveryUI::draw_background_locked(Icon icon)
+{
+    gr_color(0, 0, 0, 255);
+    gr_fill(0, 0, gr_fb_width(), gr_fb_height());
+
+    if (icon) {
+        GRSurface* surface;
+        if (icon == INSTALLING_UPDATE || icon == ERASING) {
+            if (!intro_done) {
+                surface = introFrames[current_frame];
+            } else {
+                surface = loopFrames[current_frame];
+            }
+        }
+        else {
+            surface = backgroundIcon[icon];
+        }
+
+        int width = gr_get_width(surface);
+        int height = gr_get_height(surface);
+
+        int x = (gr_fb_width() - width) / 2;
+        int y = (gr_fb_height() - height) / 2;
+
+        gr_blit(surface, 0, 0, width, height, x, y);
+    }
+}
+
+// Draw the progress bar (if any) on the screen.  Does not flip pages.
+// Should only be called with updateMutex locked.
+void WearRecoveryUI::draw_progress_locked()
+{
+    if (currentIcon == ERROR) return;
+    if (progressBarType != DETERMINATE) return;
+
+    int width = progress_bar_width;
+    int height = progress_bar_height;
+    int dx = (gr_fb_width() - width)/2;
+    int dy = progress_bar_y;
+
+    float p = progressScopeStart + progress * progressScopeSize;
+    int pos = (int) (p * width);
+
+    gr_color(0x43, 0x43, 0x43, 0xff);
+    gr_fill(dx, dy, dx + width, dy + height);
+
+    if (pos > 0) {
+        gr_color(0x02, 0xa8, 0xf3, 255);
+        if (rtl_locale) {
+            // Fill the progress bar from right to left.
+            gr_fill(dx + width - pos, dy, dx + width, dy + height);
+        } else {
+            // Fill the progress bar from left to right.
+            gr_fill(dx, dy, dx + pos, dy + height);
+        }
+    }
+}
+
+void WearRecoveryUI::SetColor(UIElement e) {
+    switch (e) {
+        case HEADER:
+            gr_color(247, 0, 6, 255);
+            break;
+        case MENU:
+        case MENU_SEL_BG:
+            gr_color(0, 106, 157, 255);
+            break;
+        case MENU_SEL_FG:
+            gr_color(255, 255, 255, 255);
+            break;
+        case LOG:
+            gr_color(249, 194, 0, 255);
+            break;
+        case TEXT_FILL:
+            gr_color(0, 0, 0, 160);
+            break;
+        default:
+            gr_color(255, 255, 255, 255);
+            break;
+    }
+}
+
+void WearRecoveryUI::DrawTextLine(int x, int* y, const char* line, bool bold) {
+    gr_text(x, *y, line, bold);
+    *y += char_height + 4;
+}
+
+void WearRecoveryUI::DrawTextLines(int x, int* y, const char* const* lines) {
+    for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
+        DrawTextLine(x, y, lines[i], false);
+    }
+}
+
+static const char* HEADERS[] = {
+    "Swipe up/down to move.",
+    "Swipe left/right to select.",
+    "",
+    NULL
+};
+
+void WearRecoveryUI::draw_screen_locked()
+{
+    draw_background_locked(currentIcon);
+    draw_progress_locked();
+    char cur_selection_str[50];
+
+    if (show_text) {
+        SetColor(TEXT_FILL);
+        gr_fill(0, 0, gr_fb_width(), gr_fb_height());
+
+        int y = outer_height;
+        int x = outer_width;
+        if (show_menu) {
+            char recovery_fingerprint[PROPERTY_VALUE_MAX];
+            property_get("ro.bootimage.build.fingerprint", recovery_fingerprint, "");
+            SetColor(HEADER);
+            DrawTextLine(x + 4, &y, "Android Recovery", true);
+            for (auto& chunk: android::base::Split(recovery_fingerprint, ":")) {
+                DrawTextLine(x +4, &y, chunk.c_str(), false);
+            }
+
+            // This is actually the help strings.
+            DrawTextLines(x + 4, &y, HEADERS);
+            SetColor(HEADER);
+            DrawTextLines(x + 4, &y, menu_headers_);
+
+            // Show the current menu item number in relation to total number if
+            // items don't fit on the screen.
+            if (menu_items > menu_end - menu_start) {
+                sprintf(cur_selection_str, "Current item: %d/%d", menu_sel + 1, menu_items);
+                gr_text(x+4, y, cur_selection_str, 1);
+                y += char_height+4;
+            }
+
+            // Menu begins here
+            SetColor(MENU);
+
+            for (int i = menu_start; i < menu_end; ++i) {
+
+                if (i == menu_sel) {
+                    // draw the highlight bar
+                    SetColor(MENU_SEL_BG);
+                    gr_fill(x, y-2, gr_fb_width()-x, y+char_height+2);
+                    // white text of selected item
+                    SetColor(MENU_SEL_FG);
+                    if (menu[i][0]) gr_text(x+4, y, menu[i], 1);
+                    SetColor(MENU);
+                } else {
+                    if (menu[i][0]) gr_text(x+4, y, menu[i], 0);
+                }
+                y += char_height+4;
+            }
+            SetColor(MENU);
+            y += 4;
+            gr_fill(0, y, gr_fb_width(), y+2);
+            y += 4;
+        }
+
+        SetColor(LOG);
+
+        // display from the bottom up, until we hit the top of the
+        // screen, the bottom of the menu, or we've displayed the
+        // entire text buffer.
+        int ty;
+        int row = (text_top+text_rows-1) % text_rows;
+        size_t count = 0;
+        for (int ty = gr_fb_height() - char_height - outer_height;
+             ty > y+2 && count < text_rows;
+             ty -= char_height, ++count) {
+            gr_text(x+4, ty, text[row], 0);
+            --row;
+            if (row < 0) row = text_rows-1;
+        }
+    }
+}
+
+void WearRecoveryUI::update_screen_locked()
+{
+    draw_screen_locked();
+    gr_flip();
+}
+
+// Keeps the progress bar updated, even when the process is otherwise busy.
+void* WearRecoveryUI::progress_thread(void *cookie) {
+    self->progress_loop();
+    return NULL;
+}
+
+void WearRecoveryUI::progress_loop() {
+    double interval = 1.0 / animation_fps;
+    for (;;) {
+        double start = now();
+        pthread_mutex_lock(&updateMutex);
+        int redraw = 0;
+
+        if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING)
+                                                            && !show_text) {
+            if (!intro_done) {
+                if (current_frame == intro_frames - 1) {
+                    intro_done = true;
+                    current_frame = 0;
+                } else {
+                    current_frame++;
+                }
+            } else {
+                current_frame = (current_frame + 1) % loop_frames;
+            }
+            redraw = 1;
+        }
+
+        // move the progress bar forward on timed intervals, if configured
+        int duration = progressScopeDuration;
+        if (progressBarType == DETERMINATE && duration > 0) {
+            double elapsed = now() - progressScopeTime;
+            float p = 1.0 * elapsed / duration;
+            if (p > 1.0) p = 1.0;
+            if (p > progress) {
+                progress = p;
+                redraw = 1;
+            }
+        }
+
+        if (redraw)
+            update_screen_locked();
+
+        pthread_mutex_unlock(&updateMutex);
+        double end = now();
+        // minimum of 20ms delay between frames
+        double delay = interval - (end-start);
+        if (delay < 0.02) delay = 0.02;
+        usleep(static_cast<useconds_t>(delay * 1000000));
+    }
+}
+
+void WearRecoveryUI::LoadBitmap(const char* filename, GRSurface** surface) {
+    int result = res_create_display_surface(filename, surface);
+    if (result < 0) {
+        LOGE("missing bitmap %s\n(Code %d)\n", filename, result);
+    }
+}
+
+void WearRecoveryUI::Init()
+{
+    gr_init();
+
+    gr_font_size(&char_width, &char_height);
+
+    text_col = text_row = 0;
+    text_rows = (gr_fb_height()) / char_height;
+    visible_text_rows = (gr_fb_height() - (outer_height * 2)) / char_height;
+    if (text_rows > kMaxRows) text_rows = kMaxRows;
+    text_top = 1;
+
+    text_cols = (gr_fb_width() - (outer_width * 2)) / char_width;
+    if (text_cols > kMaxCols - 1) text_cols = kMaxCols - 1;
+
+    LoadBitmap("icon_installing", &backgroundIcon[INSTALLING_UPDATE]);
+    backgroundIcon[ERASING] = backgroundIcon[INSTALLING_UPDATE];
+    LoadBitmap("icon_error", &backgroundIcon[ERROR]);
+    backgroundIcon[NO_COMMAND] = backgroundIcon[ERROR];
+
+    introFrames = (GRSurface**)malloc(intro_frames * sizeof(GRSurface*));
+    for (int i = 0; i < intro_frames; ++i) {
+        char filename[40];
+        sprintf(filename, "intro%02d", i);
+        LoadBitmap(filename, introFrames + i);
+    }
+
+    loopFrames = (GRSurface**)malloc(loop_frames * sizeof(GRSurface*));
+    for (int i = 0; i < loop_frames; ++i) {
+        char filename[40];
+        sprintf(filename, "loop%02d", i);
+        LoadBitmap(filename, loopFrames + i);
+    }
+
+    pthread_create(&progress_t, NULL, progress_thread, NULL);
+    RecoveryUI::Init();
+}
+
+void WearRecoveryUI::SetLocale(const char* locale) {
+    if (locale) {
+        char* lang = strdup(locale);
+        for (char* p = lang; *p; ++p) {
+            if (*p == '_') {
+                *p = '\0';
+                break;
+            }
+        }
+
+        // A bit cheesy: keep an explicit list of supported languages
+        // that are RTL.
+        if (strcmp(lang, "ar") == 0 ||   // Arabic
+            strcmp(lang, "fa") == 0 ||   // Persian (Farsi)
+            strcmp(lang, "he") == 0 ||   // Hebrew (new language code)
+            strcmp(lang, "iw") == 0 ||   // Hebrew (old language code)
+            strcmp(lang, "ur") == 0) {   // Urdu
+            rtl_locale = true;
+        }
+        free(lang);
+    }
+}
+
+void WearRecoveryUI::SetBackground(Icon icon)
+{
+    pthread_mutex_lock(&updateMutex);
+    currentIcon = icon;
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::SetProgressType(ProgressType type)
+{
+    pthread_mutex_lock(&updateMutex);
+    if (progressBarType != type) {
+        progressBarType = type;
+    }
+    progressScopeStart = 0;
+    progressScopeSize = 0;
+    progress = 0;
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::ShowProgress(float portion, float seconds)
+{
+    pthread_mutex_lock(&updateMutex);
+    progressBarType = DETERMINATE;
+    progressScopeStart += progressScopeSize;
+    progressScopeSize = portion;
+    progressScopeTime = now();
+    progressScopeDuration = seconds;
+    progress = 0;
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::SetProgress(float fraction)
+{
+    pthread_mutex_lock(&updateMutex);
+    if (fraction < 0.0) fraction = 0.0;
+    if (fraction > 1.0) fraction = 1.0;
+    if (progressBarType == DETERMINATE && fraction > progress) {
+        // Skip updates that aren't visibly different.
+        int width = progress_bar_width;
+        float scale = width * progressScopeSize;
+        if ((int) (progress * scale) != (int) (fraction * scale)) {
+            progress = fraction;
+            update_screen_locked();
+        }
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::SetStage(int current, int max)
+{
+}
+
+void WearRecoveryUI::Print(const char *fmt, ...)
+{
+    char buf[256];
+    va_list ap;
+    va_start(ap, fmt);
+    vsnprintf(buf, 256, fmt, ap);
+    va_end(ap);
+
+    fputs(buf, stdout);
+
+    // This can get called before ui_init(), so be careful.
+    pthread_mutex_lock(&updateMutex);
+    if (text_rows > 0 && text_cols > 0) {
+        char *ptr;
+        for (ptr = buf; *ptr != '\0'; ++ptr) {
+            if (*ptr == '\n' || text_col >= text_cols) {
+                text[text_row][text_col] = '\0';
+                text_col = 0;
+                text_row = (text_row + 1) % text_rows;
+                if (text_row == text_top) text_top = (text_top + 1) % text_rows;
+            }
+            if (*ptr != '\n') text[text_row][text_col++] = *ptr;
+        }
+        text[text_row][text_col] = '\0';
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::StartMenu(const char* const * headers, const char* const * items,
+                                 int initial_selection) {
+    pthread_mutex_lock(&updateMutex);
+    if (text_rows > 0 && text_cols > 0) {
+        menu_headers_ = headers;
+        size_t i = 0;
+        // "i < text_rows" is removed from the loop termination condition,
+        // which is different from the one in ScreenRecoveryUI::StartMenu().
+        // Because WearRecoveryUI supports scrollable menu, it's fine to have
+        // more entries than text_rows. The menu may be truncated otherwise.
+        // Bug: 23752519
+        for (; items[i] != nullptr; i++) {
+            strncpy(menu[i], items[i], text_cols - 1);
+            menu[i][text_cols - 1] = '\0';
+        }
+        menu_items = i;
+        show_menu = 1;
+        menu_sel = initial_selection;
+        menu_start = 0;
+        menu_end = visible_text_rows - 1 - menu_unusable_rows;
+        if (menu_items <= menu_end)
+          menu_end = menu_items;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+int WearRecoveryUI::SelectMenu(int sel) {
+    int old_sel;
+    pthread_mutex_lock(&updateMutex);
+    if (show_menu > 0) {
+        old_sel = menu_sel;
+        menu_sel = sel;
+        if (menu_sel < 0) menu_sel = 0;
+        if (menu_sel >= menu_items) menu_sel = menu_items-1;
+        if (menu_sel < menu_start) {
+          menu_start--;
+          menu_end--;
+        } else if (menu_sel >= menu_end && menu_sel < menu_items) {
+          menu_end++;
+          menu_start++;
+        }
+        sel = menu_sel;
+        if (menu_sel != old_sel) update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+    return sel;
+}
+
+void WearRecoveryUI::EndMenu() {
+    int i;
+    pthread_mutex_lock(&updateMutex);
+    if (show_menu > 0 && text_rows > 0 && text_cols > 0) {
+        show_menu = 0;
+        update_screen_locked();
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+bool WearRecoveryUI::IsTextVisible()
+{
+    pthread_mutex_lock(&updateMutex);
+    int visible = show_text;
+    pthread_mutex_unlock(&updateMutex);
+    return visible;
+}
+
+bool WearRecoveryUI::WasTextEverVisible()
+{
+    pthread_mutex_lock(&updateMutex);
+    int ever_visible = show_text_ever;
+    pthread_mutex_unlock(&updateMutex);
+    return ever_visible;
+}
+
+void WearRecoveryUI::ShowText(bool visible)
+{
+    pthread_mutex_lock(&updateMutex);
+    // Don't show text during ota install or factory reset
+    if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
+        pthread_mutex_unlock(&updateMutex);
+        return;
+    }
+    show_text = visible;
+    if (show_text) show_text_ever = 1;
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::Redraw()
+{
+    pthread_mutex_lock(&updateMutex);
+    update_screen_locked();
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::ShowFile(FILE* fp) {
+    std::vector<off_t> offsets;
+    offsets.push_back(ftello(fp));
+    ClearText();
+
+    struct stat sb;
+    fstat(fileno(fp), &sb);
+
+    bool show_prompt = false;
+    while (true) {
+        if (show_prompt) {
+            Print("--(%d%% of %d bytes)--",
+                  static_cast<int>(100 * (double(ftello(fp)) / double(sb.st_size))),
+                  static_cast<int>(sb.st_size));
+            Redraw();
+            while (show_prompt) {
+                show_prompt = false;
+                int key = WaitKey();
+                if (key == KEY_POWER || key == KEY_ENTER) {
+                    return;
+                } else if (key == KEY_UP || key == KEY_VOLUMEUP) {
+                    if (offsets.size() <= 1) {
+                        show_prompt = true;
+                    } else {
+                        offsets.pop_back();
+                        fseek(fp, offsets.back(), SEEK_SET);
+                    }
+                } else {
+                    if (feof(fp)) {
+                        return;
+                    }
+                    offsets.push_back(ftello(fp));
+                }
+            }
+            ClearText();
+        }
+
+        int ch = getc(fp);
+        if (ch == EOF) {
+            text_row = text_top = text_rows - 2;
+            show_prompt = true;
+        } else {
+            PutChar(ch);
+            if (text_col == 0 && text_row >= text_rows - 2) {
+                text_top = text_row;
+                show_prompt = true;
+            }
+        }
+    }
+}
+
+void WearRecoveryUI::PutChar(char ch) {
+    pthread_mutex_lock(&updateMutex);
+    if (ch != '\n') text[text_row][text_col++] = ch;
+    if (ch == '\n' || text_col >= text_cols) {
+        text_col = 0;
+        ++text_row;
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
+
+void WearRecoveryUI::ShowFile(const char* filename) {
+    FILE* fp = fopen_path(filename, "re");
+    if (fp == nullptr) {
+        Print("  Unable to open %s: %s\n", filename, strerror(errno));
+        return;
+    }
+    ShowFile(fp);
+    fclose(fp);
+}
+
+void WearRecoveryUI::ClearText() {
+    pthread_mutex_lock(&updateMutex);
+    text_col = 0;
+    text_row = 0;
+    text_top = 1;
+    for (size_t i = 0; i < text_rows; ++i) {
+        memset(text[i], 0, text_cols + 1);
+    }
+    pthread_mutex_unlock(&updateMutex);
+}
diff --git a/recovery/wear_ui.h b/recovery/wear_ui.h
new file mode 100644
index 0000000..63c1b6e
--- /dev/null
+++ b/recovery/wear_ui.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef RECOVERY_WEAR_UI_H
+#define RECOVERY_WEAR_UI_H
+
+#include <pthread.h>
+#include <stdio.h>
+
+#include "ui.h"
+#include "minui/minui.h"
+
+class WearRecoveryUI : public RecoveryUI {
+  public:
+    WearRecoveryUI();
+
+    void Init();
+    void SetLocale(const char* locale);
+
+    // overall recovery state ("background image")
+    void SetBackground(Icon icon);
+
+    // progress indicator
+    void SetProgressType(ProgressType type);
+    void ShowProgress(float portion, float seconds);
+    void SetProgress(float fraction);
+
+    void SetStage(int current, int max);
+
+    // text log
+    void ShowText(bool visible);
+    bool IsTextVisible();
+    bool WasTextEverVisible();
+
+    // printing messages
+    void Print(const char* fmt, ...);
+    void ShowFile(const char* filename);
+    void ShowFile(FILE* fp);
+
+    // menu display
+    void StartMenu(const char* const * headers, const char* const * items,
+                           int initial_selection);
+    int SelectMenu(int sel);
+    void EndMenu();
+
+    void Redraw();
+
+    enum UIElement { HEADER, MENU, MENU_SEL_BG, MENU_SEL_FG, LOG, TEXT_FILL };
+    virtual void SetColor(UIElement e);
+
+  protected:
+    int progress_bar_height, progress_bar_width;
+
+    // progress bar vertical position, it's centered horizontally
+    int progress_bar_y;
+
+    // outer of window
+    int outer_height, outer_width;
+
+    // Unusable rows when displaying the recovery menu, including the lines
+    // for headers (Android Recovery, build id and etc) and the bottom lines
+    // that may otherwise go out of the screen.
+    int menu_unusable_rows;
+
+    // number of intro frames (default: 22) and loop frames (default: 60)
+    int intro_frames;
+    int loop_frames;
+
+    // Number of frames per sec (default: 30) for both of intro and loop.
+    int animation_fps;
+
+  private:
+    Icon currentIcon;
+
+    bool intro_done;
+
+    int current_frame;
+
+    bool rtl_locale;
+
+    pthread_mutex_t updateMutex;
+    GRSurface* backgroundIcon[5];
+    GRSurface* *introFrames;
+    GRSurface* *loopFrames;
+
+    ProgressType progressBarType;
+
+    float progressScopeStart, progressScopeSize, progress;
+    double progressScopeTime, progressScopeDuration;
+
+    static const int kMaxCols = 96;
+    static const int kMaxRows = 96;
+
+    // Log text overlay, displayed when a magic key is pressed
+    char text[kMaxRows][kMaxCols];
+    size_t text_cols, text_rows;
+    // Number of text rows seen on screen
+    int visible_text_rows;
+    size_t text_col, text_row, text_top;
+    bool show_text;
+    bool show_text_ever;   // has show_text ever been true?
+
+    char menu[kMaxRows][kMaxCols];
+    bool show_menu;
+    const char* const* menu_headers_;
+    int menu_items, menu_sel;
+    int menu_start, menu_end;
+
+    pthread_t progress_t;
+
+  private:
+    void draw_background_locked(Icon icon);
+    void draw_progress_locked();
+    void draw_screen_locked();
+    void update_screen_locked();
+    static void* progress_thread(void* cookie);
+    void progress_loop();
+    void LoadBitmap(const char* filename, GRSurface** surface);
+    void PutChar(char);
+    void ClearText();
+    void DrawTextLine(int x, int* y, const char* line, bool bold);
+    void DrawTextLines(int x, int* y, const char* const* lines);
+};
+
+#endif  // RECOVERY_WEAR_UI_H