[automerger skipped] DO NOT MERGE: Initialize the ZipArchive to zero before parsing skipped: e830a51351 skipped: 244aba4412 skipped: 9216bccd16 skipped: 98acdb9fce skipped: bc42a6b0eb am: 35332d7c62 am: 6b13f89f15
am: 274945cb5b  -s ours

Change-Id: Ic5569d0abf7674ca5bee1b3df01170e69b6a0c1d
diff --git a/.clang-format b/.clang-format
index 5322788..0e0f4d1 100644
--- a/.clang-format
+++ b/.clang-format
@@ -10,6 +10,5 @@
 PointerAlignment: Left
 TabWidth: 2
 UseTab: Never
-PenaltyExcessCharacter: 32
 
 Cpp11BracedListStyle: false
diff --git a/Android.bp b/Android.bp
index 49438ad..f8c6a4b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,4 +1,8 @@
 subdirs = [
+    "applypatch",
     "bootloader_message",
+    "edify",
+    "otafault",
     "otautil",
+    "uncrypt",
 ]
diff --git a/Android.mk b/Android.mk
index 776e6ea..7e0ad12 100644
--- a/Android.mk
+++ b/Android.mk
@@ -56,6 +56,7 @@
 LOCAL_MODULE := librecovery
 LOCAL_STATIC_LIBRARIES := \
     libminui \
+    libotautil \
     libvintf_recovery \
     libcrypto_utils \
     libcrypto \
@@ -88,12 +89,12 @@
 
 ifeq ($(TARGET_USERIMAGES_USE_F2FS),true)
 ifeq ($(HOST_OS),linux)
-LOCAL_REQUIRED_MODULES += mkfs.f2fs
+LOCAL_REQUIRED_MODULES += sload.f2fs mkfs.f2fs
 endif
 endif
 
 LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
-LOCAL_CFLAGS += -Wno-unused-parameter -Werror
+LOCAL_CFLAGS += -Wall -Werror
 
 ifneq ($(TARGET_RECOVERY_UI_MARGIN_HEIGHT),)
 LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=$(TARGET_RECOVERY_UI_MARGIN_HEIGHT)
@@ -146,10 +147,22 @@
 LOCAL_C_INCLUDES += \
     system/vold \
 
+# Health HAL dependency
 LOCAL_STATIC_LIBRARIES := \
+    android.hardware.health@2.0-impl \
+    android.hardware.health@2.0 \
+    android.hardware.health@1.0 \
+    android.hardware.health@1.0-convert \
+    libhealthstoragedefault \
+    libhidltransport \
+    libhidlbase \
+    libhwbinder_noltopgo \
+    libvndksupport \
+    libbatterymonitor
+
+LOCAL_STATIC_LIBRARIES += \
     librecovery \
     libverifier \
-    libbatterymonitor \
     libbootloader_message \
     libfs_mgr \
     libext4_utils \
@@ -157,8 +170,8 @@
     libziparchive \
     libotautil \
     libmounts \
-    libz \
     libminadbd \
+    libasyncio \
     libfusesideload \
     libminui \
     libpng \
@@ -166,14 +179,14 @@
     libcrypto \
     libvintf_recovery \
     libvintf \
+    libhidl-gen-utils \
     libtinyxml2 \
     libbase \
-    libcutils \
     libutils \
+    libcutils \
     liblog \
     libselinux \
-    libm \
-    libc
+    libz
 
 LOCAL_HAL_STATIC_LIBRARIES := libhealthd
 
@@ -203,7 +216,7 @@
     rotate_logs.cpp
 LOCAL_MODULE := recovery-persist
 LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_INIT_RC := recovery-persist.rc
 include $(BUILD_EXECUTABLE)
 
@@ -215,7 +228,7 @@
     rotate_logs.cpp
 LOCAL_MODULE := recovery-refresh
 LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_INIT_RC := recovery-refresh.rc
 include $(BUILD_EXECUTABLE)
 
@@ -227,16 +240,18 @@
     asn1_decoder.cpp \
     verifier.cpp
 LOCAL_STATIC_LIBRARIES := \
+    libotautil \
     libcrypto_utils \
     libcrypto \
     libbase
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 include $(BUILD_STATIC_LIBRARY)
 
 # Wear default device
 # ===============================
 include $(CLEAR_VARS)
 LOCAL_SRC_FILES := wear_device.cpp
+LOCAL_CFLAGS := -Wall -Werror
 
 # Should match TARGET_RECOVERY_UI_LIB in BoardConfig.mk.
 LOCAL_MODULE := librecovery_ui_wear
@@ -248,6 +263,7 @@
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := vr_device.cpp
+LOCAL_CFLAGS := -Wall -Werror
 
 # should match TARGET_RECOVERY_UI_LIB set in BoardConfig.mk
 LOCAL_MODULE := librecovery_ui_vr
@@ -255,14 +271,10 @@
 include $(BUILD_STATIC_LIBRARY)
 
 include \
-    $(LOCAL_PATH)/applypatch/Android.mk \
     $(LOCAL_PATH)/boot_control/Android.mk \
-    $(LOCAL_PATH)/edify/Android.mk \
     $(LOCAL_PATH)/minadbd/Android.mk \
     $(LOCAL_PATH)/minui/Android.mk \
-    $(LOCAL_PATH)/otafault/Android.mk \
     $(LOCAL_PATH)/tests/Android.mk \
     $(LOCAL_PATH)/tools/Android.mk \
-    $(LOCAL_PATH)/uncrypt/Android.mk \
     $(LOCAL_PATH)/updater/Android.mk \
     $(LOCAL_PATH)/update_verifier/Android.mk \
diff --git a/OWNERS b/OWNERS
index 09754c6..b3f11dc 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,3 @@
-enh+aosp-gerrit@google.com
+enh@google.com
 tbao@google.com
 xunchang@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..b5f5f03
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,6 @@
+[Builtin Hooks]
+clang_format = true
+
+[Builtin Hooks Options]
+# Handle native codes only.
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
diff --git a/README.md b/README.md
index 8e20b5a..0aeadae 100644
--- a/README.md
+++ b/README.md
@@ -47,3 +47,94 @@
     1. `adb sync data` to make sure the test-dir has the images to test.
     2. The test will automatically pickup and verify all `_text.png` files in
        the test dir.
+
+Using `adb` under recovery
+--------------------------
+
+When running recovery image from debuggable builds (i.e. `-eng` or `-userdebug` build variants, or
+`ro.debuggable=1` in `/prop.default`), `adbd` service is enabled and started by default, which
+allows `adb` communication. A device should be listed under `adb devices`, either in `recovery` or
+`sideload` state.
+
+    $ adb devices
+    List of devices attached
+    1234567890abcdef    recovery
+
+Although `/sbin/adbd` shares the same binary between normal boot and recovery images, only a subset
+of `adb` commands are meaningful under recovery, such as `adb root`, `adb shell`, `adb push`, `adb
+pull` etc. `adb shell` works only after manually mounting `/system` from recovery menu (assuming a
+valid system image on device).
+
+## Troubleshooting
+
+### `adb devices` doesn't show the device.
+
+    $ adb devices
+    List of devices attached
+
+ * Ensure `adbd` is built and running.
+
+By default, `adbd` is always included into recovery image, as `/sbin/adbd`. `init` starts `adbd`
+service automatically only in debuggable builds. This behavior is controlled by the recovery
+specific `/init.rc`, whose source code is at `bootable/recovery/etc/init.rc`.
+
+The best way to confirm a running `adbd` is by checking the serial output, which shows a service
+start log as below.
+
+    [   18.961986] c1      1 init: starting service 'adbd'...
+
+ * Ensure USB gadget has been enabled.
+
+If `adbd` service has been started but device not shown under `adb devices`, use `lsusb(8)` (on
+host) to check if the device is visible to the host.
+
+`bootable/recovery/etc/init.rc` disables Android USB gadget (via sysfs) as part of the `fs` action
+trigger, and will only re-enable it in debuggable builds (the `on property` rule will always run
+_after_ `on fs`).
+
+    on fs
+        write /sys/class/android_usb/android0/enable 0
+
+    # Always start adbd on userdebug and eng builds
+    on property:ro.debuggable=1
+        write /sys/class/android_usb/android0/enable 1
+        start adbd
+
+If device is using [configfs](https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt),
+check if configfs has been properly set up in init rc scripts. See the [example
+configuration](https://android.googlesource.com/device/google/wahoo/+/master/init.recovery.hardware.rc)
+for Pixel 2 devices. Note that the flag set via sysfs (i.e. the one above) is no-op when using
+configfs.
+
+### `adb devices` shows the device, but in `unauthorized` state.
+
+    $ adb devices
+    List of devices attached
+    1234567890abcdef    unauthorized
+
+recovery image doesn't honor the USB debugging toggle and the authorizations added under normal boot
+(because such authorization data stays in /data, which recovery doesn't mount), nor does it support
+authorizing a host device under recovery. We can use one of the following options instead.
+
+ * **Option 1 (Recommended):** Authorize a host device with adb vendor keys.
+
+For debuggable builds, an RSA keypair can be used to authorize a host device that has the private
+key. The public key, defined via `PRODUCT_ADB_KEYS`, will be copied to `/adb_keys`. When starting
+the host-side `adbd`, make sure the filename (or the directory) of the matching private key has been
+added to `$ADB_VENDOR_KEYS`.
+
+    $ export ADB_VENDOR_KEYS=/path/to/adb/private/key
+    $ adb kill-server
+    $ adb devices
+
+`-user` builds filter out `PRODUCT_ADB_KEYS`, so no `/adb_keys` will be included there.
+
+Note that this mechanism applies to both of normal boot and recovery modes.
+
+ * **Option 2:** Allow `adbd` to connect without authentication.
+   * `adbd` is compiled with `ALLOW_ADBD_NO_AUTH` (only on debuggable builds).
+   * `ro.adb.secure` has a value of `0`.
+
+Both of the two conditions need to be satisfied. Although `ro.adb.secure` is a runtime property, its
+value is set at build time (written into `/prop.default`). It defaults to `1` on `-user` builds, and
+`0` for other build variants. The value is overridable via `PRODUCT_DEFAULT_PROPERTY_OVERRIDES`.
diff --git a/applypatch/Android.bp b/applypatch/Android.bp
new file mode 100644
index 0000000..cb0b367
--- /dev/null
+++ b/applypatch/Android.bp
@@ -0,0 +1,206 @@
+// Copyright (C) 2017 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.
+
+cc_defaults {
+    name: "applypatch_defaults",
+
+    cflags: [
+        "-D_FILE_OFFSET_BITS=64",
+        "-DZLIB_CONST",
+        "-Wall",
+        "-Werror",
+    ],
+
+    local_include_dirs: [
+        "include",
+    ],
+}
+
+cc_library_static {
+    name: "libapplypatch",
+
+    host_supported: true,
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    srcs: [
+        "applypatch.cpp",
+        "bspatch.cpp",
+        "freecache.cpp",
+        "imgpatch.cpp",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    static_libs: [
+        "libbase",
+        "libbspatch",
+        "libbz",
+        "libcrypto",
+        "libedify",
+        "libotafault",
+        "libotautil",
+        "libz",
+    ],
+
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
+
+cc_library_static {
+    name: "libapplypatch_modes",
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    srcs: [
+        "applypatch_modes.cpp",
+    ],
+
+    static_libs: [
+        "libapplypatch",
+        "libbase",
+        "libcrypto",
+        "libedify",
+        "libotautil",
+    ],
+}
+
+cc_binary {
+    name: "applypatch",
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    srcs: [
+        "applypatch_main.cpp",
+    ],
+
+    static_libs: [
+        "libapplypatch_modes",
+        "libapplypatch",
+        "libedify",
+        "libotafault",
+        "libotautil",
+        "libbspatch",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libbrotli",
+        "libbz",
+        "libcrypto",
+        "liblog",
+        "libz",
+        "libziparchive",
+    ],
+}
+
+cc_library_static {
+    name: "libimgdiff",
+
+    host_supported: true,
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    srcs: [
+        "imgdiff.cpp",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    static_libs: [
+        "libbase",
+        "libbsdiff",
+        "libdivsufsort",
+        "libdivsufsort64",
+        "liblog",
+        "libotautil",
+        "libutils",
+        "libz",
+        "libziparchive",
+    ],
+}
+
+cc_binary_host {
+    name: "imgdiff",
+
+    srcs: [
+        "imgdiff_main.cpp",
+    ],
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    static_libs: [
+        "libimgdiff",
+        "libotautil",
+        "libbsdiff",
+        "libdivsufsort",
+        "libdivsufsort64",
+        "libziparchive",
+        "libbase",
+        "libutils",
+        "liblog",
+        "libbrotli",
+        "libbz",
+        "libz",
+    ],
+}
+
+cc_library_static {
+    name: "libimgpatch",
+
+    // The host module is for recovery_host_test (Linux only).
+    host_supported: true,
+
+    defaults: [
+        "applypatch_defaults",
+    ],
+
+    srcs: [
+        "bspatch.cpp",
+        "imgpatch.cpp",
+    ],
+
+    static_libs: [
+        "libbase",
+        "libbspatch",
+        "libbz",
+        "libcrypto",
+        "libedify",
+        "libotautil",
+        "libz",
+    ],
+
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
diff --git a/applypatch/Android.mk b/applypatch/Android.mk
deleted file mode 100644
index a7412d2..0000000
--- a/applypatch/Android.mk
+++ /dev/null
@@ -1,182 +0,0 @@
-# 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_SRC_FILES := \
-    applypatch.cpp \
-    bspatch.cpp \
-    freecache.cpp \
-    imgpatch.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 \
-    libbspatch \
-    libbz \
-    libz
-LOCAL_CFLAGS := \
-    -DZLIB_CONST \
-    -Werror
-include $(BUILD_STATIC_LIBRARY)
-
-# libimgpatch (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    bspatch.cpp \
-    imgpatch.cpp
-LOCAL_MODULE := libimgpatch
-LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/include \
-    bootable/recovery
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-LOCAL_STATIC_LIBRARIES := \
-    libcrypto \
-    libbspatch \
-    libbase \
-    libbz \
-    libz
-LOCAL_CFLAGS := \
-    -DZLIB_CONST \
-    -Werror
-include $(BUILD_STATIC_LIBRARY)
-
-# libimgpatch (host static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    bspatch.cpp \
-    imgpatch.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 \
-    libbspatch \
-    libbase \
-    libbz \
-    libz
-LOCAL_CFLAGS := \
-    -DZLIB_CONST \
-    -Werror
-include $(BUILD_HOST_STATIC_LIBRARY)
-
-# libapplypatch_modes (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    applypatch_modes.cpp
-LOCAL_MODULE := libapplypatch_modes
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_STATIC_LIBRARIES := \
-    libapplypatch \
-    libbase \
-    libedify \
-    libcrypto
-LOCAL_CFLAGS := -Werror
-include $(BUILD_STATIC_LIBRARY)
-
-# applypatch (target executable)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := applypatch_main.cpp
-LOCAL_MODULE := applypatch
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_STATIC_LIBRARIES := \
-    libapplypatch_modes \
-    libapplypatch \
-    libbase \
-    libedify \
-    libotafault \
-    libcrypto \
-    libbspatch \
-    libbz
-LOCAL_SHARED_LIBRARIES := \
-    libbase \
-    libz \
-    libcutils
-LOCAL_CFLAGS := -Werror
-include $(BUILD_EXECUTABLE)
-
-libimgdiff_src_files := imgdiff.cpp
-
-# libbsdiff is compiled with -D_FILE_OFFSET_BITS=64.
-libimgdiff_cflags := \
-    -Werror \
-    -D_FILE_OFFSET_BITS=64
-
-libimgdiff_static_libraries := \
-    libbsdiff \
-    libdivsufsort \
-    libdivsufsort64 \
-    libziparchive \
-    libutils \
-    liblog \
-    libbase \
-    libz
-
-# libimgdiff (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    $(libimgdiff_src_files)
-LOCAL_MODULE := libimgdiff
-LOCAL_CFLAGS := \
-    $(libimgdiff_cflags)
-LOCAL_STATIC_LIBRARIES := \
-    $(libimgdiff_static_libraries)
-LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-include $(BUILD_STATIC_LIBRARY)
-
-# libimgdiff (host static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    $(libimgdiff_src_files)
-LOCAL_MODULE := libimgdiff
-LOCAL_CFLAGS := \
-    $(libimgdiff_cflags)
-LOCAL_STATIC_LIBRARIES := \
-    $(libimgdiff_static_libraries)
-LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-include $(BUILD_HOST_STATIC_LIBRARY)
-
-# imgdiff (host static executable)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := imgdiff_main.cpp
-LOCAL_MODULE := imgdiff
-LOCAL_CFLAGS := -Werror
-LOCAL_STATIC_LIBRARIES := \
-    libimgdiff \
-    $(libimgdiff_static_libraries) \
-    libbz
-include $(BUILD_HOST_EXECUTABLE)
diff --git a/applypatch/Makefile b/applypatch/Makefile
deleted file mode 100644
index fb49843..0000000
--- a/applypatch/Makefile
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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/applypatch/applypatch.cpp b/applypatch/applypatch.cpp
index 51bf393..7645a40 100644
--- a/applypatch/applypatch.cpp
+++ b/applypatch/applypatch.cpp
@@ -39,8 +39,9 @@
 #include <openssl/sha.h>
 
 #include "edify/expr.h"
-#include "ota_io.h"
-#include "print_sha1.h"
+#include "otafault/ota_io.h"
+#include "otautil/cache_location.h"
+#include "otautil/print_sha1.h"
 
 static int LoadPartitionContents(const std::string& filename, FileContents* file);
 static size_t FileSink(const unsigned char* data, size_t len, int fd);
@@ -56,12 +57,13 @@
     return LoadPartitionContents(filename, file);
   }
 
-  if (stat(filename, &file->st) == -1) {
+  struct stat sb;
+  if (stat(filename, &sb) == -1) {
     printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
     return -1;
   }
 
-  std::vector<unsigned char> data(file->st.st_size);
+  std::vector<unsigned char> data(sb.st_size);
   unique_file f(ota_fopen(filename, "rb"));
   if (!f) {
     printf("failed to open \"%s\": %s\n", filename, strerror(errno));
@@ -178,10 +180,6 @@
 
   buffer.resize(buffer_size);
   file->data = std::move(buffer);
-  // Fake some stat() info.
-  file->st.st_mode = 0644;
-  file->st.st_uid = 0;
-  file->st.st_gid = 0;
 
   return 0;
 }
@@ -210,15 +208,6 @@
     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;
 }
 
@@ -411,12 +400,10 @@
       (!patch_sha1_str.empty() && FindMatchingPatch(file.sha1, patch_sha1_str) < 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) {
+    // 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(CacheLocation::location().cache_temp_source().c_str(), &file) != 0) {
       printf("failed to load cache file\n");
       return 1;
     }
@@ -462,9 +449,8 @@
     if (MakeFreeSpaceOnCache(bytes) < 0) {
         printf("unable to make %zu bytes available on /cache\n", bytes);
         return 1;
-    } else {
-        return 0;
     }
+    return 0;
 }
 
 // This function applies binary patches to EMMC target files in a way that is safe (the original
@@ -489,7 +475,7 @@
 // become obsolete since we have dropped the support for patching non-EMMC targets (EMMC targets
 // have the size embedded in the filename).
 int applypatch(const char* source_filename, const char* target_filename,
-               const char* target_sha1_str, size_t target_size __unused,
+               const char* target_sha1_str, size_t /* target_size */,
                const std::vector<std::string>& patch_sha1_str,
                const std::vector<std::unique_ptr<Value>>& patch_data, const Value* bonus_data) {
   printf("patch %s: ", source_filename);
@@ -539,7 +525,7 @@
   printf("source file is bad; trying copy\n");
 
   FileContents copy_file;
-  if (LoadFileContents(CACHE_TEMP_SOURCE, &copy_file) < 0) {
+  if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), &copy_file) < 0) {
     printf("failed to read copy file\n");
     return 1;
   }
@@ -634,7 +620,7 @@
     printf("not enough free space on /cache\n");
     return 1;
   }
-  if (SaveFileContents(CACHE_TEMP_SOURCE, &source_file) < 0) {
+  if (SaveFileContents(CacheLocation::location().cache_temp_source().c_str(), &source_file) < 0) {
     printf("failed to back up source file\n");
     return 1;
   }
@@ -651,11 +637,11 @@
 
   int result;
   if (use_bsdiff) {
-    result = ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), patch.get(), 0,
-                              sink, &ctx);
+    result =
+        ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), *patch, 0, sink, &ctx);
   } else {
-    result = ApplyImagePatch(source_file.data.data(), source_file.data.size(), patch.get(), sink,
-                             &ctx, bonus_data);
+    result = ApplyImagePatch(source_file.data.data(), source_file.data.size(), *patch, sink, &ctx,
+                             bonus_data);
   }
 
   if (result != 0) {
@@ -680,7 +666,7 @@
   }
 
   // Delete the backup copy of the source.
-  unlink(CACHE_TEMP_SOURCE);
+  unlink(CacheLocation::location().cache_temp_source().c_str());
 
   // Success!
   return 0;
diff --git a/applypatch/bspatch.cpp b/applypatch/bspatch.cpp
index 65ee614..912dbbd 100644
--- a/applypatch/bspatch.cpp
+++ b/applypatch/bspatch.cpp
@@ -26,11 +26,12 @@
 #include <string>
 
 #include <android-base/logging.h>
-#include <bspatch.h>
+#include <bsdiff/bspatch.h>
 #include <openssl/sha.h>
 
 #include "applypatch/applypatch.h"
-#include "print_sha1.h"
+#include "edify/expr.h"
+#include "otautil/print_sha1.h"
 
 void ShowBSDiffLicense() {
     puts("The bsdiff library used herein is:\n"
@@ -64,7 +65,7 @@
         );
 }
 
-int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value* patch,
+int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch,
                      size_t patch_offset, SinkFn sink, SHA_CTX* ctx) {
   auto sha_sink = [&sink, &ctx](const uint8_t* data, size_t len) {
     len = sink(data, len);
@@ -72,23 +73,21 @@
     return len;
   };
 
-  CHECK(patch != nullptr);
-  CHECK_LE(patch_offset, patch->data.size());
+  CHECK_LE(patch_offset, patch.data.size());
 
   int result = bsdiff::bspatch(old_data, old_size,
-                               reinterpret_cast<const uint8_t*>(&patch->data[patch_offset]),
-                               patch->data.size() - patch_offset, sha_sink);
+                               reinterpret_cast<const uint8_t*>(&patch.data[patch_offset]),
+                               patch.data.size() - patch_offset, sha_sink);
   if (result != 0) {
     LOG(ERROR) << "bspatch failed, result: " << result;
     // print SHA1 of the patch in the case of a data error.
     if (result == 2) {
       uint8_t digest[SHA_DIGEST_LENGTH];
-      SHA1(reinterpret_cast<const uint8_t*>(patch->data.data() + patch_offset),
-           patch->data.size() - patch_offset, digest);
+      SHA1(reinterpret_cast<const uint8_t*>(patch.data.data() + patch_offset),
+           patch.data.size() - patch_offset, digest);
       std::string patch_sha1 = print_sha1(digest);
-      LOG(ERROR) << "Patch may be corrupted, offset: " << patch_offset << ", SHA1: "
-                 << patch_sha1;
+      LOG(ERROR) << "Patch may be corrupted, offset: " << patch_offset << ", SHA1: " << patch_sha1;
     }
   }
   return result;
-}
\ No newline at end of file
+}
diff --git a/applypatch/freecache.cpp b/applypatch/freecache.cpp
index 331cae2..ea364d8 100644
--- a/applypatch/freecache.cpp
+++ b/applypatch/freecache.cpp
@@ -33,6 +33,7 @@
 #include <android-base/stringprintf.h>
 
 #include "applypatch/applypatch.h"
+#include "otautil/cache_location.h"
 
 static int EliminateOpenFiles(std::set<std::string>* files) {
   std::unique_ptr<DIR, decltype(&closedir)> d(opendir("/proc"), closedir);
@@ -90,10 +91,9 @@
     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) {
+      // 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 == CacheLocation::location().cache_temp_source()) {
         continue;
       }
 
@@ -112,6 +112,12 @@
 }
 
 int MakeFreeSpaceOnCache(size_t bytes_needed) {
+#ifndef __ANDROID__
+  // TODO (xunchang) implement a heuristic cache size check during host simulation.
+  printf("Skip making (%zu) bytes free space on cache; program is running on host\n", bytes_needed);
+  return 0;
+#endif
+
   size_t free_now = FreeSpaceForFile("/cache");
   printf("%zu bytes free on /cache (%zu needed)\n", free_now, bytes_needed);
 
diff --git a/applypatch/imgdiff.cpp b/applypatch/imgdiff.cpp
index fc24064..674cc2b 100644
--- a/applypatch/imgdiff.cpp
+++ b/applypatch/imgdiff.cpp
@@ -15,53 +15,44 @@
  */
 
 /*
- * 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.
+ * 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 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.
+ * 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.
+ * 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.
+ * For a diff to be generated, the source and target must be in well-formed zip archive format;
+ * or they are image files with 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.)
+ * 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]
+ *    "IMGDIFF2"                  (8)   [magic number and version]
  *    chunk count                 (4)
  *    for each chunk:
  *        chunk type              (4)   [CHUNK_{NORMAL, GZIP, DEFLATE, RAW}]
@@ -98,33 +89,62 @@
  *           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).
+ * 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.
+ * 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
+ * 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.
+ *
+ * When generating the patch between two zip files, this tool has an option "--block-limit" to
+ * split the large source/target files into several pair of pieces, with each piece has at most
+ * *limit* blocks.  When this option is used, we also need to output the split info into the file
+ * path specified by "--split-info".
+ *
+ * Format of split info file:
+ *   2                                      [version of imgdiff]
+ *   n                                      [count of split pieces]
+ *   <patch_size>, <tgt_size>, <src_range>  [size and ranges for split piece#1]
+ *   ...
+ *   <patch_size>, <tgt_size>, <src_range>  [size and ranges for split piece#n]
+ *
+ * To split a pair of large zip files, we walk through the chunks in target zip and search by its
+ * entry_name in the source zip.  If the entry_name is non-empty and a matching entry in source
+ * is found, we'll add the source entry to the current split source image; otherwise we'll skip
+ * this chunk and later do bsdiff between all the skipped trunks and the whole split source image.
+ * We move on to the next pair of pieces if the size of the split source image reaches the block
+ * limit.
+ *
+ * After the split, the target pieces are continuous and block aligned, while the source pieces
+ * are mutually exclusive.  Some of the source blocks may not be used if there's no matching
+ * entry_name in the target; as a result, they won't be included in any of these split source
+ * images.  Then we will generate patches accordingly between each split image pairs; in particular,
+ * the unmatched trunks in the split target will diff against the entire split source image.
+ *
+ * For example:
+ * Input: [src_image, tgt_image]
+ * Split: [src-0, tgt-0; src-1, tgt-1, src-2, tgt-2]
+ * Diff:  [  patch-0;      patch-1;      patch-2]
+ *
+ * Patch: [(src-0, patch-0) = tgt-0; (src-1, patch-1) = tgt-1; (src-2, patch-2) = tgt-2]
+ * Concatenate: [tgt-0 + tgt-1 + tgt-2 = tgt_image]
  */
 
 #include "applypatch/imgdiff.h"
 
 #include <errno.h>
 #include <fcntl.h>
+#include <getopt.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -139,15 +159,26 @@
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/memory.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
 #include <android-base/unique_fd.h>
+#include <bsdiff/bsdiff.h>
 #include <ziparchive/zip_archive.h>
-
-#include <bsdiff.h>
 #include <zlib.h>
 
+#include "applypatch/imgdiff_image.h"
+#include "otautil/rangeset.h"
+
 using android::base::get_unaligned;
 
-static constexpr auto BUFFER_SIZE = 0x8000;
+static constexpr size_t VERSION = 2;
+
+// We assume the header "IMGDIFF#" is 8 bytes.
+static_assert(VERSION <= 9, "VERSION occupies more than one byte");
+
+static constexpr size_t BLOCK_SIZE = 4096;
+static constexpr size_t BUFFER_SIZE = 0x8000;
 
 // If we use this function to write the offset and length (type size_t), their values should not
 // exceed 2^63; because the signed bit will be casted away.
@@ -161,99 +192,80 @@
   return android::base::WriteFully(fd, &value, sizeof(int32_t));
 }
 
-class ImageChunk {
- public:
-  static constexpr auto WINDOWBITS = -15;  // 32kb window; negative to indicate a raw stream.
-  static constexpr auto MEMLEVEL = 8;      // the default value.
-  static constexpr auto METHOD = Z_DEFLATED;
-  static constexpr auto STRATEGY = Z_DEFAULT_STRATEGY;
+// Trim the head or tail to align with the block size. Return false if the chunk has nothing left
+// after alignment.
+static bool AlignHead(size_t* start, size_t* length) {
+  size_t residual = (*start % BLOCK_SIZE == 0) ? 0 : BLOCK_SIZE - *start % BLOCK_SIZE;
 
-  ImageChunk(int type, size_t start, const std::vector<uint8_t>* file_content, size_t raw_data_len)
-      : type_(type),
-        start_(start),
-        input_file_ptr_(file_content),
-        raw_data_len_(raw_data_len),
-        compress_level_(6),
-        source_start_(0),
-        source_len_(0),
-        source_uncompressed_len_(0) {
-    CHECK(file_content != nullptr) << "input file container can't be nullptr";
+  if (*length <= residual) {
+    *length = 0;
+    return false;
   }
 
-  int GetType() const {
-    return type_;
-  }
-  size_t GetRawDataLength() const {
-    return raw_data_len_;
-  }
-  const std::string& GetEntryName() const {
-    return entry_name_;
+  // Trim the data in the beginning.
+  *start += residual;
+  *length -= residual;
+  return true;
+}
+
+static bool AlignTail(size_t* start, size_t* length) {
+  size_t residual = (*start + *length) % BLOCK_SIZE;
+  if (*length <= residual) {
+    *length = 0;
+    return false;
   }
 
-  // CHUNK_DEFLATE will return the uncompressed data for diff, while other types will simply return
-  // the raw data.
-  const uint8_t * DataForPatch() const;
-  size_t DataLengthForPatch() const;
+  // Trim the data in the end.
+  *length -= residual;
+  return true;
+}
 
-  void Dump() const {
-    printf("type %d start %zu len %zu\n", type_, start_, DataLengthForPatch());
+// Remove the used blocks from the source chunk to make sure the source ranges are mutually
+// exclusive after split. Return false if we fail to get the non-overlapped ranges. In such
+// a case, we'll skip the entire source chunk.
+static bool RemoveUsedBlocks(size_t* start, size_t* length, const SortedRangeSet& used_ranges) {
+  if (!used_ranges.Overlaps(*start, *length)) {
+    return true;
   }
 
-  void SetSourceInfo(const ImageChunk& other);
-  void SetEntryName(std::string entryname);
-  void SetUncompressedData(std::vector<uint8_t> data);
-  bool SetBonusData(const std::vector<uint8_t>& bonus_data);
+  // TODO find the largest non-overlap chunk.
+  LOG(INFO) << "Removing block " << used_ranges.ToString() << " from " << *start << " - "
+            << *start + *length - 1;
 
-  bool operator==(const ImageChunk& other) const;
-  bool operator!=(const ImageChunk& other) const {
-    return !(*this == other);
+  // If there's no duplicate entry name, we should only overlap in the head or tail block. Try to
+  // trim both blocks. Skip this source chunk in case it still overlaps with the used ranges.
+  if (AlignHead(start, length) && !used_ranges.Overlaps(*start, *length)) {
+    return true;
+  }
+  if (AlignTail(start, length) && !used_ranges.Overlaps(*start, *length)) {
+    return true;
   }
 
-  size_t GetHeaderSize(size_t patch_size) const;
-  // Return the offset of the next patch into the patch data.
-  size_t WriteHeaderToFd(int fd, const std::vector<uint8_t>& patch, size_t offset);
+  LOG(WARNING) << "Failed to remove the overlapped block ranges; skip the source";
+  return false;
+}
 
-  /*
-   * 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();
-  bool ChangeChunkToRaw(size_t patch_size);
-
-  /*
-   * 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.
-   */
-  bool ReconstructDeflateChunk();
-  bool IsAdjacentNormal(const ImageChunk& other) const;
-  void MergeAdjacentNormal(const ImageChunk& other);
-
- private:
-  int type_;                                    // CHUNK_NORMAL, CHUNK_DEFLATE, CHUNK_RAW
-  size_t start_;                                // offset of chunk in the original input file
-  const std::vector<uint8_t>* input_file_ptr_;  // ptr to the full content of original input file
-  size_t raw_data_len_;
-
-  // --- for CHUNK_DEFLATE chunks only: ---
-  std::vector<uint8_t> uncompressed_data_;
-  std::string entry_name_;  // used for zip entries
-
-  // deflate encoder parameters
-  int compress_level_;
-
-  size_t source_start_;
-  size_t source_len_;
-  size_t source_uncompressed_len_;
-
-  const uint8_t* GetRawData() const;
-  bool TryReconstruction(int level);
+static const struct option OPTIONS[] = {
+  { "zip-mode", no_argument, nullptr, 'z' },
+  { "bonus-file", required_argument, nullptr, 'b' },
+  { "block-limit", required_argument, nullptr, 0 },
+  { "debug-dir", required_argument, nullptr, 0 },
+  { "split-info", required_argument, nullptr, 0 },
+  { "verbose", no_argument, nullptr, 'v' },
+  { nullptr, 0, nullptr, 0 },
 };
 
+ImageChunk::ImageChunk(int type, size_t start, const std::vector<uint8_t>* file_content,
+                       size_t raw_data_len, std::string entry_name)
+    : type_(type),
+      start_(start),
+      input_file_ptr_(file_content),
+      raw_data_len_(raw_data_len),
+      compress_level_(6),
+      entry_name_(std::move(entry_name)) {
+  CHECK(file_content != nullptr) << "input file container can't be nullptr";
+}
+
 const uint8_t* ImageChunk::GetRawData() const {
   CHECK_LE(start_ + raw_data_len_, input_file_ptr_->size());
   return input_file_ptr_->data() + start_;
@@ -273,6 +285,11 @@
   return raw_data_len_;
 }
 
+void ImageChunk::Dump(size_t index) const {
+  LOG(INFO) << "chunk: " << index << ", type: " << type_ << ", start: " << start_
+            << ", len: " << DataLengthForPatch() << ", name: " << entry_name_;
+}
+
 bool ImageChunk::operator==(const ImageChunk& other) const {
   if (type_ != other.type_) {
     return false;
@@ -281,20 +298,6 @@
           memcmp(GetRawData(), other.GetRawData(), raw_data_len_) == 0);
 }
 
-void ImageChunk::SetSourceInfo(const ImageChunk& src) {
-  source_start_ = src.start_;
-  if (type_ == CHUNK_NORMAL) {
-    source_len_ = src.raw_data_len_;
-  } else if (type_ == CHUNK_DEFLATE) {
-    source_len_ = src.raw_data_len_;
-    source_uncompressed_len_ = src.uncompressed_data_.size();
-  }
-}
-
-void ImageChunk::SetEntryName(std::string entryname) {
-  entry_name_ = std::move(entryname);
-}
-
 void ImageChunk::SetUncompressedData(std::vector<uint8_t> data) {
   uncompressed_data_ = std::move(data);
 }
@@ -307,80 +310,13 @@
   return true;
 }
 
-// Convert CHUNK_NORMAL & CHUNK_DEFLATE to CHUNK_RAW if the target size is
-// smaller. Also take the header size into account during size comparison.
-bool ImageChunk::ChangeChunkToRaw(size_t patch_size) {
-  if (type_ == CHUNK_RAW) {
-    return true;
-  } else if (type_ == CHUNK_NORMAL && (raw_data_len_ <= 160 || raw_data_len_ < patch_size)) {
-    type_ = CHUNK_RAW;
-    return true;
-  }
-  return false;
-}
-
 void ImageChunk::ChangeDeflateChunkToNormal() {
   if (type_ != CHUNK_DEFLATE) return;
   type_ = CHUNK_NORMAL;
-  entry_name_.clear();
+  // No need to clear the entry name.
   uncompressed_data_.clear();
 }
 
-// Header size:
-// header_type    4 bytes
-// CHUNK_NORMAL   8*3 = 24 bytes
-// CHUNK_DEFLATE  8*5 + 4*5 = 60 bytes
-// CHUNK_RAW      4 bytes + patch_size
-size_t ImageChunk::GetHeaderSize(size_t patch_size) const {
-  switch (type_) {
-    case CHUNK_NORMAL:
-      return 4 + 8 * 3;
-    case CHUNK_DEFLATE:
-      return 4 + 8 * 5 + 4 * 5;
-    case CHUNK_RAW:
-      return 4 + 4 + patch_size;
-    default:
-      CHECK(false) << "unexpected chunk type: " << type_;  // Should not reach here.
-      return 0;
-  }
-}
-
-size_t ImageChunk::WriteHeaderToFd(int fd, const std::vector<uint8_t>& patch, size_t offset) {
-  Write4(fd, type_);
-  switch (type_) {
-    case CHUNK_NORMAL:
-      printf("normal   (%10zu, %10zu)  %10zu\n", start_, raw_data_len_, patch.size());
-      Write8(fd, static_cast<int64_t>(source_start_));
-      Write8(fd, static_cast<int64_t>(source_len_));
-      Write8(fd, static_cast<int64_t>(offset));
-      return offset + patch.size();
-    case CHUNK_DEFLATE:
-      printf("deflate  (%10zu, %10zu)  %10zu  %s\n", start_, raw_data_len_, patch.size(),
-             entry_name_.c_str());
-      Write8(fd, static_cast<int64_t>(source_start_));
-      Write8(fd, static_cast<int64_t>(source_len_));
-      Write8(fd, static_cast<int64_t>(offset));
-      Write8(fd, static_cast<int64_t>(source_uncompressed_len_));
-      Write8(fd, static_cast<int64_t>(uncompressed_data_.size()));
-      Write4(fd, compress_level_);
-      Write4(fd, METHOD);
-      Write4(fd, WINDOWBITS);
-      Write4(fd, MEMLEVEL);
-      Write4(fd, STRATEGY);
-      return offset + patch.size();
-    case CHUNK_RAW:
-      printf("raw      (%10zu, %10zu)\n", start_, raw_data_len_);
-      Write4(fd, static_cast<int32_t>(patch.size()));
-      if (!android::base::WriteFully(fd, patch.data(), patch.size())) {
-        CHECK(false) << "failed to write " << patch.size() <<" bytes patch";
-      }
-      return offset;
-    default:
-      CHECK(false) << "unexpected chunk type: " << type_;
-      return offset;
-  }
-}
-
 bool ImageChunk::IsAdjacentNormal(const ImageChunk& other) const {
   if (type_ != CHUNK_NORMAL || other.type_ != CHUNK_NORMAL) {
     return false;
@@ -393,14 +329,62 @@
   raw_data_len_ = raw_data_len_ + other.raw_data_len_;
 }
 
-bool ImageChunk::ReconstructDeflateChunk() {
-  if (type_ != CHUNK_DEFLATE) {
-    printf("attempt to reconstruct non-deflate chunk\n");
+bool ImageChunk::MakePatch(const ImageChunk& tgt, const ImageChunk& src,
+                           std::vector<uint8_t>* patch_data,
+                           bsdiff::SuffixArrayIndexInterface** bsdiff_cache) {
+#if defined(__ANDROID__)
+  char ptemp[] = "/data/local/tmp/imgdiff-patch-XXXXXX";
+#else
+  char ptemp[] = "/tmp/imgdiff-patch-XXXXXX";
+#endif
+
+  int fd = mkstemp(ptemp);
+  if (fd == -1) {
+    PLOG(ERROR) << "MakePatch failed to create a temporary file";
+    return false;
+  }
+  close(fd);
+
+  int r = bsdiff::bsdiff(src.DataForPatch(), src.DataLengthForPatch(), tgt.DataForPatch(),
+                         tgt.DataLengthForPatch(), ptemp, bsdiff_cache);
+  if (r != 0) {
+    LOG(ERROR) << "bsdiff() failed: " << r;
     return false;
   }
 
-  // We only check two combinations of encoder parameters:  level 6
-  // (the default) and level 9 (the maximum).
+  android::base::unique_fd patch_fd(open(ptemp, O_RDONLY));
+  if (patch_fd == -1) {
+    PLOG(ERROR) << "Failed to open " << ptemp;
+    return false;
+  }
+  struct stat st;
+  if (fstat(patch_fd, &st) != 0) {
+    PLOG(ERROR) << "Failed to stat patch file " << ptemp;
+    return false;
+  }
+
+  size_t sz = static_cast<size_t>(st.st_size);
+
+  patch_data->resize(sz);
+  if (!android::base::ReadFully(patch_fd, patch_data->data(), sz)) {
+    PLOG(ERROR) << "Failed to read " << ptemp;
+    unlink(ptemp);
+    return false;
+  }
+
+  unlink(ptemp);
+
+  return true;
+}
+
+bool ImageChunk::ReconstructDeflateChunk() {
+  if (type_ != CHUNK_DEFLATE) {
+    LOG(ERROR) << "Attempted to reconstruct non-deflate chunk";
+    return false;
+  }
+
+  // We only check two combinations of encoder parameters:  level 6 (the default) and level 9
+  // (the maximum).
   for (int level = 6; level <= 9; level += 3) {
     if (TryReconstruction(level)) {
       compress_level_ = level;
@@ -412,10 +396,9 @@
 }
 
 /*
- * 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).
+ * 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).
  */
 bool ImageChunk::TryReconstruction(int level) {
   z_stream strm;
@@ -426,7 +409,7 @@
   strm.next_in = uncompressed_data_.data();
   int ret = deflateInit2(&strm, level, METHOD, WINDOWBITS, MEMLEVEL, STRATEGY);
   if (ret < 0) {
-    printf("failed to initialize deflate: %d\n", ret);
+    LOG(ERROR) << "Failed to initialize deflate: " << ret;
     return false;
   }
 
@@ -437,7 +420,7 @@
     strm.next_out = buffer.data();
     ret = deflate(&strm, Z_FINISH);
     if (ret < 0) {
-      printf("failed to deflate: %d\n", ret);
+      LOG(ERROR) << "Failed to deflate: " << ret;
       return false;
     }
 
@@ -458,29 +441,381 @@
   return true;
 }
 
+PatchChunk::PatchChunk(const ImageChunk& tgt, const ImageChunk& src, std::vector<uint8_t> data)
+    : type_(tgt.GetType()),
+      source_start_(src.GetStartOffset()),
+      source_len_(src.GetRawDataLength()),
+      source_uncompressed_len_(src.DataLengthForPatch()),
+      target_start_(tgt.GetStartOffset()),
+      target_len_(tgt.GetRawDataLength()),
+      target_uncompressed_len_(tgt.DataLengthForPatch()),
+      target_compress_level_(tgt.GetCompressLevel()),
+      data_(std::move(data)) {}
+
+// Construct a CHUNK_RAW patch from the target data directly.
+PatchChunk::PatchChunk(const ImageChunk& tgt)
+    : type_(CHUNK_RAW),
+      source_start_(0),
+      source_len_(0),
+      source_uncompressed_len_(0),
+      target_start_(tgt.GetStartOffset()),
+      target_len_(tgt.GetRawDataLength()),
+      target_uncompressed_len_(tgt.DataLengthForPatch()),
+      target_compress_level_(tgt.GetCompressLevel()),
+      data_(tgt.DataForPatch(), tgt.DataForPatch() + tgt.DataLengthForPatch()) {}
+
+// Return true if raw data is smaller than the patch size.
+bool PatchChunk::RawDataIsSmaller(const ImageChunk& tgt, size_t patch_size) {
+  size_t target_len = tgt.GetRawDataLength();
+  return (tgt.GetType() == CHUNK_NORMAL && (target_len <= 160 || target_len < patch_size));
+}
+
+void PatchChunk::UpdateSourceOffset(const SortedRangeSet& src_range) {
+  if (type_ == CHUNK_DEFLATE) {
+    source_start_ = src_range.GetOffsetInRangeSet(source_start_);
+  }
+}
+
+// Header size:
+// header_type    4 bytes
+// CHUNK_NORMAL   8*3 = 24 bytes
+// CHUNK_DEFLATE  8*5 + 4*5 = 60 bytes
+// CHUNK_RAW      4 bytes + patch_size
+size_t PatchChunk::GetHeaderSize() const {
+  switch (type_) {
+    case CHUNK_NORMAL:
+      return 4 + 8 * 3;
+    case CHUNK_DEFLATE:
+      return 4 + 8 * 5 + 4 * 5;
+    case CHUNK_RAW:
+      return 4 + 4 + data_.size();
+    default:
+      CHECK(false) << "unexpected chunk type: " << type_;  // Should not reach here.
+      return 0;
+  }
+}
+
+// Return the offset of the next patch into the patch data.
+size_t PatchChunk::WriteHeaderToFd(int fd, size_t offset, size_t index) const {
+  Write4(fd, type_);
+  switch (type_) {
+    case CHUNK_NORMAL:
+      LOG(INFO) << android::base::StringPrintf("chunk %zu: normal   (%10zu, %10zu)  %10zu", index,
+                                               target_start_, target_len_, data_.size());
+      Write8(fd, static_cast<int64_t>(source_start_));
+      Write8(fd, static_cast<int64_t>(source_len_));
+      Write8(fd, static_cast<int64_t>(offset));
+      return offset + data_.size();
+    case CHUNK_DEFLATE:
+      LOG(INFO) << android::base::StringPrintf("chunk %zu: deflate  (%10zu, %10zu)  %10zu", index,
+                                               target_start_, target_len_, data_.size());
+      Write8(fd, static_cast<int64_t>(source_start_));
+      Write8(fd, static_cast<int64_t>(source_len_));
+      Write8(fd, static_cast<int64_t>(offset));
+      Write8(fd, static_cast<int64_t>(source_uncompressed_len_));
+      Write8(fd, static_cast<int64_t>(target_uncompressed_len_));
+      Write4(fd, target_compress_level_);
+      Write4(fd, ImageChunk::METHOD);
+      Write4(fd, ImageChunk::WINDOWBITS);
+      Write4(fd, ImageChunk::MEMLEVEL);
+      Write4(fd, ImageChunk::STRATEGY);
+      return offset + data_.size();
+    case CHUNK_RAW:
+      LOG(INFO) << android::base::StringPrintf("chunk %zu: raw      (%10zu, %10zu)", index,
+                                               target_start_, target_len_);
+      Write4(fd, static_cast<int32_t>(data_.size()));
+      if (!android::base::WriteFully(fd, data_.data(), data_.size())) {
+        CHECK(false) << "Failed to write " << data_.size() << " bytes patch";
+      }
+      return offset;
+    default:
+      CHECK(false) << "unexpected chunk type: " << type_;
+      return offset;
+  }
+}
+
+size_t PatchChunk::PatchSize() const {
+  if (type_ == CHUNK_RAW) {
+    return GetHeaderSize();
+  }
+  return GetHeaderSize() + data_.size();
+}
+
+// Write the contents of |patch_chunks| to |patch_fd|.
+bool PatchChunk::WritePatchDataToFd(const std::vector<PatchChunk>& patch_chunks, int patch_fd) {
+  // 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 (const auto& patch : patch_chunks) {
+    total_header_size += patch.GetHeaderSize();
+  }
+
+  size_t offset = total_header_size;
+
+  // Write out the headers.
+  if (!android::base::WriteStringToFd("IMGDIFF" + std::to_string(VERSION), patch_fd)) {
+    PLOG(ERROR) << "Failed to write \"IMGDIFF" << VERSION << "\"";
+    return false;
+  }
+
+  Write4(patch_fd, static_cast<int32_t>(patch_chunks.size()));
+  LOG(INFO) << "Writing " << patch_chunks.size() << " patch headers...";
+  for (size_t i = 0; i < patch_chunks.size(); ++i) {
+    offset = patch_chunks[i].WriteHeaderToFd(patch_fd, offset, i);
+  }
+
+  // Append each chunk's bsdiff patch, in order.
+  for (const auto& patch : patch_chunks) {
+    if (patch.type_ == CHUNK_RAW) {
+      continue;
+    }
+    if (!android::base::WriteFully(patch_fd, patch.data_.data(), patch.data_.size())) {
+      PLOG(ERROR) << "Failed to write " << patch.data_.size() << " bytes patch to patch_fd";
+      return false;
+    }
+  }
+
+  return true;
+}
+
+ImageChunk& Image::operator[](size_t i) {
+  CHECK_LT(i, chunks_.size());
+  return chunks_[i];
+}
+
+const ImageChunk& Image::operator[](size_t i) const {
+  CHECK_LT(i, chunks_.size());
+  return chunks_[i];
+}
+
+void Image::MergeAdjacentNormalChunks() {
+  size_t merged_last = 0, cur = 0;
+  while (cur < chunks_.size()) {
+    // Look for normal chunks adjacent to the current one. If such chunk exists, extend the
+    // length of the current normal chunk.
+    size_t to_check = cur + 1;
+    while (to_check < chunks_.size() && chunks_[cur].IsAdjacentNormal(chunks_[to_check])) {
+      chunks_[cur].MergeAdjacentNormal(chunks_[to_check]);
+      to_check++;
+    }
+
+    if (merged_last != cur) {
+      chunks_[merged_last] = std::move(chunks_[cur]);
+    }
+    merged_last++;
+    cur = to_check;
+  }
+  if (merged_last < chunks_.size()) {
+    chunks_.erase(chunks_.begin() + merged_last, chunks_.end());
+  }
+}
+
+void Image::DumpChunks() const {
+  std::string type = is_source_ ? "source" : "target";
+  LOG(INFO) << "Dumping chunks for " << type;
+  for (size_t i = 0; i < chunks_.size(); ++i) {
+    chunks_[i].Dump(i);
+  }
+}
+
+bool Image::ReadFile(const std::string& filename, std::vector<uint8_t>* file_content) {
+  CHECK(file_content != nullptr);
+
+  android::base::unique_fd fd(open(filename.c_str(), O_RDONLY));
+  if (fd == -1) {
+    PLOG(ERROR) << "Failed to open " << filename;
+    return false;
+  }
+  struct stat st;
+  if (fstat(fd, &st) != 0) {
+    PLOG(ERROR) << "Failed to stat " << filename;
+    return false;
+  }
+
+  size_t sz = static_cast<size_t>(st.st_size);
+  file_content->resize(sz);
+  if (!android::base::ReadFully(fd, file_content->data(), sz)) {
+    PLOG(ERROR) << "Failed to read " << filename;
+    return false;
+  }
+  fd.reset();
+
+  return true;
+}
+
+bool ZipModeImage::Initialize(const std::string& filename) {
+  if (!ReadFile(filename, &file_content_)) {
+    return false;
+  }
+
+  // Omit the trailing zeros before we pass the file to ziparchive handler.
+  size_t zipfile_size;
+  if (!GetZipFileSize(&zipfile_size)) {
+    LOG(ERROR) << "Failed to parse the actual size of " << filename;
+    return false;
+  }
+  ZipArchiveHandle handle;
+  int err = OpenArchiveFromMemory(const_cast<uint8_t*>(file_content_.data()), zipfile_size,
+                                  filename.c_str(), &handle);
+  if (err != 0) {
+    LOG(ERROR) << "Failed to open zip file " << filename << ": " << ErrorCodeString(err);
+    CloseArchive(handle);
+    return false;
+  }
+
+  if (!InitializeChunks(filename, handle)) {
+    CloseArchive(handle);
+    return false;
+  }
+
+  CloseArchive(handle);
+  return true;
+}
+
+// Iterate the zip entries and compose the image chunks accordingly.
+bool ZipModeImage::InitializeChunks(const std::string& filename, ZipArchiveHandle handle) {
+  void* cookie;
+  int ret = StartIteration(handle, &cookie, nullptr, nullptr);
+  if (ret != 0) {
+    LOG(ERROR) << "Failed to iterate over entries in " << filename << ": " << ErrorCodeString(ret);
+    return false;
+  }
+
+  // Create a list of deflated zip entries, sorted by offset.
+  std::vector<std::pair<std::string, ZipEntry>> temp_entries;
+  ZipString name;
+  ZipEntry entry;
+  while ((ret = Next(cookie, &entry, &name)) == 0) {
+    if (entry.method == kCompressDeflated || limit_ > 0) {
+      std::string entry_name(name.name, name.name + name.name_length);
+      temp_entries.emplace_back(entry_name, entry);
+    }
+  }
+
+  if (ret != -1) {
+    LOG(ERROR) << "Error while iterating over zip entries: " << ErrorCodeString(ret);
+    return false;
+  }
+  std::sort(temp_entries.begin(), temp_entries.end(),
+            [](auto& entry1, auto& entry2) { return entry1.second.offset < entry2.second.offset; });
+
+  EndIteration(cookie);
+
+  // For source chunks, we don't need to compose chunks for the metadata.
+  if (is_source_) {
+    for (auto& entry : temp_entries) {
+      if (!AddZipEntryToChunks(handle, entry.first, &entry.second)) {
+        LOG(ERROR) << "Failed to add " << entry.first << " to source chunks";
+        return false;
+      }
+    }
+
+    // Add the end of zip file (mainly central directory) as a normal chunk.
+    size_t entries_end = 0;
+    if (!temp_entries.empty()) {
+      entries_end = static_cast<size_t>(temp_entries.back().second.offset +
+                                        temp_entries.back().second.compressed_length);
+    }
+    CHECK_LT(entries_end, file_content_.size());
+    chunks_.emplace_back(CHUNK_NORMAL, entries_end, &file_content_,
+                         file_content_.size() - entries_end);
+
+    return true;
+  }
+
+  // For target chunks, add the deflate entries as CHUNK_DEFLATE and the contents between two
+  // deflate entries as CHUNK_NORMAL.
+  size_t pos = 0;
+  size_t nextentry = 0;
+  while (pos < file_content_.size()) {
+    if (nextentry < temp_entries.size() &&
+        static_cast<off64_t>(pos) == temp_entries[nextentry].second.offset) {
+      // Add the next zip entry.
+      std::string entry_name = temp_entries[nextentry].first;
+      if (!AddZipEntryToChunks(handle, entry_name, &temp_entries[nextentry].second)) {
+        LOG(ERROR) << "Failed to add " << entry_name << " to target chunks";
+        return false;
+      }
+
+      pos += temp_entries[nextentry].second.compressed_length;
+      ++nextentry;
+      continue;
+    }
+
+    // Use a normal chunk to take all the data up to the start of the next entry.
+    size_t raw_data_len;
+    if (nextentry < temp_entries.size()) {
+      raw_data_len = temp_entries[nextentry].second.offset - pos;
+    } else {
+      raw_data_len = file_content_.size() - pos;
+    }
+    chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, raw_data_len);
+
+    pos += raw_data_len;
+  }
+
+  return true;
+}
+
+bool ZipModeImage::AddZipEntryToChunks(ZipArchiveHandle handle, const std::string& entry_name,
+                                       ZipEntry* entry) {
+  size_t compressed_len = entry->compressed_length;
+  if (compressed_len == 0) return true;
+
+  // Split the entry into several normal chunks if it's too large.
+  if (limit_ > 0 && compressed_len > limit_) {
+    int count = 0;
+    while (compressed_len > 0) {
+      size_t length = std::min(limit_, compressed_len);
+      std::string name = entry_name + "-" + std::to_string(count);
+      chunks_.emplace_back(CHUNK_NORMAL, entry->offset + limit_ * count, &file_content_, length,
+                           name);
+
+      count++;
+      compressed_len -= length;
+    }
+  } else if (entry->method == kCompressDeflated) {
+    size_t uncompressed_len = entry->uncompressed_length;
+    std::vector<uint8_t> uncompressed_data(uncompressed_len);
+    int ret = ExtractToMemory(handle, entry, uncompressed_data.data(), uncompressed_len);
+    if (ret != 0) {
+      LOG(ERROR) << "Failed to extract " << entry_name << " with size " << uncompressed_len << ": "
+                 << ErrorCodeString(ret);
+      return false;
+    }
+    ImageChunk curr(CHUNK_DEFLATE, entry->offset, &file_content_, compressed_len, entry_name);
+    curr.SetUncompressedData(std::move(uncompressed_data));
+    chunks_.push_back(std::move(curr));
+  } else {
+    chunks_.emplace_back(CHUNK_NORMAL, entry->offset, &file_content_, compressed_len, entry_name);
+  }
+
+  return true;
+}
+
 // EOCD record
 // offset 0: signature 0x06054b50, 4 bytes
 // offset 4: number of this disk, 2 bytes
 // ...
 // offset 20: comment length, 2 bytes
 // offset 22: comment, n bytes
-static bool GetZipFileSize(const std::vector<uint8_t>& zip_file, size_t* input_file_size) {
-  if (zip_file.size() < 22) {
-    printf("file is too small to be a zip file\n");
+bool ZipModeImage::GetZipFileSize(size_t* input_file_size) {
+  if (file_content_.size() < 22) {
+    LOG(ERROR) << "File is too small to be a zip file";
     return false;
   }
 
   // Look for End of central directory record of the zip file, and calculate the actual
   // zip_file size.
-  for (int i = zip_file.size() - 22; i >= 0; i--) {
-    if (zip_file[i] == 0x50) {
-      if (get_unaligned<uint32_t>(&zip_file[i]) == 0x06054b50) {
+  for (int i = file_content_.size() - 22; i >= 0; i--) {
+    if (file_content_[i] == 0x50) {
+      if (get_unaligned<uint32_t>(&file_content_[i]) == 0x06054b50) {
         // double-check: this archive consists of a single "disk".
-        CHECK_EQ(get_unaligned<uint16_t>(&zip_file[i + 4]), 0);
+        CHECK_EQ(get_unaligned<uint16_t>(&file_content_[i + 4]), 0);
 
-        uint16_t comment_length = get_unaligned<uint16_t>(&zip_file[i + 20]);
+        uint16_t comment_length = get_unaligned<uint16_t>(&file_content_[i + 20]);
         size_t file_size = i + 22 + comment_length;
-        CHECK_LE(file_size, zip_file.size());
+        CHECK_LE(file_size, file_content_.size());
         *input_file_size = file_size;
         return true;
       }
@@ -491,162 +826,445 @@
   return false;
 }
 
-static bool ReadZip(const char* filename, std::vector<ImageChunk>* chunks,
-                    std::vector<uint8_t>* zip_file, bool include_pseudo_chunk) {
-  CHECK(chunks != nullptr && zip_file != nullptr);
+ImageChunk ZipModeImage::PseudoSource() const {
+  CHECK(is_source_);
+  return ImageChunk(CHUNK_NORMAL, 0, &file_content_, file_content_.size());
+}
 
-  android::base::unique_fd fd(open(filename, O_RDONLY));
-  if (fd == -1) {
-    printf("failed to open \"%s\" %s\n", filename, strerror(errno));
-    return false;
+const ImageChunk* ZipModeImage::FindChunkByName(const std::string& name, bool find_normal) const {
+  if (name.empty()) {
+    return nullptr;
   }
-  struct stat st;
-  if (fstat(fd, &st) != 0) {
-    printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
-    return false;
-  }
-
-  size_t sz = static_cast<size_t>(st.st_size);
-  zip_file->resize(sz);
-  if (!android::base::ReadFully(fd, zip_file->data(), sz)) {
-    printf("failed to read \"%s\" %s\n", filename, strerror(errno));
-    return false;
-  }
-  fd.reset();
-
-  // Trim the trailing zeros before we pass the file to ziparchive handler.
-  size_t zipfile_size;
-  if (!GetZipFileSize(*zip_file, &zipfile_size)) {
-    printf("failed to parse the actual size of %s\n", filename);
-    return false;
-  }
-  ZipArchiveHandle handle;
-  int err = OpenArchiveFromMemory(zip_file->data(), zipfile_size, filename, &handle);
-  if (err != 0) {
-    printf("failed to open zip file %s: %s\n", filename, ErrorCodeString(err));
-    CloseArchive(handle);
-    return false;
-  }
-
-  // Create a list of deflated zip entries, sorted by offset.
-  std::vector<std::pair<std::string, ZipEntry>> temp_entries;
-  void* cookie;
-  int ret = StartIteration(handle, &cookie, nullptr, nullptr);
-  if (ret != 0) {
-    printf("failed to iterate over entries in %s: %s\n", filename, ErrorCodeString(ret));
-    CloseArchive(handle);
-    return false;
-  }
-
-  ZipString name;
-  ZipEntry entry;
-  while ((ret = Next(cookie, &entry, &name)) == 0) {
-    if (entry.method == kCompressDeflated) {
-      std::string entryname(name.name, name.name + name.name_length);
-      temp_entries.push_back(std::make_pair(entryname, entry));
-    }
-  }
-
-  if (ret != -1) {
-    printf("Error while iterating over zip entries: %s\n", ErrorCodeString(ret));
-    CloseArchive(handle);
-    return false;
-  }
-  std::sort(temp_entries.begin(), temp_entries.end(),
-            [](auto& entry1, auto& entry2) {
-              return entry1.second.offset < entry2.second.offset;
-            });
-
-  EndIteration(cookie);
-
-  if (include_pseudo_chunk) {
-    chunks->emplace_back(CHUNK_NORMAL, 0, zip_file, zip_file->size());
-  }
-
-  size_t pos = 0;
-  size_t nextentry = 0;
-  while (pos < zip_file->size()) {
-    if (nextentry < temp_entries.size() &&
-        static_cast<off64_t>(pos) == temp_entries[nextentry].second.offset) {
-      // compose the next deflate chunk.
-      std::string entryname = temp_entries[nextentry].first;
-      size_t uncompressed_len = temp_entries[nextentry].second.uncompressed_length;
-      std::vector<uint8_t> uncompressed_data(uncompressed_len);
-      if ((ret = ExtractToMemory(handle, &temp_entries[nextentry].second, uncompressed_data.data(),
-                                 uncompressed_len)) != 0) {
-        printf("failed to extract %s with size %zu: %s\n", entryname.c_str(), uncompressed_len,
-               ErrorCodeString(ret));
-        CloseArchive(handle);
-        return false;
-      }
-
-      size_t compressed_len = temp_entries[nextentry].second.compressed_length;
-      ImageChunk curr(CHUNK_DEFLATE, pos, zip_file, compressed_len);
-      curr.SetEntryName(std::move(entryname));
-      curr.SetUncompressedData(std::move(uncompressed_data));
-      chunks->push_back(curr);
-
-      pos += compressed_len;
-      ++nextentry;
+  for (auto& chunk : chunks_) {
+    if (chunk.GetType() != CHUNK_DEFLATE && !find_normal) {
       continue;
     }
 
-    // Use a normal chunk to take all the data up to the start of the next deflate section.
-    size_t raw_data_len;
-    if (nextentry < temp_entries.size()) {
-      raw_data_len = temp_entries[nextentry].second.offset - pos;
-    } else {
-      raw_data_len = zip_file->size() - pos;
+    if (chunk.GetEntryName() == name) {
+      return &chunk;
     }
-    chunks->emplace_back(CHUNK_NORMAL, pos, zip_file, raw_data_len);
 
-    pos += raw_data_len;
+    // Edge case when target chunk is split due to size limit but source chunk isn't.
+    if (name == (chunk.GetEntryName() + "-0") || chunk.GetEntryName() == (name + "-0")) {
+      return &chunk;
+    }
+
+    // TODO handle the .so files with incremental version number.
+    // (e.g. lib/arm64-v8a/libcronet.59.0.3050.4.so)
   }
 
-  CloseArchive(handle);
+  return nullptr;
+}
+
+ImageChunk* ZipModeImage::FindChunkByName(const std::string& name, bool find_normal) {
+  return const_cast<ImageChunk*>(
+      static_cast<const ZipModeImage*>(this)->FindChunkByName(name, find_normal));
+}
+
+bool ZipModeImage::CheckAndProcessChunks(ZipModeImage* tgt_image, ZipModeImage* src_image) {
+  for (auto& tgt_chunk : *tgt_image) {
+    if (tgt_chunk.GetType() != CHUNK_DEFLATE) {
+      continue;
+    }
+
+    ImageChunk* src_chunk = src_image->FindChunkByName(tgt_chunk.GetEntryName());
+    if (src_chunk == nullptr) {
+      tgt_chunk.ChangeDeflateChunkToNormal();
+    } else if (tgt_chunk == *src_chunk) {
+      // 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.
+      tgt_chunk.ChangeDeflateChunkToNormal();
+      src_chunk->ChangeDeflateChunkToNormal();
+    } else if (!tgt_chunk.ReconstructDeflateChunk()) {
+      // We cannot recompress the data and get exactly the same bits as are in the input target
+      // image. Treat the chunk as a normal non-deflated chunk.
+      LOG(WARNING) << "Failed to reconstruct target deflate chunk [" << tgt_chunk.GetEntryName()
+                   << "]; treating as normal";
+
+      tgt_chunk.ChangeDeflateChunkToNormal();
+      src_chunk->ChangeDeflateChunkToNormal();
+    }
+  }
+
+  // For zips, we only need merge normal chunks for the target:  deflated chunks are matched via
+  // filename, and normal chunks are patched using the entire source file as the source.
+  if (tgt_image->limit_ == 0) {
+    tgt_image->MergeAdjacentNormalChunks();
+    tgt_image->DumpChunks();
+  }
+
   return true;
 }
 
-// Read the given file and break it up into chunks, and putting the data in to a vector.
-static bool ReadImage(const char* filename, std::vector<ImageChunk>* chunks,
-                      std::vector<uint8_t>* img) {
-  CHECK(chunks != nullptr && img != nullptr);
+// For each target chunk, look for the corresponding source chunk by the zip_entry name. If
+// found, add the range of this chunk in the original source file to the block aligned source
+// ranges. Construct the split src & tgt image once the size of source range reaches limit.
+bool ZipModeImage::SplitZipModeImageWithLimit(const ZipModeImage& tgt_image,
+                                              const ZipModeImage& src_image,
+                                              std::vector<ZipModeImage>* split_tgt_images,
+                                              std::vector<ZipModeImage>* split_src_images,
+                                              std::vector<SortedRangeSet>* split_src_ranges) {
+  CHECK_EQ(tgt_image.limit_, src_image.limit_);
+  size_t limit = tgt_image.limit_;
 
-  android::base::unique_fd fd(open(filename, O_RDONLY));
-  if (fd == -1) {
-    printf("failed to open \"%s\" %s\n", filename, strerror(errno));
-    return false;
+  src_image.DumpChunks();
+  LOG(INFO) << "Splitting " << tgt_image.NumOfChunks() << " tgt chunks...";
+
+  SortedRangeSet used_src_ranges;  // ranges used for previous split source images.
+
+  // Reserve the central directory in advance for the last split image.
+  const auto& central_directory = src_image.cend() - 1;
+  CHECK_EQ(CHUNK_NORMAL, central_directory->GetType());
+  used_src_ranges.Insert(central_directory->GetStartOffset(),
+                         central_directory->DataLengthForPatch());
+
+  SortedRangeSet src_ranges;
+  std::vector<ImageChunk> split_src_chunks;
+  std::vector<ImageChunk> split_tgt_chunks;
+  for (auto tgt = tgt_image.cbegin(); tgt != tgt_image.cend(); tgt++) {
+    const ImageChunk* src = src_image.FindChunkByName(tgt->GetEntryName(), true);
+    if (src == nullptr) {
+      split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_,
+                                    tgt->GetRawDataLength());
+      continue;
+    }
+
+    size_t src_offset = src->GetStartOffset();
+    size_t src_length = src->GetRawDataLength();
+
+    CHECK(src_length > 0);
+    CHECK_LE(src_length, limit);
+
+    // Make sure this source range hasn't been used before so that the src_range pieces don't
+    // overlap with each other.
+    if (!RemoveUsedBlocks(&src_offset, &src_length, used_src_ranges)) {
+      split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_,
+                                    tgt->GetRawDataLength());
+    } else if (src_ranges.blocks() * BLOCK_SIZE + src_length <= limit) {
+      src_ranges.Insert(src_offset, src_length);
+
+      // Add the deflate source chunk if it hasn't been aligned.
+      if (src->GetType() == CHUNK_DEFLATE && src_length == src->GetRawDataLength()) {
+        split_src_chunks.push_back(*src);
+        split_tgt_chunks.push_back(*tgt);
+      } else {
+        // TODO split smarter to avoid alignment of large deflate chunks
+        split_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt->GetStartOffset(), &tgt_image.file_content_,
+                                      tgt->GetRawDataLength());
+      }
+    } else {
+      bool added_image = ZipModeImage::AddSplitImageFromChunkList(
+          tgt_image, src_image, src_ranges, split_tgt_chunks, split_src_chunks, split_tgt_images,
+          split_src_images);
+
+      split_tgt_chunks.clear();
+      split_src_chunks.clear();
+      // No need to update the split_src_ranges if we don't update the split source images.
+      if (added_image) {
+        used_src_ranges.Insert(src_ranges);
+        split_src_ranges->push_back(std::move(src_ranges));
+      }
+      src_ranges.Clear();
+
+      // We don't have enough space for the current chunk; start a new split image and handle
+      // this chunk there.
+      tgt--;
+    }
   }
-  struct stat st;
-  if (fstat(fd, &st) != 0) {
-    printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
+
+  // TODO Trim it in case the CD exceeds limit too much.
+  src_ranges.Insert(central_directory->GetStartOffset(), central_directory->DataLengthForPatch());
+  bool added_image = ZipModeImage::AddSplitImageFromChunkList(tgt_image, src_image, src_ranges,
+                                                              split_tgt_chunks, split_src_chunks,
+                                                              split_tgt_images, split_src_images);
+  if (added_image) {
+    split_src_ranges->push_back(std::move(src_ranges));
+  }
+
+  ValidateSplitImages(*split_tgt_images, *split_src_images, *split_src_ranges,
+                      tgt_image.file_content_.size());
+
+  return true;
+}
+
+bool ZipModeImage::AddSplitImageFromChunkList(const ZipModeImage& tgt_image,
+                                              const ZipModeImage& src_image,
+                                              const SortedRangeSet& split_src_ranges,
+                                              const std::vector<ImageChunk>& split_tgt_chunks,
+                                              const std::vector<ImageChunk>& split_src_chunks,
+                                              std::vector<ZipModeImage>* split_tgt_images,
+                                              std::vector<ZipModeImage>* split_src_images) {
+  CHECK(!split_tgt_chunks.empty());
+
+  std::vector<ImageChunk> aligned_tgt_chunks;
+
+  // Align the target chunks in the beginning with BLOCK_SIZE.
+  size_t i = 0;
+  while (i < split_tgt_chunks.size()) {
+    size_t tgt_start = split_tgt_chunks[i].GetStartOffset();
+    size_t tgt_length = split_tgt_chunks[i].GetRawDataLength();
+
+    // Current ImageChunk is long enough to align.
+    if (AlignHead(&tgt_start, &tgt_length)) {
+      aligned_tgt_chunks.emplace_back(CHUNK_NORMAL, tgt_start, &tgt_image.file_content_,
+                                      tgt_length);
+      break;
+    }
+
+    i++;
+  }
+
+  // Nothing left after alignment in the current split tgt chunks; skip adding the split_tgt_image.
+  if (i == split_tgt_chunks.size()) {
     return false;
   }
 
-  size_t sz = static_cast<size_t>(st.st_size);
-  img->resize(sz);
-  if (!android::base::ReadFully(fd, img->data(), sz)) {
-    printf("failed to read \"%s\" %s\n", filename, strerror(errno));
+  aligned_tgt_chunks.insert(aligned_tgt_chunks.end(), split_tgt_chunks.begin() + i + 1,
+                            split_tgt_chunks.end());
+  CHECK(!aligned_tgt_chunks.empty());
+
+  // Add a normal chunk to align the contents in the end.
+  size_t end_offset =
+      aligned_tgt_chunks.back().GetStartOffset() + aligned_tgt_chunks.back().GetRawDataLength();
+  if (end_offset % BLOCK_SIZE != 0 && end_offset < tgt_image.file_content_.size()) {
+    size_t tail_block_length = std::min<size_t>(tgt_image.file_content_.size() - end_offset,
+                                                BLOCK_SIZE - (end_offset % BLOCK_SIZE));
+    aligned_tgt_chunks.emplace_back(CHUNK_NORMAL, end_offset, &tgt_image.file_content_,
+                                    tail_block_length);
+  }
+
+  ZipModeImage split_tgt_image(false);
+  split_tgt_image.Initialize(std::move(aligned_tgt_chunks), {});
+  split_tgt_image.MergeAdjacentNormalChunks();
+
+  // Construct the dummy source file based on the src_ranges.
+  std::vector<uint8_t> src_content;
+  for (const auto& r : split_src_ranges) {
+    size_t end = std::min(src_image.file_content_.size(), r.second * BLOCK_SIZE);
+    src_content.insert(src_content.end(), src_image.file_content_.begin() + r.first * BLOCK_SIZE,
+                       src_image.file_content_.begin() + end);
+  }
+
+  // We should not have an empty src in our design; otherwise we will encounter an error in
+  // bsdiff since src_content.data() == nullptr.
+  CHECK(!src_content.empty());
+
+  ZipModeImage split_src_image(true);
+  split_src_image.Initialize(split_src_chunks, std::move(src_content));
+
+  split_tgt_images->push_back(std::move(split_tgt_image));
+  split_src_images->push_back(std::move(split_src_image));
+
+  return true;
+}
+
+void ZipModeImage::ValidateSplitImages(const std::vector<ZipModeImage>& split_tgt_images,
+                                       const std::vector<ZipModeImage>& split_src_images,
+                                       std::vector<SortedRangeSet>& split_src_ranges,
+                                       size_t total_tgt_size) {
+  CHECK_EQ(split_tgt_images.size(), split_src_images.size());
+
+  LOG(INFO) << "Validating " << split_tgt_images.size() << " images";
+
+  // Verify that the target image pieces is continuous and can add up to the total size.
+  size_t last_offset = 0;
+  for (const auto& tgt_image : split_tgt_images) {
+    CHECK(!tgt_image.chunks_.empty());
+
+    CHECK_EQ(last_offset, tgt_image.chunks_.front().GetStartOffset());
+    CHECK(last_offset % BLOCK_SIZE == 0);
+
+    // Check the target chunks within the split image are continuous.
+    for (const auto& chunk : tgt_image.chunks_) {
+      CHECK_EQ(last_offset, chunk.GetStartOffset());
+      last_offset += chunk.GetRawDataLength();
+    }
+  }
+  CHECK_EQ(total_tgt_size, last_offset);
+
+  // Verify that the source ranges are mutually exclusive.
+  CHECK_EQ(split_src_images.size(), split_src_ranges.size());
+  SortedRangeSet used_src_ranges;
+  for (size_t i = 0; i < split_src_ranges.size(); i++) {
+    CHECK(!used_src_ranges.Overlaps(split_src_ranges[i]))
+        << "src range " << split_src_ranges[i].ToString() << " overlaps "
+        << used_src_ranges.ToString();
+    used_src_ranges.Insert(split_src_ranges[i]);
+  }
+}
+
+bool ZipModeImage::GeneratePatchesInternal(const ZipModeImage& tgt_image,
+                                           const ZipModeImage& src_image,
+                                           std::vector<PatchChunk>* patch_chunks) {
+  LOG(INFO) << "Constructing patches for " << tgt_image.NumOfChunks() << " chunks...";
+  patch_chunks->clear();
+
+  bsdiff::SuffixArrayIndexInterface* bsdiff_cache = nullptr;
+  for (size_t i = 0; i < tgt_image.NumOfChunks(); i++) {
+    const auto& tgt_chunk = tgt_image[i];
+
+    if (PatchChunk::RawDataIsSmaller(tgt_chunk, 0)) {
+      patch_chunks->emplace_back(tgt_chunk);
+      continue;
+    }
+
+    const ImageChunk* src_chunk = (tgt_chunk.GetType() != CHUNK_DEFLATE)
+                                      ? nullptr
+                                      : src_image.FindChunkByName(tgt_chunk.GetEntryName());
+
+    const auto& src_ref = (src_chunk == nullptr) ? src_image.PseudoSource() : *src_chunk;
+    bsdiff::SuffixArrayIndexInterface** bsdiff_cache_ptr =
+        (src_chunk == nullptr) ? &bsdiff_cache : nullptr;
+
+    std::vector<uint8_t> patch_data;
+    if (!ImageChunk::MakePatch(tgt_chunk, src_ref, &patch_data, bsdiff_cache_ptr)) {
+      LOG(ERROR) << "Failed to generate patch, name: " << tgt_chunk.GetEntryName();
+      return false;
+    }
+
+    LOG(INFO) << "patch " << i << " is " << patch_data.size() << " bytes (of "
+              << tgt_chunk.GetRawDataLength() << ")";
+
+    if (PatchChunk::RawDataIsSmaller(tgt_chunk, patch_data.size())) {
+      patch_chunks->emplace_back(tgt_chunk);
+    } else {
+      patch_chunks->emplace_back(tgt_chunk, src_ref, std::move(patch_data));
+    }
+  }
+  delete bsdiff_cache;
+
+  CHECK_EQ(patch_chunks->size(), tgt_image.NumOfChunks());
+  return true;
+}
+
+bool ZipModeImage::GeneratePatches(const ZipModeImage& tgt_image, const ZipModeImage& src_image,
+                                   const std::string& patch_name) {
+  std::vector<PatchChunk> patch_chunks;
+
+  ZipModeImage::GeneratePatchesInternal(tgt_image, src_image, &patch_chunks);
+
+  CHECK_EQ(tgt_image.NumOfChunks(), patch_chunks.size());
+
+  android::base::unique_fd patch_fd(
+      open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
+  if (patch_fd == -1) {
+    PLOG(ERROR) << "Failed to open " << patch_name;
     return false;
   }
 
+  return PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd);
+}
+
+bool ZipModeImage::GeneratePatches(const std::vector<ZipModeImage>& split_tgt_images,
+                                   const std::vector<ZipModeImage>& split_src_images,
+                                   const std::vector<SortedRangeSet>& split_src_ranges,
+                                   const std::string& patch_name,
+                                   const std::string& split_info_file,
+                                   const std::string& debug_dir) {
+  LOG(INFO) << "Constructing patches for " << split_tgt_images.size() << " split images...";
+
+  android::base::unique_fd patch_fd(
+      open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
+  if (patch_fd == -1) {
+    PLOG(ERROR) << "Failed to open " << patch_name;
+    return false;
+  }
+
+  std::vector<std::string> split_info_list;
+  for (size_t i = 0; i < split_tgt_images.size(); i++) {
+    std::vector<PatchChunk> patch_chunks;
+    if (!ZipModeImage::GeneratePatchesInternal(split_tgt_images[i], split_src_images[i],
+                                               &patch_chunks)) {
+      LOG(ERROR) << "Failed to generate split patch";
+      return false;
+    }
+
+    size_t total_patch_size = 12;
+    for (auto& p : patch_chunks) {
+      p.UpdateSourceOffset(split_src_ranges[i]);
+      total_patch_size += p.PatchSize();
+    }
+
+    if (!PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd)) {
+      return false;
+    }
+
+    size_t split_tgt_size = split_tgt_images[i].chunks_.back().GetStartOffset() +
+                            split_tgt_images[i].chunks_.back().GetRawDataLength() -
+                            split_tgt_images[i].chunks_.front().GetStartOffset();
+    std::string split_info = android::base::StringPrintf(
+        "%zu %zu %s", total_patch_size, split_tgt_size, split_src_ranges[i].ToString().c_str());
+    split_info_list.push_back(split_info);
+
+    // Write the split source & patch into the debug directory.
+    if (!debug_dir.empty()) {
+      std::string src_name = android::base::StringPrintf("%s/src-%zu", debug_dir.c_str(), i);
+      android::base::unique_fd fd(
+          open(src_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
+
+      if (fd == -1) {
+        PLOG(ERROR) << "Failed to open " << src_name;
+        return false;
+      }
+      if (!android::base::WriteFully(fd, split_src_images[i].PseudoSource().DataForPatch(),
+                                     split_src_images[i].PseudoSource().DataLengthForPatch())) {
+        PLOG(ERROR) << "Failed to write split source data into " << src_name;
+        return false;
+      }
+
+      std::string patch_name = android::base::StringPrintf("%s/patch-%zu", debug_dir.c_str(), i);
+      fd.reset(open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
+
+      if (fd == -1) {
+        PLOG(ERROR) << "Failed to open " << patch_name;
+        return false;
+      }
+      if (!PatchChunk::WritePatchDataToFd(patch_chunks, fd)) {
+        return false;
+      }
+    }
+  }
+
+  // Store the split in the following format:
+  // Line 0:   imgdiff version#
+  // Line 1:   number of pieces
+  // Line 2:   patch_size_1 tgt_size_1 src_range_1
+  // ...
+  // Line n+1: patch_size_n tgt_size_n src_range_n
+  std::string split_info_string = android::base::StringPrintf(
+      "%zu\n%zu\n", VERSION, split_info_list.size()) + android::base::Join(split_info_list, '\n');
+  if (!android::base::WriteStringToFile(split_info_string, split_info_file)) {
+    PLOG(ERROR) << "Failed to write split info to " << split_info_file;
+    return false;
+  }
+
+  return true;
+}
+
+bool ImageModeImage::Initialize(const std::string& filename) {
+  if (!ReadFile(filename, &file_content_)) {
+    return false;
+  }
+
+  size_t sz = file_content_.size();
   size_t pos = 0;
-
   while (pos < sz) {
     // 0x00 no header flags, 0x08 deflate compression, 0x1f8b gzip magic number
-    if (sz - pos >= 4 && get_unaligned<uint32_t>(img->data() + pos) == 0x00088b1f) {
+    if (sz - pos >= 4 && get_unaligned<uint32_t>(file_content_.data() + pos) == 0x00088b1f) {
       // 'pos' is the offset of the start of a gzip chunk.
       size_t chunk_offset = pos;
 
       // The remaining data is too small to be a gzip chunk; treat them as a normal chunk.
       if (sz - pos < GZIP_HEADER_LEN + GZIP_FOOTER_LEN) {
-        chunks->emplace_back(CHUNK_NORMAL, pos, img, sz - pos);
+        chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, sz - pos);
         break;
       }
 
       // We need three chunks for the deflated image in total, one normal chunk for the header,
       // one deflated chunk for the body, and another normal chunk for the footer.
-      chunks->emplace_back(CHUNK_NORMAL, pos, img, GZIP_HEADER_LEN);
+      chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, GZIP_HEADER_LEN);
       pos += GZIP_HEADER_LEN;
 
       // We must decompress this chunk in order to discover where it ends, and so we can update
@@ -657,13 +1275,13 @@
       strm.zfree = Z_NULL;
       strm.opaque = Z_NULL;
       strm.avail_in = sz - pos;
-      strm.next_in = img->data() + pos;
+      strm.next_in = file_content_.data() + pos;
 
       // -15 means we are decoding a 'raw' deflate stream; zlib will
       // not expect zlib headers.
       int ret = inflateInit2(&strm, -15);
       if (ret < 0) {
-        printf("failed to initialize inflate: %d\n", ret);
+        LOG(ERROR) << "Failed to initialize inflate: " << ret;
         return false;
       }
 
@@ -675,8 +1293,8 @@
         strm.next_out = uncompressed_data.data() + uncompressed_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);
+          LOG(WARNING) << "Inflate failed [" << strm.msg << "] at offset [" << chunk_offset
+                       << "]; treating as a normal chunk";
           break;
         }
         uncompressed_len = allocated - strm.avail_out;
@@ -697,25 +1315,25 @@
       // matches the size of the data we got when we actually did the decompression.
       size_t footer_index = pos + raw_data_len + GZIP_FOOTER_LEN - 4;
       if (sz - footer_index < 4) {
-        printf("Warning: invalid footer position; treating as a nomal chunk\n");
+        LOG(WARNING) << "invalid footer position; treating as a normal chunk";
         continue;
       }
-      size_t footer_size = get_unaligned<uint32_t>(img->data() + footer_index);
+      size_t footer_size = get_unaligned<uint32_t>(file_content_.data() + footer_index);
       if (footer_size != uncompressed_len) {
-        printf("Warning: footer size %zu != decompressed size %zu; treating as a nomal chunk\n",
-               footer_size, uncompressed_len);
+        LOG(WARNING) << "footer size " << footer_size << " != " << uncompressed_len
+                     << "; treating as a normal chunk";
         continue;
       }
 
-      ImageChunk body(CHUNK_DEFLATE, pos, img, raw_data_len);
+      ImageChunk body(CHUNK_DEFLATE, pos, &file_content_, raw_data_len);
       uncompressed_data.resize(uncompressed_len);
       body.SetUncompressedData(std::move(uncompressed_data));
-      chunks->push_back(body);
+      chunks_.push_back(std::move(body));
 
       pos += raw_data_len;
 
       // create a normal chunk for the footer
-      chunks->emplace_back(CHUNK_NORMAL, pos, img, GZIP_FOOTER_LEN);
+      chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, GZIP_FOOTER_LEN);
 
       pos += GZIP_FOOTER_LEN;
     } else {
@@ -726,12 +1344,12 @@
       size_t data_len = 0;
       while (data_len + pos < sz) {
         if (data_len + pos + 4 <= sz &&
-            get_unaligned<uint32_t>(img->data() + pos + data_len) == 0x00088b1f) {
+            get_unaligned<uint32_t>(file_content_.data() + pos + data_len) == 0x00088b1f) {
           break;
         }
         data_len++;
       }
-      chunks->emplace_back(CHUNK_NORMAL, pos, img, data_len);
+      chunks_.emplace_back(CHUNK_NORMAL, pos, &file_content_, data_len);
 
       pos += data_len;
     }
@@ -740,346 +1358,256 @@
   return true;
 }
 
-/*
- * Given source and target chunks, compute a bsdiff patch between them.
- * Store the result in the patch_data.
- * |bsdiff_cache| can be used to cache the suffix array if the same |src| chunk
- * is used repeatedly, pass nullptr if not needed.
- */
-static bool MakePatch(const ImageChunk* src, ImageChunk* tgt, std::vector<uint8_t>* patch_data,
-                      saidx_t** bsdiff_cache) {
-  if (tgt->ChangeChunkToRaw(0)) {
-    size_t patch_size = tgt->DataLengthForPatch();
-    patch_data->resize(patch_size);
-    std::copy(tgt->DataForPatch(), tgt->DataForPatch() + patch_size, patch_data->begin());
-    return true;
-  }
-
-#if defined(__ANDROID__)
-  char ptemp[] = "/data/local/tmp/imgdiff-patch-XXXXXX";
-#else
-  char ptemp[] = "/tmp/imgdiff-patch-XXXXXX";
-#endif
-
-  int fd = mkstemp(ptemp);
-  if (fd == -1) {
-    printf("MakePatch failed to create a temporary file: %s\n", strerror(errno));
-    return false;
-  }
-  close(fd);
-
-  int r = bsdiff::bsdiff(src->DataForPatch(), src->DataLengthForPatch(), tgt->DataForPatch(),
-                         tgt->DataLengthForPatch(), ptemp, bsdiff_cache);
-  if (r != 0) {
-    printf("bsdiff() failed: %d\n", r);
+bool ImageModeImage::SetBonusData(const std::vector<uint8_t>& bonus_data) {
+  CHECK(is_source_);
+  if (chunks_.size() < 2 || !chunks_[1].SetBonusData(bonus_data)) {
+    LOG(ERROR) << "Failed to set bonus data";
+    DumpChunks();
     return false;
   }
 
-  android::base::unique_fd patch_fd(open(ptemp, O_RDONLY));
-  if (patch_fd == -1) {
-    printf("failed to open %s: %s\n", ptemp, strerror(errno));
+  LOG(INFO) << "  using " << bonus_data.size() << " bytes of bonus data";
+  return true;
+}
+
+// In Image Mode, verify that the source and target images have the same chunk structure (ie, the
+// same sequence of deflate and normal chunks).
+bool ImageModeImage::CheckAndProcessChunks(ImageModeImage* tgt_image, ImageModeImage* src_image) {
+  // In image mode, merge the gzip header and footer in with any adjacent normal chunks.
+  tgt_image->MergeAdjacentNormalChunks();
+  src_image->MergeAdjacentNormalChunks();
+
+  if (tgt_image->NumOfChunks() != src_image->NumOfChunks()) {
+    LOG(ERROR) << "Source and target don't have same number of chunks!";
+    tgt_image->DumpChunks();
+    src_image->DumpChunks();
     return false;
   }
-  struct stat st;
-  if (fstat(patch_fd, &st) != 0) {
-    printf("failed to stat patch file %s: %s\n", ptemp, strerror(errno));
-    return false;
+  for (size_t i = 0; i < tgt_image->NumOfChunks(); ++i) {
+    if ((*tgt_image)[i].GetType() != (*src_image)[i].GetType()) {
+      LOG(ERROR) << "Source and target don't have same chunk structure! (chunk " << i << ")";
+      tgt_image->DumpChunks();
+      src_image->DumpChunks();
+      return false;
+    }
   }
 
-  size_t sz = static_cast<size_t>(st.st_size);
-  // Change the chunk type to raw if the patch takes less space that way.
-  if (tgt->ChangeChunkToRaw(sz)) {
-    unlink(ptemp);
-    size_t patch_size = tgt->DataLengthForPatch();
-    patch_data->resize(patch_size);
-    std::copy(tgt->DataForPatch(), tgt->DataForPatch() + patch_size, patch_data->begin());
-    return true;
-  }
-  patch_data->resize(sz);
-  if (!android::base::ReadFully(patch_fd, patch_data->data(), sz)) {
-    printf("failed to read \"%s\" %s\n", ptemp, strerror(errno));
-    return false;
+  for (size_t i = 0; i < tgt_image->NumOfChunks(); ++i) {
+    auto& tgt_chunk = (*tgt_image)[i];
+    auto& src_chunk = (*src_image)[i];
+    if (tgt_chunk.GetType() != CHUNK_DEFLATE) {
+      continue;
+    }
+
+    // If two deflate chunks are identical treat them as normal chunks.
+    if (tgt_chunk == src_chunk) {
+      tgt_chunk.ChangeDeflateChunkToNormal();
+      src_chunk.ChangeDeflateChunkToNormal();
+    } else if (!tgt_chunk.ReconstructDeflateChunk()) {
+      // We cannot recompress the data and get exactly the same bits as are in the input target
+      // image, fall back to normal
+      LOG(WARNING) << "Failed to reconstruct target deflate chunk " << i << " ["
+                   << tgt_chunk.GetEntryName() << "]; treating as normal";
+      tgt_chunk.ChangeDeflateChunkToNormal();
+      src_chunk.ChangeDeflateChunkToNormal();
+    }
   }
 
-  unlink(ptemp);
-  tgt->SetSourceInfo(*src);
+  // For images, we need to maintain the parallel structure of the chunk lists, so do the merging
+  // in both the source and target lists.
+  tgt_image->MergeAdjacentNormalChunks();
+  src_image->MergeAdjacentNormalChunks();
+  if (tgt_image->NumOfChunks() != src_image->NumOfChunks()) {
+    // This shouldn't happen.
+    LOG(ERROR) << "Merging normal chunks went awry";
+    return false;
+  }
 
   return true;
 }
 
-/*
- * 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.)
- */
-static void MergeAdjacentNormalChunks(std::vector<ImageChunk>* chunks) {
-  size_t merged_last = 0, cur = 0;
-  while (cur < chunks->size()) {
-    // Look for normal chunks adjacent to the current one. If such chunk exists, extend the
-    // length of the current normal chunk.
-    size_t to_check = cur + 1;
-    while (to_check < chunks->size() && chunks->at(cur).IsAdjacentNormal(chunks->at(to_check))) {
-      chunks->at(cur).MergeAdjacentNormal(chunks->at(to_check));
-      to_check++;
+// In image mode, generate patches against the given source chunks and bonus_data; write the
+// result to |patch_name|.
+bool ImageModeImage::GeneratePatches(const ImageModeImage& tgt_image,
+                                     const ImageModeImage& src_image,
+                                     const std::string& patch_name) {
+  LOG(INFO) << "Constructing patches for " << tgt_image.NumOfChunks() << " chunks...";
+  std::vector<PatchChunk> patch_chunks;
+  patch_chunks.reserve(tgt_image.NumOfChunks());
+
+  for (size_t i = 0; i < tgt_image.NumOfChunks(); i++) {
+    const auto& tgt_chunk = tgt_image[i];
+    const auto& src_chunk = src_image[i];
+
+    if (PatchChunk::RawDataIsSmaller(tgt_chunk, 0)) {
+      patch_chunks.emplace_back(tgt_chunk);
+      continue;
     }
 
-    if (merged_last != cur) {
-      chunks->at(merged_last) = std::move(chunks->at(cur));
+    std::vector<uint8_t> patch_data;
+    if (!ImageChunk::MakePatch(tgt_chunk, src_chunk, &patch_data, nullptr)) {
+      LOG(ERROR) << "Failed to generate patch for target chunk " << i;
+      return false;
     }
-    merged_last++;
-    cur = to_check;
-  }
-  if (merged_last < chunks->size()) {
-    chunks->erase(chunks->begin() + merged_last, chunks->end());
-  }
-}
+    LOG(INFO) << "patch " << i << " is " << patch_data.size() << " bytes (of "
+              << tgt_chunk.GetRawDataLength() << ")";
 
-static ImageChunk* FindChunkByName(const std::string& name, std::vector<ImageChunk>& chunks) {
-  for (size_t i = 0; i < chunks.size(); ++i) {
-    if (chunks[i].GetType() == CHUNK_DEFLATE && chunks[i].GetEntryName() == name) {
-      return &chunks[i];
+    if (PatchChunk::RawDataIsSmaller(tgt_chunk, patch_data.size())) {
+      patch_chunks.emplace_back(tgt_chunk);
+    } else {
+      patch_chunks.emplace_back(tgt_chunk, src_chunk, std::move(patch_data));
     }
   }
-  return nullptr;
-}
 
-static void DumpChunks(const std::vector<ImageChunk>& chunks) {
-  for (size_t i = 0; i < chunks.size(); ++i) {
-    printf("chunk %zu: ", i);
-    chunks[i].Dump();
+  CHECK_EQ(tgt_image.NumOfChunks(), patch_chunks.size());
+
+  android::base::unique_fd patch_fd(
+      open(patch_name.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
+  if (patch_fd == -1) {
+    PLOG(ERROR) << "Failed to open " << patch_name;
+    return false;
   }
+
+  return PatchChunk::WritePatchDataToFd(patch_chunks, patch_fd);
 }
 
 int imgdiff(int argc, const char** argv) {
+  bool verbose = false;
   bool zip_mode = false;
-
-  if (argc >= 2 && strcmp(argv[1], "-z") == 0) {
-    zip_mode = true;
-    --argc;
-    ++argv;
-  }
-
   std::vector<uint8_t> bonus_data;
-  if (argc >= 3 && strcmp(argv[1], "-b") == 0) {
-    android::base::unique_fd fd(open(argv[2], O_RDONLY));
-    if (fd == -1) {
-      printf("failed to open bonus file %s: %s\n", argv[2], strerror(errno));
-      return 1;
-    }
-    struct stat st;
-    if (fstat(fd, &st) != 0) {
-      printf("failed to stat bonus file %s: %s\n", argv[2], strerror(errno));
-      return 1;
-    }
+  size_t blocks_limit = 0;
+  std::string split_info_file;
+  std::string debug_dir;
 
-    size_t bonus_size = st.st_size;
-    bonus_data.resize(bonus_size);
-    if (!android::base::ReadFully(fd, bonus_data.data(), bonus_size)) {
-      printf("failed to read bonus file %s: %s\n", argv[2], strerror(errno));
-      return 1;
-    }
+  int opt;
+  int option_index;
+  optind = 0;  // Reset the getopt state so that we can call it multiple times for test.
 
-    argc -= 2;
-    argv += 2;
+  while ((opt = getopt_long(argc, const_cast<char**>(argv), "zb:v", OPTIONS, &option_index)) !=
+         -1) {
+    switch (opt) {
+      case 'z':
+        zip_mode = true;
+        break;
+      case 'b': {
+        android::base::unique_fd fd(open(optarg, O_RDONLY));
+        if (fd == -1) {
+          PLOG(ERROR) << "Failed to open bonus file " << optarg;
+          return 1;
+        }
+        struct stat st;
+        if (fstat(fd, &st) != 0) {
+          PLOG(ERROR) << "Failed to stat bonus file " << optarg;
+          return 1;
+        }
+
+        size_t bonus_size = st.st_size;
+        bonus_data.resize(bonus_size);
+        if (!android::base::ReadFully(fd, bonus_data.data(), bonus_size)) {
+          PLOG(ERROR) << "Failed to read bonus file " << optarg;
+          return 1;
+        }
+        break;
+      }
+      case 'v':
+        verbose = true;
+        break;
+      case 0: {
+        std::string name = OPTIONS[option_index].name;
+        if (name == "block-limit" && !android::base::ParseUint(optarg, &blocks_limit)) {
+          LOG(ERROR) << "Failed to parse size blocks_limit: " << optarg;
+          return 1;
+        } else if (name == "split-info") {
+          split_info_file = optarg;
+        } else if (name == "debug-dir") {
+          debug_dir = optarg;
+        }
+        break;
+      }
+      default:
+        LOG(ERROR) << "unexpected opt: " << static_cast<char>(opt);
+        return 2;
+    }
   }
 
-  if (argc != 4) {
-    printf("usage: %s [-z] [-b <bonus-file>] <src-img> <tgt-img> <patch-file>\n",
-            argv[0]);
+  if (!verbose) {
+    android::base::SetMinimumLogSeverity(android::base::WARNING);
+  }
+
+  if (argc - optind != 3) {
+    LOG(ERROR) << "usage: " << argv[0] << " [options] <src-img> <tgt-img> <patch-file>";
+    LOG(ERROR)
+        << "  -z <zip-mode>,    Generate patches in zip mode, src and tgt should be zip files.\n"
+           "  -b <bonus-file>,  Bonus file in addition to src, image mode only.\n"
+           "  --block-limit,    For large zips, split the src and tgt based on the block limit;\n"
+           "                    and generate patches between each pair of pieces. Concatenate "
+           "these\n"
+           "                    patches together and output them into <patch-file>.\n"
+           "  --split-info,     Output the split information (patch_size, tgt_size, src_ranges);\n"
+           "                    zip mode with block-limit only.\n"
+           "  --debug-dir,      Debug directory to put the split srcs and patches, zip mode only.\n"
+           "  -v, --verbose,    Enable verbose logging.";
     return 2;
   }
 
-  std::vector<ImageChunk> src_chunks;
-  std::vector<ImageChunk> tgt_chunks;
-  std::vector<uint8_t> src_file;
-  std::vector<uint8_t> tgt_file;
-
   if (zip_mode) {
-    if (!ReadZip(argv[1], &src_chunks, &src_file, true)) {
-      printf("failed to break apart source zip file\n");
+    ZipModeImage src_image(true, blocks_limit * BLOCK_SIZE);
+    ZipModeImage tgt_image(false, blocks_limit * BLOCK_SIZE);
+
+    if (!src_image.Initialize(argv[optind])) {
       return 1;
     }
-    if (!ReadZip(argv[2], &tgt_chunks, &tgt_file, false)) {
-      printf("failed to break apart target zip file\n");
+    if (!tgt_image.Initialize(argv[optind + 1])) {
+      return 1;
+    }
+
+    if (!ZipModeImage::CheckAndProcessChunks(&tgt_image, &src_image)) {
+      return 1;
+    }
+
+    // Compute bsdiff patches for each chunk's data (the uncompressed data, in the case of
+    // deflate chunks).
+    if (blocks_limit > 0) {
+      if (split_info_file.empty()) {
+        LOG(ERROR) << "split-info path cannot be empty when generating patches with a block-limit";
+        return 1;
+      }
+
+      std::vector<ZipModeImage> split_tgt_images;
+      std::vector<ZipModeImage> split_src_images;
+      std::vector<SortedRangeSet> split_src_ranges;
+      ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images,
+                                               &split_src_images, &split_src_ranges);
+
+      if (!ZipModeImage::GeneratePatches(split_tgt_images, split_src_images, split_src_ranges,
+                                         argv[optind + 2], split_info_file, debug_dir)) {
+        return 1;
+      }
+
+    } else if (!ZipModeImage::GeneratePatches(tgt_image, src_image, argv[optind + 2])) {
       return 1;
     }
   } else {
-    if (!ReadImage(argv[1], &src_chunks, &src_file)) {
-      printf("failed to break apart source image\n");
+    ImageModeImage src_image(true);
+    ImageModeImage tgt_image(false);
+
+    if (!src_image.Initialize(argv[optind])) {
       return 1;
     }
-    if (!ReadImage(argv[2], &tgt_chunks, &tgt_file)) {
-      printf("failed to break apart target image\n");
+    if (!tgt_image.Initialize(argv[optind + 1])) {
       return 1;
     }
 
-    // Verify that the source and target images have the same chunk
-    // structure (ie, the same sequence of deflate and normal chunks).
-
-    // Merge the gzip header and footer in with any adjacent normal chunks.
-    MergeAdjacentNormalChunks(&tgt_chunks);
-    MergeAdjacentNormalChunks(&src_chunks);
-
-    if (src_chunks.size() != tgt_chunks.size()) {
-      printf("source and target don't have same number of chunks!\n");
-      printf("source chunks:\n");
-      DumpChunks(src_chunks);
-      printf("target chunks:\n");
-      DumpChunks(tgt_chunks);
+    if (!ImageModeImage::CheckAndProcessChunks(&tgt_image, &src_image)) {
       return 1;
     }
-    for (size_t i = 0; i < src_chunks.size(); ++i) {
-      if (src_chunks[i].GetType() != tgt_chunks[i].GetType()) {
-        printf("source and target don't have same chunk structure! (chunk %zu)\n", i);
-        printf("source chunks:\n");
-        DumpChunks(src_chunks);
-        printf("target chunks:\n");
-        DumpChunks(tgt_chunks);
-        return 1;
-      }
-    }
-  }
 
-  for (size_t i = 0; i < tgt_chunks.size(); ++i) {
-    if (tgt_chunks[i].GetType() == 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 (!tgt_chunks[i].ReconstructDeflateChunk()) {
-        printf("failed to reconstruct target deflate chunk %zu [%s]; treating as normal\n", i,
-               tgt_chunks[i].GetEntryName().c_str());
-        tgt_chunks[i].ChangeDeflateChunkToNormal();
-        if (zip_mode) {
-          ImageChunk* src = FindChunkByName(tgt_chunks[i].GetEntryName(), src_chunks);
-          if (src != nullptr) {
-            src->ChangeDeflateChunkToNormal();
-          }
-        } else {
-          src_chunks[i].ChangeDeflateChunkToNormal();
-        }
-        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].GetEntryName(), src_chunks);
-      } else {
-        src = &src_chunks[i];
-      }
-
-      if (src == nullptr) {
-        tgt_chunks[i].ChangeDeflateChunkToNormal();
-      } else if (tgt_chunks[i] == *src) {
-        tgt_chunks[i].ChangeDeflateChunkToNormal();
-        src->ChangeDeflateChunkToNormal();
-      }
-    }
-  }
-
-  // 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);
-
-  } 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);
-    MergeAdjacentNormalChunks(&src_chunks);
-    if (src_chunks.size() != tgt_chunks.size()) {
-      // This shouldn't happen.
-      printf("merging normal chunks went awry\n");
+    if (!bonus_data.empty() && !src_image.SetBonusData(bonus_data)) {
       return 1;
     }
-  }
 
-  // Compute bsdiff patches for each chunk's data (the uncompressed
-  // data, in the case of deflate chunks).
-
-  DumpChunks(src_chunks);
-
-  printf("Construct patches for %zu chunks...\n", tgt_chunks.size());
-  std::vector<std::vector<uint8_t>> patch_data(tgt_chunks.size());
-  saidx_t* bsdiff_cache = nullptr;
-  for (size_t i = 0; i < tgt_chunks.size(); ++i) {
-    if (zip_mode) {
-      ImageChunk* src;
-      if (tgt_chunks[i].GetType() == CHUNK_DEFLATE &&
-          (src = FindChunkByName(tgt_chunks[i].GetEntryName(), src_chunks))) {
-        if (!MakePatch(src, &tgt_chunks[i], &patch_data[i], nullptr)) {
-          printf("Failed to generate patch for target chunk %zu: ", i);
-          return 1;
-        }
-      } else {
-        if (!MakePatch(&src_chunks[0], &tgt_chunks[i], &patch_data[i], &bsdiff_cache)) {
-          printf("Failed to generate patch for target chunk %zu: ", i);
-          return 1;
-        }
-      }
-    } else {
-      if (i == 1 && !bonus_data.empty()) {
-        printf("  using %zu bytes of bonus data for chunk %zu\n", bonus_data.size(), i);
-        src_chunks[i].SetBonusData(bonus_data);
-      }
-
-      if (!MakePatch(&src_chunks[i], &tgt_chunks[i], &patch_data[i], nullptr)) {
-        printf("Failed to generate patch for target chunk %zu: ", i);
-        return 1;
-      }
-    }
-    printf("patch %3zu is %zu bytes (of %zu)\n", i, patch_data[i].size(),
-           src_chunks[i].GetRawDataLength());
-  }
-
-  if (bsdiff_cache != nullptr) {
-    free(bsdiff_cache);
-  }
-
-  // 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 (size_t i = 0; i < tgt_chunks.size(); ++i) {
-    total_header_size += tgt_chunks[i].GetHeaderSize(patch_data[i].size());
-  }
-
-  size_t offset = total_header_size;
-
-  android::base::unique_fd patch_fd(open(argv[3], O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR));
-  if (patch_fd == -1) {
-    printf("failed to open \"%s\": %s\n", argv[3], strerror(errno));
-    return 1;
-  }
-
-  // Write out the headers.
-  if (!android::base::WriteStringToFd("IMGDIFF2", patch_fd)) {
-    printf("failed to write \"IMGDIFF2\" to \"%s\": %s\n", argv[3], strerror(errno));
-    return 1;
-  }
-  Write4(patch_fd, static_cast<int32_t>(tgt_chunks.size()));
-  for (size_t i = 0; i < tgt_chunks.size(); ++i) {
-    printf("chunk %zu: ", i);
-    offset = tgt_chunks[i].WriteHeaderToFd(patch_fd, patch_data[i], offset);
-  }
-
-  // Append each chunk's bsdiff patch, in order.
-  for (size_t i = 0; i < tgt_chunks.size(); ++i) {
-    if (tgt_chunks[i].GetType() != CHUNK_RAW) {
-      if (!android::base::WriteFully(patch_fd, patch_data[i].data(), patch_data[i].size())) {
-        CHECK(false) << "failed to write " << patch_data[i].size() << " bytes patch for chunk "
-                     << i;
-      }
+    if (!ImageModeImage::GeneratePatches(tgt_image, src_image, argv[optind + 2])) {
+      return 1;
     }
   }
 
diff --git a/applypatch/imgpatch.cpp b/applypatch/imgpatch.cpp
index df75f98..3682d61 100644
--- a/applypatch/imgpatch.cpp
+++ b/applypatch/imgpatch.cpp
@@ -37,6 +37,8 @@
 #include <openssl/sha.h>
 #include <zlib.h>
 
+#include "edify/expr.h"
+
 static inline int64_t Read8(const void *address) {
   return android::base::get_unaligned<int64_t>(address);
 }
@@ -48,7 +50,7 @@
 // This function is a wrapper of ApplyBSDiffPatch(). It has a custom sink function to deflate the
 // patched data and stream the deflated data to output.
 static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_len,
-                                            const Value* patch, size_t patch_offset,
+                                            const Value& patch, size_t patch_offset,
                                             const char* deflate_header, SinkFn sink, SHA_CTX* ctx) {
   size_t expected_target_length = static_cast<size_t>(Read8(deflate_header + 32));
   int level = Read4(deflate_header + 40);
@@ -57,13 +59,13 @@
   int mem_level = Read4(deflate_header + 52);
   int strategy = Read4(deflate_header + 56);
 
-  std::unique_ptr<z_stream, decltype(&deflateEnd)> strm(new z_stream(), deflateEnd);
-  strm->zalloc = Z_NULL;
-  strm->zfree = Z_NULL;
-  strm->opaque = Z_NULL;
-  strm->avail_in = 0;
-  strm->next_in = nullptr;
-  int ret = deflateInit2(strm.get(), level, method, window_bits, mem_level, strategy);
+  z_stream strm;
+  strm.zalloc = Z_NULL;
+  strm.zfree = Z_NULL;
+  strm.opaque = Z_NULL;
+  strm.avail_in = 0;
+  strm.next_in = nullptr;
+  int ret = deflateInit2(&strm, level, method, window_bits, mem_level, strategy);
   if (ret != Z_OK) {
     LOG(ERROR) << "Failed to init uncompressed data deflation: " << ret;
     return false;
@@ -74,18 +76,19 @@
   size_t actual_target_length = 0;
   size_t total_written = 0;
   static constexpr size_t buffer_size = 32768;
-  auto compression_sink = [&](const uint8_t* data, size_t len) -> size_t {
+  auto compression_sink = [&strm, &actual_target_length, &expected_target_length, &total_written,
+                           &ret, &ctx, &sink](const uint8_t* data, size_t len) -> size_t {
     // The input patch length for an update never exceeds INT_MAX.
-    strm->avail_in = len;
-    strm->next_in = data;
+    strm.avail_in = len;
+    strm.next_in = data;
     do {
       std::vector<uint8_t> buffer(buffer_size);
-      strm->avail_out = buffer_size;
-      strm->next_out = buffer.data();
+      strm.avail_out = buffer_size;
+      strm.next_out = buffer.data();
       if (actual_target_length + len < expected_target_length) {
-        ret = deflate(strm.get(), Z_NO_FLUSH);
+        ret = deflate(&strm, Z_NO_FLUSH);
       } else {
-        ret = deflate(strm.get(), Z_FINISH);
+        ret = deflate(&strm, Z_FINISH);
       }
       if (ret != Z_OK && ret != Z_STREAM_END) {
         LOG(ERROR) << "Failed to deflate stream: " << ret;
@@ -93,20 +96,24 @@
         return 0;
       }
 
-      size_t have = buffer_size - strm->avail_out;
+      size_t have = buffer_size - strm.avail_out;
       total_written += have;
       if (sink(buffer.data(), have) != have) {
         LOG(ERROR) << "Failed to write " << have << " compressed bytes to output.";
         return 0;
       }
       if (ctx) SHA1_Update(ctx, buffer.data(), have);
-    } while ((strm->avail_in != 0 || strm->avail_out == 0) && ret != Z_STREAM_END);
+    } while ((strm.avail_in != 0 || strm.avail_out == 0) && ret != Z_STREAM_END);
 
     actual_target_length += len;
     return len;
   };
 
-  if (ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink, nullptr) != 0) {
+  int bspatch_result =
+      ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink, nullptr);
+  deflateEnd(&strm);
+
+  if (bspatch_result != 0) {
     return false;
   }
 
@@ -128,48 +135,39 @@
 int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const unsigned char* patch_data,
                     size_t patch_size, SinkFn sink) {
   Value patch(VAL_BLOB, std::string(reinterpret_cast<const char*>(patch_data), patch_size));
-
-  return ApplyImagePatch(old_data, old_size, &patch, sink, nullptr, nullptr);
+  return ApplyImagePatch(old_data, old_size, patch, sink, 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, size_t old_size, const Value* patch, SinkFn sink,
+int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink,
                     SHA_CTX* ctx, const Value* bonus_data) {
-  if (patch->data.size() < 12) {
+  if (patch.data.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.)
-  size_t pos = 12;
-  const char* header = &patch->data[0];
-  if (memcmp(header, "IMGDIFF2", 8) != 0) {
+  // IMGDIFF2 uses CHUNK_NORMAL, CHUNK_DEFLATE, and CHUNK_RAW. (IMGDIFF1, which is no longer
+  // supported, used CHUNK_NORMAL and CHUNK_GZIP.)
+  const char* const patch_header = patch.data.data();
+  if (memcmp(patch_header, "IMGDIFF2", 8) != 0) {
     printf("corrupt patch file header (magic number)\n");
     return -1;
   }
 
-  int num_chunks = Read4(header + 8);
-
+  int num_chunks = Read4(patch_header + 8);
+  size_t pos = 12;
   for (int i = 0; i < num_chunks; ++i) {
     // each chunk's header record starts with 4 bytes.
-    if (pos + 4 > patch->data.size()) {
+    if (pos + 4 > patch.data.size()) {
       printf("failed to read chunk %d record\n", i);
       return -1;
     }
-    int type = Read4(&patch->data[pos]);
+    int type = Read4(patch_header + pos);
     pos += 4;
 
     if (type == CHUNK_NORMAL) {
-      const char* normal_header = &patch->data[pos];
+      const char* normal_header = patch_header + pos;
       pos += 24;
-      if (pos > patch->data.size()) {
+      if (pos > patch.data.size()) {
         printf("failed to read chunk %d normal header data\n", i);
         return -1;
       }
@@ -187,30 +185,32 @@
         return -1;
       }
     } else if (type == CHUNK_RAW) {
-      const char* raw_header = &patch->data[pos];
+      const char* raw_header = patch_header + pos;
       pos += 4;
-      if (pos > patch->data.size()) {
+      if (pos > patch.data.size()) {
         printf("failed to read chunk %d raw header data\n", i);
         return -1;
       }
 
       size_t data_len = static_cast<size_t>(Read4(raw_header));
 
-      if (pos + data_len > patch->data.size()) {
+      if (pos + data_len > patch.data.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(reinterpret_cast<const unsigned char*>(&patch->data[pos]), data_len) != data_len) {
+      if (ctx) {
+        SHA1_Update(ctx, patch_header + pos, data_len);
+      }
+      if (sink(reinterpret_cast<const unsigned char*>(patch_header + pos), data_len) != 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.
-      const char* deflate_header = &patch->data[pos];
+      const char* deflate_header = patch_header + pos;
       pos += 60;
-      if (pos > patch->data.size()) {
+      if (pos > patch.data.size()) {
         printf("failed to read chunk %d deflate header data\n", i);
         return -1;
       }
diff --git a/applypatch/include/applypatch/applypatch.h b/applypatch/include/applypatch/applypatch.h
index 581360e..912ead1 100644
--- a/applypatch/include/applypatch/applypatch.h
+++ b/applypatch/include/applypatch/applypatch.h
@@ -18,7 +18,6 @@
 #define _APPLYPATCH_H
 
 #include <stdint.h>
-#include <sys/stat.h>
 
 #include <functional>
 #include <memory>
@@ -27,24 +26,18 @@
 
 #include <openssl/sha.h>
 
-#include "edify/expr.h"
+// Forward declaration to avoid including "edify/expr.h" in the header.
+struct Value;
 
 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"
-
 using SinkFn = std::function<size_t(const unsigned char*, size_t)>;
 
 // applypatch.cpp
+
 int ShowLicenses();
 size_t FreeSpaceForFile(const char* filename);
 int CacheSizeCheck(size_t bytes);
@@ -66,15 +59,25 @@
 int SaveFileContents(const char* filename, const FileContents* file);
 
 // bspatch.cpp
+
 void ShowBSDiffLicense();
-int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value* patch,
+
+// Applies the bsdiff-patch given in 'patch' (from offset 'patch_offset' to the end) to the source
+// data given by (old_data, old_size). Writes the patched output through the given 'sink', and
+// updates the SHA-1 context with the output data. Returns 0 on success.
+int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch,
                      size_t patch_offset, SinkFn sink, SHA_CTX* ctx);
 
 // imgpatch.cpp
-int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value* patch, SinkFn sink,
+
+// Applies the imgdiff-patch given in 'patch' to the source data given by (old_data, old_size), with
+// the optional bonus data. Writes the patched output through the given 'sink', and updates the
+// SHA-1 context with the output data. Returns 0 on success.
+int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink,
                     SHA_CTX* ctx, const Value* bonus_data);
 
 // freecache.cpp
+
 int MakeFreeSpaceOnCache(size_t bytes_needed);
 
 #endif
diff --git a/applypatch/include/applypatch/imgdiff_image.h b/applypatch/include/applypatch/imgdiff_image.h
new file mode 100644
index 0000000..0848072
--- /dev/null
+++ b/applypatch/include/applypatch/imgdiff_image.h
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2017 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_IMGDIFF_IMAGE_H
+#define _APPLYPATCH_IMGDIFF_IMAGE_H
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+#include <string>
+#include <vector>
+
+#include <bsdiff/bsdiff.h>
+#include <ziparchive/zip_archive.h>
+#include <zlib.h>
+
+#include "imgdiff.h"
+#include "otautil/rangeset.h"
+
+class ImageChunk {
+ public:
+  static constexpr auto WINDOWBITS = -15;  // 32kb window; negative to indicate a raw stream.
+  static constexpr auto MEMLEVEL = 8;      // the default value.
+  static constexpr auto METHOD = Z_DEFLATED;
+  static constexpr auto STRATEGY = Z_DEFAULT_STRATEGY;
+
+  ImageChunk(int type, size_t start, const std::vector<uint8_t>* file_content, size_t raw_data_len,
+             std::string entry_name = {});
+
+  int GetType() const {
+    return type_;
+  }
+  size_t GetRawDataLength() const {
+    return raw_data_len_;
+  }
+  const std::string& GetEntryName() const {
+    return entry_name_;
+  }
+  size_t GetStartOffset() const {
+    return start_;
+  }
+  int GetCompressLevel() const {
+    return compress_level_;
+  }
+
+  // CHUNK_DEFLATE will return the uncompressed data for diff, while other types will simply return
+  // the raw data.
+  const uint8_t* DataForPatch() const;
+  size_t DataLengthForPatch() const;
+
+  void Dump(size_t index) const;
+
+  void SetUncompressedData(std::vector<uint8_t> data);
+  bool SetBonusData(const std::vector<uint8_t>& bonus_data);
+
+  bool operator==(const ImageChunk& other) const;
+  bool operator!=(const ImageChunk& other) const {
+    return !(*this == other);
+  }
+
+  /*
+   * 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();
+
+  /*
+   * 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.
+   */
+  bool ReconstructDeflateChunk();
+  bool IsAdjacentNormal(const ImageChunk& other) const;
+  void MergeAdjacentNormal(const ImageChunk& other);
+
+  /*
+   * Compute a bsdiff patch between |src| and |tgt|; Store the result in the patch_data.
+   * |bsdiff_cache| can be used to cache the suffix array if the same |src| chunk is used
+   * repeatedly, pass nullptr if not needed.
+   */
+  static bool MakePatch(const ImageChunk& tgt, const ImageChunk& src,
+                        std::vector<uint8_t>* patch_data,
+                        bsdiff::SuffixArrayIndexInterface** bsdiff_cache);
+
+ private:
+  const uint8_t* GetRawData() const;
+  bool TryReconstruction(int level);
+
+  int type_;                                    // CHUNK_NORMAL, CHUNK_DEFLATE, CHUNK_RAW
+  size_t start_;                                // offset of chunk in the original input file
+  const std::vector<uint8_t>* input_file_ptr_;  // ptr to the full content of original input file
+  size_t raw_data_len_;
+
+  // deflate encoder parameters
+  int compress_level_;
+
+  // --- for CHUNK_DEFLATE chunks only: ---
+  std::vector<uint8_t> uncompressed_data_;
+  std::string entry_name_;  // used for zip entries
+};
+
+// PatchChunk stores the patch data between a source chunk and a target chunk. It also keeps track
+// of the metadata of src&tgt chunks (e.g. offset, raw data length, uncompressed data length).
+class PatchChunk {
+ public:
+  PatchChunk(const ImageChunk& tgt, const ImageChunk& src, std::vector<uint8_t> data);
+
+  // Construct a CHUNK_RAW patch from the target data directly.
+  explicit PatchChunk(const ImageChunk& tgt);
+
+  // Return true if raw data size is smaller than the patch size.
+  static bool RawDataIsSmaller(const ImageChunk& tgt, size_t patch_size);
+
+  // Update the source start with the new offset within the source range.
+  void UpdateSourceOffset(const SortedRangeSet& src_range);
+
+  // Return the total size (header + data) of the patch.
+  size_t PatchSize() const;
+
+  static bool WritePatchDataToFd(const std::vector<PatchChunk>& patch_chunks, int patch_fd);
+
+ private:
+  size_t GetHeaderSize() const;
+  size_t WriteHeaderToFd(int fd, size_t offset, size_t index) const;
+
+  // The patch chunk type is the same as the target chunk type. The only exception is we change
+  // the |type_| to CHUNK_RAW if target length is smaller than the patch size.
+  int type_;
+
+  size_t source_start_;
+  size_t source_len_;
+  size_t source_uncompressed_len_;
+
+  size_t target_start_;  // offset of the target chunk within the target file
+  size_t target_len_;
+  size_t target_uncompressed_len_;
+  size_t target_compress_level_;  // the deflate compression level of the target chunk.
+
+  std::vector<uint8_t> data_;  // storage for the patch data
+};
+
+// Interface for zip_mode and image_mode images. We initialize the image from an input file and
+// split the file content into a list of image chunks.
+class Image {
+ public:
+  explicit Image(bool is_source) : is_source_(is_source) {}
+
+  virtual ~Image() {}
+
+  // Create a list of image chunks from input file.
+  virtual bool Initialize(const std::string& filename) = 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();
+
+  void DumpChunks() const;
+
+  // Non const iterators to access the stored ImageChunks.
+  std::vector<ImageChunk>::iterator begin() {
+    return chunks_.begin();
+  }
+
+  std::vector<ImageChunk>::iterator end() {
+    return chunks_.end();
+  }
+
+  std::vector<ImageChunk>::const_iterator cbegin() const {
+    return chunks_.cbegin();
+  }
+
+  std::vector<ImageChunk>::const_iterator cend() const {
+    return chunks_.cend();
+  }
+
+  ImageChunk& operator[](size_t i);
+  const ImageChunk& operator[](size_t i) const;
+
+  size_t NumOfChunks() const {
+    return chunks_.size();
+  }
+
+ protected:
+  bool ReadFile(const std::string& filename, std::vector<uint8_t>* file_content);
+
+  bool is_source_;                     // True if it's for source chunks.
+  std::vector<ImageChunk> chunks_;     // Internal storage of ImageChunk.
+  std::vector<uint8_t> file_content_;  // Store the whole input file in memory.
+};
+
+class ZipModeImage : public Image {
+ public:
+  explicit ZipModeImage(bool is_source, size_t limit = 0) : Image(is_source), limit_(limit) {}
+
+  bool Initialize(const std::string& filename) override;
+
+  // Initialize a dummy ZipModeImage from an existing ImageChunk vector. For src img pieces, we
+  // reconstruct a new file_content based on the source ranges; but it's not needed for the tgt img
+  // pieces; because for each chunk both the data and their offset within the file are unchanged.
+  void Initialize(const std::vector<ImageChunk>& chunks, const std::vector<uint8_t>& file_content) {
+    chunks_ = chunks;
+    file_content_ = file_content;
+  }
+
+  // The pesudo source chunk for bsdiff if there's no match for the given target chunk. It's in
+  // fact the whole source file.
+  ImageChunk PseudoSource() const;
+
+  // Find the matching deflate source chunk by entry name. Search for normal chunks also if
+  // |find_normal| is true.
+  ImageChunk* FindChunkByName(const std::string& name, bool find_normal = false);
+
+  const ImageChunk* FindChunkByName(const std::string& name, bool find_normal = false) const;
+
+  // Verify that we can reconstruct the deflate chunks; also change the type to CHUNK_NORMAL if
+  // src and tgt are identical.
+  static bool CheckAndProcessChunks(ZipModeImage* tgt_image, ZipModeImage* src_image);
+
+  // Compute the patch between tgt & src images, and write the data into |patch_name|.
+  static bool GeneratePatches(const ZipModeImage& tgt_image, const ZipModeImage& src_image,
+                              const std::string& patch_name);
+
+  // Compute the patch based on the lists of split src and tgt images. Generate patches for each
+  // pair of split pieces and write the data to |patch_name|. If |debug_dir| is specified, write
+  // each split src data and patch data into that directory.
+  static bool GeneratePatches(const std::vector<ZipModeImage>& split_tgt_images,
+                              const std::vector<ZipModeImage>& split_src_images,
+                              const std::vector<SortedRangeSet>& split_src_ranges,
+                              const std::string& patch_name, const std::string& split_info_file,
+                              const std::string& debug_dir);
+
+  // Split the tgt chunks and src chunks based on the size limit.
+  static bool SplitZipModeImageWithLimit(const ZipModeImage& tgt_image,
+                                         const ZipModeImage& src_image,
+                                         std::vector<ZipModeImage>* split_tgt_images,
+                                         std::vector<ZipModeImage>* split_src_images,
+                                         std::vector<SortedRangeSet>* split_src_ranges);
+
+ private:
+  // Initialize image chunks based on the zip entries.
+  bool InitializeChunks(const std::string& filename, ZipArchiveHandle handle);
+  // Add the a zip entry to the list.
+  bool AddZipEntryToChunks(ZipArchiveHandle handle, const std::string& entry_name, ZipEntry* entry);
+  // Return the real size of the zip file. (omit the trailing zeros that used for alignment)
+  bool GetZipFileSize(size_t* input_file_size);
+
+  static void ValidateSplitImages(const std::vector<ZipModeImage>& split_tgt_images,
+                                  const std::vector<ZipModeImage>& split_src_images,
+                                  std::vector<SortedRangeSet>& split_src_ranges,
+                                  size_t total_tgt_size);
+  // Construct the dummy split images based on the chunks info and source ranges; and move them into
+  // the given vectors. Return true if we add a new split image into |split_tgt_images|, and
+  // false otherwise.
+  static bool AddSplitImageFromChunkList(const ZipModeImage& tgt_image,
+                                         const ZipModeImage& src_image,
+                                         const SortedRangeSet& split_src_ranges,
+                                         const std::vector<ImageChunk>& split_tgt_chunks,
+                                         const std::vector<ImageChunk>& split_src_chunks,
+                                         std::vector<ZipModeImage>* split_tgt_images,
+                                         std::vector<ZipModeImage>* split_src_images);
+
+  // Function that actually iterates the tgt_chunks and makes patches.
+  static bool GeneratePatchesInternal(const ZipModeImage& tgt_image, const ZipModeImage& src_image,
+                                      std::vector<PatchChunk>* patch_chunks);
+
+  // size limit in bytes of each chunk. Also, if the length of one zip_entry exceeds the limit,
+  // we'll split that entry into several smaller chunks in advance.
+  size_t limit_;
+};
+
+class ImageModeImage : public Image {
+ public:
+  explicit ImageModeImage(bool is_source) : Image(is_source) {}
+
+  // Initialize the image chunks list by searching the magic numbers in an image file.
+  bool Initialize(const std::string& filename) override;
+
+  bool SetBonusData(const std::vector<uint8_t>& bonus_data);
+
+  // In Image Mode, verify that the source and target images have the same chunk structure (ie, the
+  // same sequence of deflate and normal chunks).
+  static bool CheckAndProcessChunks(ImageModeImage* tgt_image, ImageModeImage* src_image);
+
+  // In image mode, generate patches against the given source chunks and bonus_data; write the
+  // result to |patch_name|.
+  static bool GeneratePatches(const ImageModeImage& tgt_image, const ImageModeImage& src_image,
+                              const std::string& patch_name);
+};
+
+#endif  // _APPLYPATCH_IMGDIFF_IMAGE_H
diff --git a/applypatch/libimgpatch.pc b/applypatch/libimgpatch.pc
deleted file mode 100644
index e500293..0000000
--- a/applypatch/libimgpatch.pc
+++ /dev/null
@@ -1,6 +0,0 @@
-# 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/boot_control/Android.mk b/boot_control/Android.mk
index 27e3d97..9814d71 100644
--- a/boot_control/Android.mk
+++ b/boot_control/Android.mk
@@ -24,8 +24,7 @@
   -D_FILE_OFFSET_BITS=64 \
   -Werror \
   -Wall \
-  -Wextra \
-  -Wno-unused-parameter
+  -Wextra
 LOCAL_SHARED_LIBRARIES := liblog
 LOCAL_STATIC_LIBRARIES := libbootloader_message libfs_mgr libbase
 LOCAL_POST_INSTALL_CMD := \
diff --git a/bootloader_message/Android.bp b/bootloader_message/Android.bp
index f0d76e7..c81c67b 100644
--- a/bootloader_message/Android.bp
+++ b/bootloader_message/Android.bp
@@ -17,7 +17,10 @@
 cc_library_static {
     name: "libbootloader_message",
     srcs: ["bootloader_message.cpp"],
-    cppflags: ["-Werror"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
     static_libs: [
         "libbase",
         "libfs_mgr",
diff --git a/bootloader_message/bootloader_message.cpp b/bootloader_message/bootloader_message.cpp
index f91446b..aaeffdc 100644
--- a/bootloader_message/bootloader_message.cpp
+++ b/bootloader_message/bootloader_message.cpp
@@ -159,14 +159,8 @@
 
 bool write_bootloader_message(const std::vector<std::string>& options, std::string* err) {
   bootloader_message boot = {};
-  strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
-  strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
-  for (const auto& s : options) {
-    strlcat(boot.recovery, s.c_str(), sizeof(boot.recovery));
-    if (s.back() != '\n') {
-      strlcat(boot.recovery, "\n", sizeof(boot.recovery));
-    }
-  }
+  update_bootloader_message_in_struct(&boot, options);
+
   return write_bootloader_message(boot, err);
 }
 
@@ -175,20 +169,27 @@
   if (!read_bootloader_message(&boot, err)) {
     return false;
   }
+  update_bootloader_message_in_struct(&boot, options);
 
-  // Zero out the entire fields.
-  memset(boot.command, 0, sizeof(boot.command));
-  memset(boot.recovery, 0, sizeof(boot.recovery));
+  return write_bootloader_message(boot, err);
+}
 
-  strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
-  strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
+bool update_bootloader_message_in_struct(bootloader_message* boot,
+                                         const std::vector<std::string>& options) {
+  if (!boot) return false;
+  // Replace the command & recovery fields.
+  memset(boot->command, 0, sizeof(boot->command));
+  memset(boot->recovery, 0, sizeof(boot->recovery));
+
+  strlcpy(boot->command, "boot-recovery", sizeof(boot->command));
+  strlcpy(boot->recovery, "recovery\n", sizeof(boot->recovery));
   for (const auto& s : options) {
-    strlcat(boot.recovery, s.c_str(), sizeof(boot.recovery));
+    strlcat(boot->recovery, s.c_str(), sizeof(boot->recovery));
     if (s.back() != '\n') {
-      strlcat(boot.recovery, "\n", sizeof(boot.recovery));
+      strlcat(boot->recovery, "\n", sizeof(boot->recovery));
     }
   }
-  return write_bootloader_message(boot, err);
+  return true;
 }
 
 bool write_reboot_bootloader(std::string* err) {
diff --git a/bootloader_message/include/bootloader_message/bootloader_message.h b/bootloader_message/include/bootloader_message/bootloader_message.h
index 2ffbfc9..95c19ae 100644
--- a/bootloader_message/include/bootloader_message/bootloader_message.h
+++ b/bootloader_message/include/bootloader_message/bootloader_message.h
@@ -103,13 +103,17 @@
  * 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.
+ *
+ * The update_channel field is used to store the Omaha update channel
+ * if update_engine is compiled with Omaha support.
  */
 struct bootloader_message_ab {
     struct bootloader_message message;
     char slot_suffix[32];
+    char update_channel[128];
 
     // Round up the entire struct to 4096-byte.
-    char reserved[2016];
+    char reserved[1888];
 };
 
 /**
@@ -207,6 +211,11 @@
 // only update the command and recovery fields.
 bool update_bootloader_message(const std::vector<std::string>& options, std::string* err);
 
+// Update bootloader message (boots into recovery with the |options|) in |boot|. Will only update
+// the command and recovery fields.
+bool update_bootloader_message_in_struct(bootloader_message* boot,
+                                         const std::vector<std::string>& options);
+
 // Clear BCB.
 bool clear_bootloader_message(std::string* err);
 
diff --git a/common.h b/common.h
index 62fb132..8b336f8 100644
--- a/common.h
+++ b/common.h
@@ -22,8 +22,9 @@
 
 #include <string>
 
-#define STRINGIFY(x) #x
-#define EXPAND(x) STRINGIFY(x)
+// Not using the command-line defined macro here because this header could be included by
+// device-specific recovery libraries. We static assert the value consistency in recovery.cpp.
+static constexpr int kRecoveryApiVersion = 3;
 
 class RecoveryUI;
 
diff --git a/device.cpp b/device.cpp
index 6150186..f881daf 100644
--- a/device.cpp
+++ b/device.cpp
@@ -17,34 +17,36 @@
 #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",
+  "Reboot system now",
+  "Reboot to bootloader",
+  "Apply update from ADB",
+  "Apply update from SD card",
+  "Wipe data/factory reset",
 #ifndef AB_OTA_UPDATER
-    "Wipe cache partition",
+  "Wipe cache partition",
 #endif  // !AB_OTA_UPDATER
-    "Mount /system",
-    "View recovery logs",
-    "Run graphics test",
-    "Power off",
-    NULL,
+  "Mount /system",
+  "View recovery logs",
+  "Run graphics test",
+  "Run locale test",
+  "Power off",
+  nullptr,
 };
 
 static const Device::BuiltinAction MENU_ACTIONS[] = {
-    Device::REBOOT,
-    Device::REBOOT_BOOTLOADER,
-    Device::APPLY_ADB_SIDELOAD,
-    Device::APPLY_SDCARD,
-    Device::WIPE_DATA,
+  Device::REBOOT,
+  Device::REBOOT_BOOTLOADER,
+  Device::APPLY_ADB_SIDELOAD,
+  Device::APPLY_SDCARD,
+  Device::WIPE_DATA,
 #ifndef AB_OTA_UPDATER
-    Device::WIPE_CACHE,
+  Device::WIPE_CACHE,
 #endif  // !AB_OTA_UPDATER
-    Device::MOUNT_SYSTEM,
-    Device::VIEW_RECOVERY_LOGS,
-    Device::RUN_GRAPHICS_TEST,
-    Device::SHUTDOWN,
+  Device::MOUNT_SYSTEM,
+  Device::VIEW_RECOVERY_LOGS,
+  Device::RUN_GRAPHICS_TEST,
+  Device::RUN_LOCALE_TEST,
+  Device::SHUTDOWN,
 };
 
 static_assert(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]) ==
diff --git a/device.h b/device.h
index 639e2bf..74745b3 100644
--- a/device.h
+++ b/device.h
@@ -66,6 +66,7 @@
     VIEW_RECOVERY_LOGS = 9,
     MOUNT_SYSTEM = 10,
     RUN_GRAPHICS_TEST = 11,
+    RUN_LOCALE_TEST = 12,
   };
 
   // Return the list of menu items (an array of strings, NULL-terminated). The menu_position passed
diff --git a/edify/Android.bp b/edify/Android.bp
new file mode 100644
index 0000000..42947eb
--- /dev/null
+++ b/edify/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2017 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.
+
+cc_library_static {
+    name: "libedify",
+
+    host_supported: true,
+
+    srcs: [
+        "expr.cpp",
+        "lexer.ll",
+        "parser.yy",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-deprecated-register",
+        "-Wno-unused-parameter",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    local_include_dirs: [
+        "include",
+    ],
+
+    static_libs: [
+        "libbase",
+        "libotautil",
+    ],
+}
diff --git a/edify/Android.mk b/edify/Android.mk
deleted file mode 100644
index d8058c1..0000000
--- a/edify/Android.mk
+++ /dev/null
@@ -1,58 +0,0 @@
-# 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)
-
-edify_src_files := \
-    lexer.ll \
-    parser.yy \
-    expr.cpp
-
-#
-# Build the host-side command line tool (host executable)
-#
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    $(edify_src_files) \
-    edify_parser.cpp
-
-LOCAL_CFLAGS := -Werror
-LOCAL_CPPFLAGS := -g -O0
-LOCAL_MODULE := edify_parser
-LOCAL_YACCFLAGS := -v
-LOCAL_CPPFLAGS += -Wno-unused-parameter
-LOCAL_CPPFLAGS += -Wno-deprecated-register
-LOCAL_CLANG := true
-LOCAL_C_INCLUDES += $(LOCAL_PATH)/..
-LOCAL_STATIC_LIBRARIES += libbase
-
-include $(BUILD_HOST_EXECUTABLE)
-
-#
-# Build the device-side library (static library)
-#
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(edify_src_files)
-
-LOCAL_CFLAGS := -Werror
-LOCAL_CPPFLAGS := -Wno-unused-parameter
-LOCAL_CPPFLAGS += -Wno-deprecated-register
-LOCAL_MODULE := libedify
-LOCAL_CLANG := true
-LOCAL_C_INCLUDES += $(LOCAL_PATH)/..
-LOCAL_STATIC_LIBRARIES += libbase
-
-include $(BUILD_STATIC_LIBRARY)
diff --git a/edify/edify_parser.cpp b/edify/edify_parser.cpp
deleted file mode 100644
index f1b5628..0000000
--- a/edify/edify_parser.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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 is a host-side tool for validating a given edify script file.
- *
- * We used to have edify test cases here, which have been moved to
- * tests/component/edify_test.cpp.
- *
- * Caveat: It doesn't recognize functions defined through updater, which
- * makes the tool less useful. We should either extend the tool or remove it.
- */
-
-#include <errno.h>
-#include <stdio.h>
-
-#include <memory>
-#include <string>
-
-#include <android-base/file.h>
-
-#include "expr.h"
-
-static void ExprDump(int depth, const std::unique_ptr<Expr>& n, const std::string& script) {
-    printf("%*s", depth*2, "");
-    printf("%s %p (%d-%d) \"%s\"\n",
-           n->name.c_str(), n->fn, n->start, n->end,
-           script.substr(n->start, n->end - n->start).c_str());
-    for (size_t i = 0; i < n->argv.size(); ++i) {
-        ExprDump(depth+1, n->argv[i], script);
-    }
-}
-
-int main(int argc, char** argv) {
-    RegisterBuiltins();
-
-    if (argc != 2) {
-        printf("Usage: %s <edify script>\n", argv[0]);
-        return 1;
-    }
-
-    std::string buffer;
-    if (!android::base::ReadFileToString(argv[1], &buffer)) {
-        printf("%s: failed to read %s: %s\n", argv[0], argv[1], strerror(errno));
-        return 1;
-    }
-
-    std::unique_ptr<Expr> root;
-    int error_count = 0;
-    int error = parse_string(buffer.data(), &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(buffer, nullptr);
-        std::string result;
-        if (!Evaluate(&state, root, &result)) {
-            printf("result was NULL, message is: %s\n",
-                   (state.errmsg.empty() ? "(NULL)" : state.errmsg.c_str()));
-        } else {
-            printf("result is [%s]\n", result.c_str());
-        }
-    }
-    return 0;
-}
diff --git a/edify/expr.cpp b/edify/expr.cpp
index 54ab332..6823b73 100644
--- a/edify/expr.cpp
+++ b/edify/expr.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "expr.h"
+#include "edify/expr.h"
 
 #include <stdarg.h>
 #include <stdio.h>
@@ -31,6 +31,8 @@
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 
+#include "otautil/error_code.h"
+
 // Functions should:
 //
 //    - return a malloc()'d string
@@ -112,9 +114,9 @@
 Value* AbortFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
     std::string msg;
     if (!argv.empty() && Evaluate(state, argv[0], &msg)) {
-        state->errmsg = msg;
+      state->errmsg += msg;
     } else {
-        state->errmsg = "called abort()";
+      state->errmsg += "called abort()";
     }
     return nullptr;
 }
@@ -408,16 +410,16 @@
 }
 
 Value* ErrorAbort(State* state, CauseCode cause_code, const char* format, ...) {
-    va_list ap;
-    va_start(ap, format);
-    android::base::StringAppendV(&state->errmsg, format, ap);
-    va_end(ap);
-    state->cause_code = cause_code;
-    return nullptr;
+  std::string err_message;
+  va_list ap;
+  va_start(ap, format);
+  android::base::StringAppendV(&err_message, format, ap);
+  va_end(ap);
+  // Ensure that there's exactly one line break at the end of the error message.
+  state->errmsg = android::base::Trim(err_message) + "\n";
+  state->cause_code = cause_code;
+  return nullptr;
 }
 
-State::State(const std::string& script, void* cookie) :
-    script(script),
-    cookie(cookie) {
-}
-
+State::State(const std::string& script, void* cookie)
+    : script(script), cookie(cookie), error_code(kNoError), cause_code(kNoCause) {}
diff --git a/edify/expr.h b/edify/include/edify/expr.h
similarity index 86%
rename from edify/expr.h
rename to edify/include/edify/expr.h
index 4838d20..770d1cf 100644
--- a/edify/expr.h
+++ b/edify/include/edify/expr.h
@@ -23,32 +23,34 @@
 #include <string>
 #include <vector>
 
-#include "error_code.h"
+// Forward declaration to avoid including "otautil/error_code.h".
+enum ErrorCode : int;
+enum CauseCode : int;
 
 struct State {
-    State(const std::string& script, void* cookie);
+  State(const std::string& script, void* cookie);
 
-    // The source of the original script.
-    const std::string& script;
+  // The source of the original script.
+  const std::string& script;
 
-    // Optional pointer to app-specific data; the core of edify never
-    // uses this value.
-    void* cookie;
+  // Optional pointer to app-specific data; the core of edify never
+  // uses this value.
+  void* cookie;
 
-    // The error message (if any) returned if the evaluation aborts.
-    // Should be empty initially, will be either empty or a string that
-    // Evaluate() returns.
-    std::string errmsg;
+  // The error message (if any) returned if the evaluation aborts.
+  // Should be empty initially, will be either empty or a string that
+  // Evaluate() returns.
+  std::string errmsg;
 
-    // error code indicates the type of failure (e.g. failure to update system image)
-    // during the OTA process.
-    ErrorCode error_code = kNoError;
+  // error code indicates the type of failure (e.g. failure to update system image)
+  // during the OTA process.
+  ErrorCode error_code;
 
-    // cause code provides more detailed reason of an OTA failure (e.g. fsync error)
-    // in addition to the error code.
-    CauseCode cause_code = kNoCause;
+  // cause code provides more detailed reason of an OTA failure (e.g. fsync error)
+  // in addition to the error code.
+  CauseCode cause_code;
 
-    bool is_retry = false;
+  bool is_retry = false;
 };
 
 enum ValueType {
diff --git a/edify/lexer.ll b/edify/lexer.ll
index b764d16..4e04003 100644
--- a/edify/lexer.ll
+++ b/edify/lexer.ll
@@ -18,7 +18,7 @@
 #include <string.h>
 #include <string>
 
-#include "expr.h"
+#include "edify/expr.h"
 #include "yydefs.h"
 #include "parser.h"
 
@@ -35,6 +35,8 @@
 
 %x STR
 
+%option noinput
+%option nounput
 %option noyywrap
 
 %%
diff --git a/edify/parser.yy b/edify/parser.yy
index b1685eb..bd2e010 100644
--- a/edify/parser.yy
+++ b/edify/parser.yy
@@ -25,7 +25,7 @@
 
 #include <android-base/macros.h>
 
-#include "expr.h"
+#include "edify/expr.h"
 #include "yydefs.h"
 #include "parser.h"
 
diff --git a/etc/init.rc b/etc/init.rc
index d8121cc..0fc6c4c 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -11,6 +11,7 @@
     export ANDROID_DATA /data
     export EXTERNAL_STORAGE /sdcard
 
+    symlink /system/bin /bin
     symlink /system/etc /etc
 
     mount cgroup none /acct cpuacct
diff --git a/fuse_sdcard_provider.cpp b/fuse_sdcard_provider.cpp
index b0ecf96..46bdf17 100644
--- a/fuse_sdcard_provider.cpp
+++ b/fuse_sdcard_provider.cpp
@@ -14,72 +14,70 @@
  * limitations under the License.
  */
 
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
+#include "fuse_sdcard_provider.h"
+
 #include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 #include <sys/mount.h>
 #include <sys/stat.h>
 #include <unistd.h>
-#include <fcntl.h>
+
+#include <functional>
 
 #include <android-base/file.h>
 
 #include "fuse_sideload.h"
 
 struct file_data {
-    int fd;  // the underlying sdcard file
+  int fd;  // the underlying sdcard file
 
-    uint64_t file_size;
-    uint32_t block_size;
+  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);
+static int read_block_file(const file_data& fd, uint32_t block, uint8_t* buffer,
+                           uint32_t fetch_size) {
+  off64_t offset = static_cast<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;
+  }
 
-    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;
-    }
+  if (!android::base::ReadFully(fd.fd, buffer, fetch_size)) {
+    fprintf(stderr, "read on sdcard failed: %s\n", strerror(errno));
+    return -EIO;
+  }
 
-    if (!android::base::ReadFully(fd->fd, buffer, fetch_size)) {
-        fprintf(stderr, "read on sdcard failed: %s\n", strerror(errno));
-        return -EIO;
-    }
-
-    return 0;
-}
-
-static void close_file(void* cookie) {
-    file_data* fd = reinterpret_cast<file_data*>(cookie);
-    close(fd->fd);
+  return 0;
 }
 
 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;
-    }
+  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;
+  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;
+  provider_vtab vtab;
+  vtab.read_block = std::bind(&read_block_file, fd, std::placeholders::_1, std::placeholders::_2,
+                              std::placeholders::_3);
+  vtab.close = [&fd]() { close(fd.fd); };
 
-    // 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);
+  // 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;
+  return run_fuse_sideload(vtab, fd.file_size, fd.block_size) == 0;
 }
diff --git a/fuse_sideload.cpp b/fuse_sideload.cpp
index 219374f..1c7e98f 100644
--- a/fuse_sideload.cpp
+++ b/fuse_sideload.cpp
@@ -41,337 +41,310 @@
 // 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 "fuse_sideload.h"
+
 #include <errno.h>
 #include <fcntl.h>
-#include <limits.h>
+#include <limits.h>  // PATH_MAX
 #include <linux/fuse.h>
-#include <pthread.h>
+#include <stdint.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/param.h>  // MIN
 #include <sys/stat.h>
-#include <sys/statfs.h>
-#include <sys/time.h>
 #include <sys/uio.h>
 #include <unistd.h>
 
+#include <array>
 #include <string>
+#include <vector>
 
 #include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
 #include <openssl/sha.h>
 
-#include "fuse_sideload.h"
+static constexpr uint64_t PACKAGE_FILE_ID = FUSE_ROOT_ID + 1;
+static constexpr uint64_t EXIT_FLAG_ID = FUSE_ROOT_ID + 2;
 
-#define PACKAGE_FILE_ID   (FUSE_ROOT_ID+1)
-#define EXIT_FLAG_ID      (FUSE_ROOT_ID+2)
+static constexpr int NO_STATUS = 1;
+static constexpr int NO_STATUS_EXIT = 2;
 
-#define NO_STATUS         1
-#define NO_STATUS_EXIT    2
+using SHA256Digest = std::array<uint8_t, SHA256_DIGEST_LENGTH>;
 
 struct fuse_data {
-    int ffd;   // file descriptor for the fuse socket
+  android::base::unique_fd ffd;  // file descriptor for the fuse socket
 
-    struct provider_vtab* vtab;
-    void* cookie;
+  provider_vtab vtab;
 
-    uint64_t file_size;     // bytes
+  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
+  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;
+  uid_t uid;
+  gid_t gid;
 
-    uint32_t curr_block;    // cache the block most recently read from the host
-    uint8_t* block_data;
+  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* 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)
+  std::vector<SHA256Digest>
+      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;
+static void fuse_reply(const fuse_data* fd, uint64_t unique, const void* data, size_t len) {
+  fuse_out_header hdr;
+  hdr.len = len + sizeof(hdr);
+  hdr.error = 0;
+  hdr.unique = unique;
 
-    hdr.len = len + sizeof(hdr);
-    hdr.error = 0;
-    hdr.unique = unique;
+  struct iovec vec[2];
+  vec[0].iov_base = &hdr;
+  vec[0].iov_len = sizeof(hdr);
+  vec[1].iov_base = const_cast<void*>(data);
+  vec[1].iov_len = len;
 
-    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));
-    }
+  int res = writev(fd->ffd, vec, 2);
+  if (res == -1) {
+    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;
+static int handle_init(void* data, fuse_data* fd, const fuse_in_header* hdr) {
+  const fuse_init_in* req = static_cast<const fuse_init_in*>(data);
 
+  // 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;
+  }
 
-    /* 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);
+  fuse_init_out out;
+  out.minor = MIN(req->minor, FUSE_KERNEL_MINOR_VERSION);
+  size_t fuse_struct_size = sizeof(out);
 #if defined(FUSE_COMPAT_22_INIT_OUT_SIZE)
-    /* FUSE_KERNEL_VERSION >= 23. */
+  /* 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;
-    }
+  // 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);
+  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;
+  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;
+static void fill_attr(fuse_attr* attr, const fuse_data* fd, uint64_t nodeid, uint64_t size,
+                      uint32_t mode) {
+  *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;
+  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;
+static int handle_getattr(void* /* data */, const fuse_data* fd, const fuse_in_header* hdr) {
+  fuse_attr_out 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;
-    }
+  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;
+  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;
+static int handle_lookup(void* data, const fuse_data* fd, const fuse_in_header* hdr) {
+  if (data == nullptr) return -ENOENT;
 
-    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_entry_out out = {};
+  out.entry_valid = 10;
+  out.attr_valid = 10;
 
-    fuse_reply(fd, hdr->unique, &out, sizeof(out));
-    return (out.nodeid == EXIT_FLAG_ID) ? NO_STATUS_EXIT : NO_STATUS;
+  std::string filename(static_cast<const char*>(data));
+  if (filename == FUSE_SIDELOAD_HOST_FILENAME) {
+    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 (filename == FUSE_SIDELOAD_HOST_EXIT_FLAG) {
+    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;
+static int handle_open(void* /* data */, const fuse_data* fd, const 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;
+  fuse_open_out 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_flush(void* /* data */, fuse_data* /* fd */, const fuse_in_header* /* hdr */) {
+  return 0;
 }
 
-static int handle_release(void* /* data */, struct fuse_data* /* fd */,
-                          const struct fuse_in_header* /* hdr */) {
-    return 0;
+static int handle_release(void* /* data */, fuse_data* /* fd */, const 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);
+static int fetch_block(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(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.
+
+  SHA256Digest hash;
+  SHA256(fd->block_data, fd->block_size, hash.data());
+
+  const SHA256Digest& blockhash = fd->hashes[block];
+  if (hash == blockhash) {
+    return 0;
+  }
+
+  for (uint8_t i : blockhash) {
+    if (i != 0) {
+      fd->curr_block = -1;
+      return -EIO;
+    }
+  }
+
+  fd->hashes[block] = hash;
+  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;
+static int handle_read(void* data, fuse_data* fd, const fuse_in_header* hdr) {
+  if (hdr->nodeid != PACKAGE_FILE_ID) return -ENOENT;
 
-    if (hdr->nodeid != PACKAGE_FILE_ID) return -ENOENT;
+  const fuse_read_in* req = static_cast<const fuse_read_in*>(data);
+  uint64_t offset = req->offset;
+  uint32_t size = req->size;
 
-    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.)
 
-    // 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.)
+  fuse_out_header outhdr;
+  outhdr.len = sizeof(outhdr) + size;
+  outhdr.error = 0;
+  outhdr.unique = hdr->unique;
 
-    outhdr.len = sizeof(outhdr) + size;
-    outhdr.error = 0;
-    outhdr.unique = hdr->unique;
-    vec[0].iov_base = &outhdr;
-    vec[0].iov_len = sizeof(outhdr);
+  struct iovec vec[3];
+  vec[0].iov_base = &outhdr;
+  vec[0].iov_len = sizeof(outhdr);
 
-    uint32_t block = offset / fd->block_size;
-    result = fetch_block(fd, block);
+  uint32_t block = offset / fd->block_size;
+  int 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);
+
+  int vec_used;
+  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;
+  }
 
-    // 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;
+  if (writev(fd->ffd, vec, vec_used) == -1) {
+    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 run_fuse_sideload(const provider_vtab& vtab, uint64_t file_size, uint32_t block_size,
+                      const char* mount_point) {
   // 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);
+  umount2(mount_point, MNT_FORCE);
 
   // fs/fuse/inode.c in kernel code uses the greater of 4096 and the passed-in max_read.
   if (block_size < 4096) {
@@ -383,9 +356,8 @@
     return -1;
   }
 
-  struct fuse_data fd = {};
+  fuse_data 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);
@@ -397,33 +369,27 @@
     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;
-  }
-
+  // All hashes will be zero-initialized.
+  fd.hashes.resize(fd.file_blocks);
   fd.uid = getuid();
   fd.gid = getgid();
 
   fd.curr_block = -1;
-  fd.block_data = (uint8_t*)malloc(block_size);
-  if (fd.block_data == NULL) {
+  fd.block_data = static_cast<uint8_t*>(malloc(block_size));
+  if (fd.block_data == nullptr) {
     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) {
+  fd.extra_block = static_cast<uint8_t*>(malloc(block_size));
+  if (fd.extra_block == nullptr) {
     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) {
+  fd.ffd.reset(open("/dev/fuse", O_RDWR));
+  if (!fd.ffd) {
     perror("open /dev/fuse");
     result = -1;
     goto done;
@@ -431,18 +397,18 @@
 
   {
     std::string opts = android::base::StringPrintf(
-        "fd=%d,user_id=%d,group_id=%d,max_read=%u,allow_other,rootmode=040000", fd.ffd, fd.uid,
-        fd.gid, block_size);
+        "fd=%d,user_id=%d,group_id=%d,max_read=%u,allow_other,rootmode=040000", fd.ffd.get(),
+        fd.uid, fd.gid, block_size);
 
-    result = mount("/dev/fuse", FUSE_SIDELOAD_HOST_MOUNTPOINT, "fuse",
-                   MS_NOSUID | MS_NODEV | MS_RDONLY | MS_NOEXEC, opts.c_str());
-    if (result < 0) {
+    result = mount("/dev/fuse", mount_point, "fuse", MS_NOSUID | MS_NODEV | MS_RDONLY | MS_NOEXEC,
+                   opts.c_str());
+    if (result == -1) {
       perror("mount");
       goto done;
     }
   }
 
-  uint8_t request_buffer[sizeof(struct fuse_in_header) + PATH_MAX * 8];
+  uint8_t request_buffer[sizeof(fuse_in_header) + PATH_MAX * 8];
   for (;;) {
     ssize_t len = TEMP_FAILURE_RETRY(read(fd.ffd, request_buffer, sizeof(request_buffer)));
     if (len == -1) {
@@ -454,13 +420,13 @@
       continue;
     }
 
-    if (static_cast<size_t>(len) < sizeof(struct fuse_in_header)) {
+    if (static_cast<size_t>(len) < sizeof(fuse_in_header)) {
       fprintf(stderr, "request too short: len=%zd\n", len);
       continue;
     }
 
-    struct fuse_in_header* hdr = reinterpret_cast<struct fuse_in_header*>(request_buffer);
-    void* data = request_buffer + sizeof(struct fuse_in_header);
+    fuse_in_header* hdr = reinterpret_cast<fuse_in_header*>(request_buffer);
+    void* data = request_buffer + sizeof(fuse_in_header);
 
     result = -ENOSYS;
 
@@ -504,7 +470,7 @@
     }
 
     if (result != NO_STATUS) {
-      struct fuse_out_header outhdr;
+      fuse_out_header outhdr;
       outhdr.len = sizeof(outhdr);
       outhdr.error = result;
       outhdr.unique = hdr->unique;
@@ -513,15 +479,12 @@
   }
 
 done:
-  fd.vtab->close(fd.cookie);
+  fd.vtab.close();
 
-  result = umount2(FUSE_SIDELOAD_HOST_MOUNTPOINT, MNT_DETACH);
-  if (result < 0) {
-    printf("fuse_sideload umount failed: %s\n", strerror(errno));
+  if (umount2(mount_point, MNT_DETACH) == -1) {
+    fprintf(stderr, "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);
 
diff --git a/fuse_sideload.h b/fuse_sideload.h
index c0b16ef..1b34cbd 100644
--- a/fuse_sideload.h
+++ b/fuse_sideload.h
@@ -17,22 +17,24 @@
 #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)
+#include <functional>
+
+// Define the filenames created by the sideload FUSE filesystem.
+static constexpr const char* FUSE_SIDELOAD_HOST_MOUNTPOINT = "/sideload";
+static constexpr const char* FUSE_SIDELOAD_HOST_FILENAME = "package.zip";
+static constexpr const char* FUSE_SIDELOAD_HOST_PATHNAME = "/sideload/package.zip";
+static constexpr const char* FUSE_SIDELOAD_HOST_EXIT_FLAG = "exit";
+static constexpr const char* FUSE_SIDELOAD_HOST_EXIT_PATHNAME = "/sideload/exit";
 
 struct provider_vtab {
-    // read a block
-    int (*read_block)(void* cookie, uint32_t block, uint8_t* buffer, uint32_t fetch_size);
+  // read a block
+  std::function<int(uint32_t block, uint8_t* buffer, uint32_t fetch_size)> read_block;
 
-    // close down
-    void (*close)(void* cookie);
+  // close down
+  std::function<void(void)> close;
 };
 
-int run_fuse_sideload(struct provider_vtab* vtab, void* cookie,
-                      uint64_t file_size, uint32_t block_size);
+int run_fuse_sideload(const provider_vtab& vtab, uint64_t file_size, uint32_t block_size,
+                      const char* mount_point = FUSE_SIDELOAD_HOST_MOUNTPOINT);
 
 #endif
diff --git a/install.cpp b/install.cpp
index 7fbf5c0..d058931 100644
--- a/install.cpp
+++ b/install.cpp
@@ -49,9 +49,9 @@
 #include <ziparchive/zip_archive.h>
 
 #include "common.h"
-#include "error_code.h"
 #include "otautil/SysUtil.h"
 #include "otautil/ThermalUtil.h"
+#include "otautil/error_code.h"
 #include "private/install.h"
 #include "roots.h"
 #include "ui.h"
@@ -148,13 +148,23 @@
     return INSTALL_ERROR;
   }
 
-  // We allow the package to not have any serialno, but if it has a non-empty
-  // value it should match.
+  // We allow the package to not have any serialno; and we also allow it to carry multiple serial
+  // numbers split by "|"; e.g. serialno=serialno1|serialno2|serialno3 ... We will fail the
+  // verification if the device's serialno doesn't match any of these carried numbers.
   value = android::base::GetProperty("ro.serialno", "");
   const std::string& pkg_serial_no = metadata["serialno"];
-  if (!pkg_serial_no.empty() && pkg_serial_no != value) {
-    LOG(ERROR) << "Package is for serial " << pkg_serial_no;
-    return INSTALL_ERROR;
+  if (!pkg_serial_no.empty()) {
+    bool match = false;
+    for (const std::string& number : android::base::Split(pkg_serial_no, "|")) {
+      if (value == android::base::Trim(number)) {
+        match = true;
+        break;
+      }
+    }
+    if (!match) {
+      LOG(ERROR) << "Package is for serial " << pkg_serial_no;
+      return INSTALL_ERROR;
+    }
   }
 
   if (metadata["ota-type"] != "AB") {
@@ -280,7 +290,7 @@
 
   *cmd = {
     binary_path,
-    EXPAND(RECOVERY_API_VERSION),  // defined in Android.mk
+    std::to_string(kRecoveryApiVersion),
     std::to_string(status_fd),
     package,
   };
@@ -321,6 +331,7 @@
   if (ret) {
     close(pipefd[0]);
     close(pipefd[1]);
+    log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
     return ret;
   }
 
@@ -385,6 +396,7 @@
     close(pipefd[0]);
     close(pipefd[1]);
     PLOG(ERROR) << "Failed to fork update binary";
+    log_buffer->push_back(android::base::StringPrintf("error: %d", kForkUpdateBinaryFailure));
     return INSTALL_ERROR;
   }
 
@@ -573,6 +585,7 @@
   MemMapping map;
   if (!map.MapFile(path)) {
     LOG(ERROR) << "failed to map file";
+    log_buffer->push_back(android::base::StringPrintf("error: %d", kMapFileFailure));
     return INSTALL_CORRUPT;
   }
 
@@ -640,7 +653,7 @@
   std::chrono::duration<double> duration = std::chrono::system_clock::now() - start;
   int time_total = static_cast<int>(duration.count());
 
-  bool has_cache = volume_for_path("/cache") != nullptr;
+  bool has_cache = volume_for_mount_point("/cache") != nullptr;
   // Skip logging the uncrypt_status on devices without /cache.
   if (has_cache) {
     static constexpr const char* UNCRYPT_STATUS = "/cache/recovery/uncrypt_status";
diff --git a/minadbd/Android.mk b/minadbd/Android.mk
index de0b0c8..50e3b34 100644
--- a/minadbd/Android.mk
+++ b/minadbd/Android.mk
@@ -1,13 +1,25 @@
 # Copyright 2005 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)
 
 minadbd_cflags := \
     -Wall -Werror \
-    -Wno-unused-parameter \
-    -Wno-missing-field-initializers \
     -DADB_HOST=0 \
 
+# libminadbd (static library)
+# ===============================
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := \
@@ -15,25 +27,29 @@
     minadbd.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)
 
+# minadbd_test (native test)
+# ===============================
 include $(CLEAR_VARS)
 
-LOCAL_CLANG := true
 LOCAL_MODULE := minadbd_test
 LOCAL_COMPATIBILITY_SUITE := device-tests
 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
+LOCAL_STATIC_LIBRARIES := \
+    libBionicGtestMain \
+    libminadbd
+LOCAL_SHARED_LIBRARIES := \
+    liblog \
+    libbase \
+    libcutils
 
 include $(BUILD_NATIVE_TEST)
diff --git a/minadbd/fuse_adb_provider.cpp b/minadbd/fuse_adb_provider.cpp
index 0f4c256..9bd3f23 100644
--- a/minadbd/fuse_adb_provider.cpp
+++ b/minadbd/fuse_adb_provider.cpp
@@ -14,46 +14,43 @@
  * limitations under the License.
  */
 
-#include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
+#include "fuse_adb_provider.h"
+
 #include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <functional>
 
 #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);
+int read_block_adb(const adb_data& ad, uint32_t block, uint8_t* buffer, uint32_t fetch_size) {
+  if (!WriteFdFmt(ad.sfd, "%08u", block)) {
+    fprintf(stderr, "failed to write to adb host: %s\n", strerror(errno));
+    return -EIO;
+  }
 
-    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;
+  }
 
-    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");
+  return 0;
 }
 
 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;
+  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;
+  provider_vtab vtab;
+  vtab.read_block = std::bind(read_block_adb, ad, std::placeholders::_1, std::placeholders::_2,
+                              std::placeholders::_3);
+  vtab.close = [&ad]() { WriteFdExactly(ad.sfd, "DONEDONE"); };
 
-    return run_fuse_sideload(&vtab, &ad, file_size, block_size);
+  return run_fuse_sideload(vtab, file_size, block_size);
 }
diff --git a/minadbd/fuse_adb_provider.h b/minadbd/fuse_adb_provider.h
index 9941709..36d86d5 100644
--- a/minadbd/fuse_adb_provider.h
+++ b/minadbd/fuse_adb_provider.h
@@ -20,13 +20,13 @@
 #include <stdint.h>
 
 struct adb_data {
-    int sfd;  // file descriptor for the adb channel
+  int sfd;  // file descriptor for the adb channel
 
-    uint64_t file_size;
-    uint32_t block_size;
+  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 read_block_adb(const adb_data& ad, 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/minadbd/fuse_adb_provider_test.cpp b/minadbd/fuse_adb_provider_test.cpp
index 31be2a6..00250e5 100644
--- a/minadbd/fuse_adb_provider_test.cpp
+++ b/minadbd/fuse_adb_provider_test.cpp
@@ -46,8 +46,8 @@
 
   uint32_t block = 1234U;
   const char expected_block[] = "00001234";
-  ASSERT_EQ(0, read_block_adb(static_cast<void*>(&data), block,
-                              reinterpret_cast<uint8_t*>(block_data), sizeof(expected_data) - 1));
+  ASSERT_EQ(0, read_block_adb(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)] = {};
@@ -84,7 +84,7 @@
   signal(SIGPIPE, SIG_IGN);
 
   char buf[1];
-  ASSERT_EQ(-EIO, read_block_adb(static_cast<void*>(&data), 0, reinterpret_cast<uint8_t*>(buf), 1));
+  ASSERT_EQ(-EIO, read_block_adb(data, 0, reinterpret_cast<uint8_t*>(buf), 1));
 
   close(sockets[0]);
 }
diff --git a/minadbd/minadbd_services.cpp b/minadbd/minadbd_services.cpp
index 61c06cc..043c51a 100644
--- a/minadbd/minadbd_services.cpp
+++ b/minadbd/minadbd_services.cpp
@@ -58,20 +58,20 @@
     return s[0];
 }
 
-int service_to_fd(const char* name, const atransport* transport) {
-    int ret = -1;
+int service_to_fd(const char* name, 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)) {
-        std::string arg(name + 14);
-        ret = create_service_thread(sideload_host_service, arg);
-    }
-    if (ret >= 0) {
-        close_on_exec(ret);
-    }
-    return ret;
+  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)) {
+    std::string arg(name + 14);
+    ret = create_service_thread(sideload_host_service, arg);
+  }
+  if (ret >= 0) {
+    close_on_exec(ret);
+  }
+  return ret;
 }
diff --git a/minui/Android.mk b/minui/Android.mk
index 6522fcf..ae1552b 100644
--- a/minui/Android.mk
+++ b/minui/Android.mk
@@ -13,6 +13,9 @@
 # limitations under the License.
 
 LOCAL_PATH := $(call my-dir)
+
+# libminui (static library)
+# ===============================
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := \
@@ -25,14 +28,14 @@
 
 LOCAL_WHOLE_STATIC_LIBRARIES := \
     libadf \
-    libdrm_platform \
+    libdrm \
     libsync_recovery
 
 LOCAL_STATIC_LIBRARIES := \
     libpng \
     libbase
 
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
 LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
 
@@ -58,8 +61,16 @@
   LOCAL_CFLAGS += -DOVERSCAN_PERCENT=0
 endif
 
+ifneq ($(TARGET_RECOVERY_DEFAULT_ROTATION),)
+  LOCAL_CFLAGS += -DDEFAULT_ROTATION=$(TARGET_RECOVERY_DEFAULT_ROTATION)
+else
+  LOCAL_CFLAGS += -DDEFAULT_ROTATION=ROTATION_NONE
+endif
+
 include $(BUILD_STATIC_LIBRARY)
 
+# libminui (shared library)
+# ===============================
 # Used by OEMs for factory test images.
 include $(CLEAR_VARS)
 LOCAL_MODULE := libminui
@@ -68,7 +79,7 @@
     libpng \
     libbase
 
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
 LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
 include $(BUILD_SHARED_LIBRARY)
diff --git a/minui/events.cpp b/minui/events.cpp
index 24c2a82..2894c3b 100644
--- a/minui/events.cpp
+++ b/minui/events.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <dirent.h>
+#include <errno.h>
 #include <fcntl.h>
 #include <linux/input.h>
 #include <stdio.h>
diff --git a/minui/graphics.cpp b/minui/graphics.cpp
index 3bfce11..56f471b 100644
--- a/minui/graphics.cpp
+++ b/minui/graphics.cpp
@@ -16,6 +16,7 @@
 
 #include "graphics.h"
 
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -35,281 +36,311 @@
 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 uint32_t gr_current = ~0;
+static constexpr uint32_t alpha_mask = 0xff000000;
 
 static GRSurface* gr_draw = NULL;
+static GRRotation rotation = ROTATION_NONE;
 
-static bool outside(int x, int y)
-{
-    return x < 0 || x >= gr_draw->width || y < 0 || y >= gr_draw->height;
+static bool outside(int x, int y) {
+  return x < 0 || x >= (rotation % 2 ? gr_draw->height : gr_draw->width) || y < 0 ||
+         y >= (rotation % 2 ? gr_draw->width : gr_draw->height);
 }
 
-const GRFont* gr_sys_font()
-{
-    return gr_font;
+const GRFont* gr_sys_font() {
+  return gr_font;
 }
 
-int gr_measure(const GRFont* font, const char *s)
-{
-    return font->char_width * strlen(s);
+int gr_measure(const GRFont* font, const char* s) {
+  return font->char_width * strlen(s);
 }
 
-void gr_font_size(const GRFont* font, int *x, int *y)
-{
-    *x = font->char_width;
-    *y = font->char_height;
+void gr_font_size(const GRFont* font, int* x, int* y) {
+  *x = font->char_width;
+  *y = font->char_height;
 }
 
-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;
+// Blends gr_current onto pix value, assumes alpha as most significant byte.
+static inline uint32_t pixel_blend(uint8_t alpha, uint32_t pix) {
+  if (alpha == 255) return gr_current;
+  if (alpha == 0) return pix;
+  uint32_t pix_r = pix & 0xff;
+  uint32_t pix_g = pix & 0xff00;
+  uint32_t pix_b = pix & 0xff0000;
+  uint32_t cur_r = gr_current & 0xff;
+  uint32_t cur_g = gr_current & 0xff00;
+  uint32_t cur_b = gr_current & 0xff0000;
+
+  uint32_t out_r = (pix_r * (255 - alpha) + cur_r * alpha) / 255;
+  uint32_t out_g = (pix_g * (255 - alpha) + cur_g * alpha) / 255;
+  uint32_t out_b = (pix_b * (255 - alpha) + cur_b * alpha) / 255;
+
+  return (out_r & 0xff) | (out_g & 0xff00) | (out_b & 0xff0000) | (gr_current & 0xff000000);
+}
+
+// increments pixel pointer right, with current rotation.
+static void incr_x(uint32_t** p, int row_pixels) {
+  if (rotation % 2) {
+    *p = *p + (rotation == 1 ? 1 : -1) * row_pixels;
+  } else {
+    *p = *p + (rotation ? -1 : 1);
+  }
+}
+
+// increments pixel pointer down, with current rotation.
+static void incr_y(uint32_t** p, int row_pixels) {
+  if (rotation % 2) {
+    *p = *p + (rotation == 1 ? -1 : 1);
+  } else {
+    *p = *p + (rotation ? -1 : 1) * row_pixels;
+  }
+}
+
+// returns pixel pointer at given coordinates with rotation adjustment.
+static uint32_t* pixel_at(GRSurface* surf, int x, int y, int row_pixels) {
+  switch (rotation) {
+    case ROTATION_NONE:
+      return reinterpret_cast<uint32_t*>(surf->data) + y * row_pixels + x;
+    case ROTATION_RIGHT:
+      return reinterpret_cast<uint32_t*>(surf->data) + x * row_pixels + (surf->width - y);
+    case ROTATION_DOWN:
+      return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - y) * row_pixels +
+             (surf->width - 1 - x);
+    case ROTATION_LEFT:
+      return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - x) * row_pixels + y;
+    default:
+      printf("invalid rotation %d", rotation);
+  }
+  return nullptr;
+}
+
+static void text_blend(uint8_t* src_p, int src_row_bytes, uint32_t* dst_p, int dst_row_pixels,
+                       int width, int height) {
+  uint8_t alpha_current = static_cast<uint8_t>((alpha_mask & gr_current) >> 24);
+  for (int j = 0; j < height; ++j) {
+    uint8_t* sx = src_p;
+    uint32_t* px = dst_p;
+    for (int i = 0; i < width; ++i, incr_x(&px, dst_row_pixels)) {
+      uint8_t a = *sx++;
+      if (alpha_current < 255) a = (static_cast<uint32_t>(a) * alpha_current) / 255;
+      *px = pixel_blend(a, *px);
     }
+    src_p += src_row_bytes;
+    incr_y(&dst_p, dst_row_pixels);
+  }
 }
 
-void gr_text(const GRFont* font, int x, int y, const char *s, bool bold)
-{
-    if (!font->texture || gr_current_a == 0) return;
+void gr_text(const GRFont* font, int x, int y, const char* s, bool bold) {
+  if (!font || !font->texture || (gr_current & alpha_mask) == 0) return;
 
-    bold = bold && (font->texture->height != font->char_height);
+  if (font->texture->pixel_bytes != 1) {
+    printf("gr_text: font has wrong format\n");
+    return;
+  }
 
-    x += overscan_offset_x;
-    y += overscan_offset_y;
+  bold = bold && (font->texture->height != font->char_height);
 
-    unsigned char ch;
-    while ((ch = *s++)) {
-        if (outside(x, y) || outside(x+font->char_width-1, y+font->char_height-1)) break;
+  x += overscan_offset_x;
+  y += overscan_offset_y;
 
-        if (ch < ' ' || ch > '~') {
-            ch = '?';
-        }
+  unsigned char ch;
+  while ((ch = *s++)) {
+    if (outside(x, y) || outside(x + font->char_width - 1, y + font->char_height - 1)) break;
 
-        unsigned char* src_p = font->texture->data + ((ch - ' ') * font->char_width) +
-                               (bold ? font->char_height * 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->char_width, font->char_height);
-
-        x += font->char_width;
+    if (ch < ' ' || ch > '~') {
+      ch = '?';
     }
+
+    int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
+    uint8_t* src_p = font->texture->data + ((ch - ' ') * font->char_width) +
+                     (bold ? font->char_height * font->texture->row_bytes : 0);
+    uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels);
+
+    text_blend(src_p, font->texture->row_bytes, dst_p, row_pixels, font->char_width,
+               font->char_height);
+
+    x += font->char_width;
+  }
 }
 
 void gr_texticon(int x, int y, GRSurface* icon) {
-    if (icon == NULL) return;
+  if (icon == NULL) return;
 
-    if (icon->pixel_bytes != 1) {
-        printf("gr_texticon: source has wrong format\n");
-        return;
-    }
+  if (icon->pixel_bytes != 1) {
+    printf("gr_texticon: source has wrong format\n");
+    return;
+  }
 
-    x += overscan_offset_x;
-    y += overscan_offset_y;
+  x += overscan_offset_x;
+  y += overscan_offset_y;
 
-    if (outside(x, y) || outside(x+icon->width-1, y+icon->height-1)) return;
+  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;
+  int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
+  uint8_t* src_p = icon->data;
+  uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels);
 
-    text_blend(src_p, icon->row_bytes,
-               dst_p, gr_draw->row_bytes,
-               icon->width, icon->height);
+  text_blend(src_p, icon->row_bytes, dst_p, row_pixels, icon->width, icon->height);
 }
 
-void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
-{
+void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
+  uint32_t r32 = r, g32 = g, b32 = b, a32 = a;
 #if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-    gr_current_r = b;
-    gr_current_g = g;
-    gr_current_b = r;
-    gr_current_a = a;
+  gr_current = (a32 << 24) | (r32 << 16) | (g32 << 8) | b32;
 #else
-    gr_current_r = r;
-    gr_current_g = g;
-    gr_current_b = b;
-    gr_current_a = a;
+  gr_current = (a32 << 24) | (b32 << 16) | (g32 << 8) | r32;
 #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_clear() {
+  if ((gr_current & 0xff) == ((gr_current >> 8) & 0xff) &&
+      (gr_current & 0xff) == ((gr_current >> 16) & 0xff) &&
+      (gr_current & 0xff) == ((gr_current >> 24) & 0xff) &&
+      gr_draw->row_bytes == gr_draw->width * gr_draw->pixel_bytes) {
+    memset(gr_draw->data, gr_current & 0xff, gr_draw->height * gr_draw->row_bytes);
+  } else {
+    uint32_t* px = reinterpret_cast<uint32_t*>(gr_draw->data);
+    int row_diff = gr_draw->row_bytes / gr_draw->pixel_bytes - gr_draw->width;
+    for (int y = 0; y < gr_draw->height; ++y) {
+      for (int x = 0; x < gr_draw->width; ++x) {
+        *px++ = gr_current;
+      }
+      px += row_diff;
     }
+  }
 }
 
-void gr_fill(int x1, int y1, int x2, int y2)
-{
-    x1 += overscan_offset_x;
-    y1 += overscan_offset_y;
+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;
+  x2 += overscan_offset_x;
+  y2 += overscan_offset_y;
 
-    if (outside(x1, y1) || outside(x2-1, y2-1)) return;
+  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;
-        }
+  int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
+  uint32_t* p = pixel_at(gr_draw, x1, y1, row_pixels);
+  uint8_t alpha = static_cast<uint8_t>(((gr_current & alpha_mask) >> 24));
+  if (alpha > 0) {
+    for (int y = y1; y < y2; ++y) {
+      uint32_t* px = p;
+      for (int x = x1; x < x2; ++x) {
+        *px = pixel_blend(alpha, *px);
+        incr_x(&px, row_pixels);
+      }
+      incr_y(&p, row_pixels);
     }
+  }
 }
 
 void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
-    if (source == NULL) return;
+  if (source == NULL) return;
 
-    if (gr_draw->pixel_bytes != source->pixel_bytes) {
-        printf("gr_blit: source has wrong format\n");
-        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;
+
+  if (rotation) {
+    int src_row_pixels = source->row_bytes / source->pixel_bytes;
+    int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
+    uint32_t* src_py = reinterpret_cast<uint32_t*>(source->data) + sy * source->row_bytes / 4 + sx;
+    uint32_t* dst_py = pixel_at(gr_draw, dx, dy, row_pixels);
+
+    for (int y = 0; y < h; y += 1) {
+      uint32_t* src_px = src_py;
+      uint32_t* dst_px = dst_py;
+      for (int x = 0; x < w; x += 1) {
+        *dst_px = *src_px++;
+        incr_x(&dst_px, row_pixels);
+      }
+      src_py += src_row_pixels;
+      incr_y(&dst_py, row_pixels);
     }
-
-    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;
+  } else {
+    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;
+      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;
+  if (surface == NULL) {
+    return 0;
+  }
+  return surface->width;
 }
 
 unsigned int gr_get_height(GRSurface* surface) {
-    if (surface == NULL) {
-        return 0;
-    }
-    return surface->height;
+  if (surface == NULL) {
+    return 0;
+  }
+  return surface->height;
 }
 
 int gr_init_font(const char* name, GRFont** dest) {
-    GRFont* font = static_cast<GRFont*>(calloc(1, sizeof(*gr_font)));
-    if (font == nullptr) {
-        return -1;
-    }
+  GRFont* font = static_cast<GRFont*>(calloc(1, sizeof(*gr_font)));
+  if (font == nullptr) {
+    return -1;
+  }
 
-    int res = res_create_alpha_surface(name, &(font->texture));
-    if (res < 0) {
-        free(font);
-        return res;
-    }
+  int res = res_create_alpha_surface(name, &(font->texture));
+  if (res < 0) {
+    free(font);
+    return res;
+  }
 
-    // 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.
-    font->char_width = font->texture->width / 96;
-    font->char_height = font->texture->height / 2;
+  // 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.
+  font->char_width = font->texture->width / 96;
+  font->char_height = font->texture->height / 2;
 
-    *dest = font;
+  *dest = font;
 
-    return 0;
+  return 0;
 }
 
-static void gr_init_font(void)
-{
-    int res = gr_init_font("font", &gr_font);
-    if (res == 0) {
-        return;
-    }
+static void gr_init_font(void) {
+  int res = gr_init_font("font", &gr_font);
+  if (res == 0) {
+    return;
+  }
 
-    printf("failed to read font: res=%d\n", res);
+  printf("failed to read font: res=%d\n", res);
 
+  // fall back to the compiled-in font.
+  gr_font = static_cast<GRFont*>(calloc(1, sizeof(*gr_font)));
+  gr_font->texture = static_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;
 
-    // fall back to the compiled-in font.
-    gr_font = static_cast<GRFont*>(calloc(1, sizeof(*gr_font)));
-    gr_font->texture = static_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 = static_cast<unsigned char*>(malloc(font.width * font.height));
+  gr_font->texture->data = bits;
 
-    unsigned char* bits = static_cast<unsigned char*>(malloc(font.width * font.height));
-    gr_font->texture->data = bits;
+  unsigned char data;
+  unsigned char* in = font.rundata;
+  while ((data = *in++)) {
+    memset(bits, (data & 0x80) ? 255 : 0, data & 0x7f);
+    bits += (data & 0x7f);
+  }
 
-    unsigned char data;
-    unsigned char* in = font.rundata;
-    while((data = *in++)) {
-        memset(bits, (data & 0x80) ? 255 : 0, data & 0x7f);
-        bits += (data & 0x7f);
-    }
-
-    gr_font->char_width = font.char_width;
-    gr_font->char_height = font.char_height;
+  gr_font->char_width = font.char_width;
+  gr_font->char_height = font.char_height;
 }
 
 void gr_flip() {
@@ -344,6 +375,12 @@
   gr_flip();
   gr_flip();
 
+  gr_rotate(DEFAULT_ROTATION);
+
+  if (gr_draw->pixel_bytes != 4) {
+    printf("gr_init: Only 4-byte pixel formats supported\n");
+  }
+
   return 0;
 }
 
@@ -352,13 +389,19 @@
 }
 
 int gr_fb_width() {
-  return gr_draw->width - 2 * overscan_offset_x;
+  return rotation % 2 ? gr_draw->height - 2 * overscan_offset_y
+                      : gr_draw->width - 2 * overscan_offset_x;
 }
 
 int gr_fb_height() {
-  return gr_draw->height - 2 * overscan_offset_y;
+  return rotation % 2 ? gr_draw->width - 2 * overscan_offset_x
+                      : gr_draw->height - 2 * overscan_offset_y;
 }
 
 void gr_fb_blank(bool blank) {
   gr_backend->Blank(blank);
 }
+
+void gr_rotate(GRRotation rot) {
+  rotation = rot;
+}
diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h
index 017ddde..f9da199 100644
--- a/minui/include/minui/minui.h
+++ b/minui/include/minui/minui.h
@@ -21,23 +21,31 @@
 
 #include <functional>
 #include <string>
+#include <vector>
 
 //
 // Graphics.
 //
 
 struct GRSurface {
-    int width;
-    int height;
-    int row_bytes;
-    int pixel_bytes;
-    unsigned char* data;
+  int width;
+  int height;
+  int row_bytes;
+  int pixel_bytes;
+  unsigned char* data;
 };
 
 struct GRFont {
-    GRSurface* texture;
-    int char_width;
-    int char_height;
+  GRSurface* texture;
+  int char_width;
+  int char_height;
+};
+
+enum GRRotation {
+  ROTATION_NONE = 0,
+  ROTATION_RIGHT = 1,
+  ROTATION_DOWN = 2,
+  ROTATION_LEFT = 3,
 };
 
 int gr_init();
@@ -57,14 +65,17 @@
 
 const GRFont* gr_sys_font();
 int gr_init_font(const char* name, GRFont** dest);
-void gr_text(const GRFont* font, int x, int y, const char *s, bool bold);
-int gr_measure(const GRFont* font, const char *s);
-void gr_font_size(const GRFont* font, int *x, int *y);
+void gr_text(const GRFont* font, int x, int y, const char* s, bool bold);
+int gr_measure(const GRFont* font, const char* s);
+void gr_font_size(const GRFont* font, 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);
 
+// Set rotation, flips gr_fb_width/height if 90 degree rotation difference
+void gr_rotate(GRRotation rotation);
+
 //
 // Input events.
 //
@@ -114,8 +125,8 @@
 // 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);
+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);
@@ -129,6 +140,9 @@
 int res_create_localized_alpha_surface(const char* name, const char* locale,
                                        GRSurface** pSurface);
 
+// Return a list of locale strings embedded in |png_name|. Return a empty list in case of failure.
+std::vector<std::string> get_locales_in_png(const std::string& png_name);
+
 // Free a surface allocated by any of the res_create_*_surface()
 // functions.
 void res_free_surface(GRSurface* surface);
diff --git a/minui/resources.cpp b/minui/resources.cpp
index 8f8d36d..52ab60b 100644
--- a/minui/resources.cpp
+++ b/minui/resources.cpp
@@ -25,10 +25,12 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <memory>
 #include <regex>
 #include <string>
 #include <vector>
 
+#include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <png.h>
 
@@ -46,89 +48,126 @@
     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;
+// This class handles the png file parsing. It also holds the ownership of the png pointer and the
+// opened file pointer. Both will be destroyed/closed when this object goes out of scope.
+class PngHandler {
+ public:
+  PngHandler(const std::string& name);
 
-    snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.png", name);
-    resPath[sizeof(resPath)-1] = '\0';
-    FILE* fp = fopen(resPath, "rbe");
-    if (fp == NULL) {
-        result = -1;
-        goto exit;
-    }
+  ~PngHandler();
 
-    bytesRead = fread(header, 1, sizeof(header), fp);
-    if (bytesRead != sizeof(header)) {
-        result = -2;
-        goto exit;
-    }
+  png_uint_32 width() const {
+    return width_;
+  }
 
-    if (png_sig_cmp(header, 0, sizeof(header))) {
-        result = -3;
-        goto exit;
-    }
+  png_uint_32 height() const {
+    return height_;
+  }
 
-    *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
-    if (!*png_ptr) {
-        result = -4;
-        goto exit;
-    }
+  png_byte channels() const {
+    return channels_;
+  }
 
-    *info_ptr = png_create_info_struct(*png_ptr);
-    if (!*info_ptr) {
-        result = -5;
-        goto exit;
-    }
+  png_structp png_ptr() const {
+    return png_ptr_;
+  }
 
-    if (setjmp(png_jmpbuf(*png_ptr))) {
-        result = -6;
-        goto exit;
-    }
+  png_infop info_ptr() const {
+    return info_ptr_;
+  }
 
-    png_init_io(*png_ptr, fp);
-    png_set_sig_bytes(*png_ptr, sizeof(header));
-    png_read_info(*png_ptr, *info_ptr);
+  int error_code() const {
+    return error_code_;
+  };
 
-    png_get_IHDR(*png_ptr, *info_ptr, width, height, &bit_depth,
-            &color_type, NULL, NULL, NULL);
+  operator bool() const {
+    return error_code_ == 0;
+  }
 
-    *channels = png_get_channels(*png_ptr, *info_ptr);
+ private:
+  png_structp png_ptr_{ nullptr };
+  png_infop info_ptr_{ nullptr };
+  png_uint_32 width_;
+  png_uint_32 height_;
+  png_byte channels_;
 
-    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;
-    }
+  // The |error_code_| is set to a negative value if an error occurs when opening the png file.
+  int error_code_;
+  // After initialization, we'll keep the file pointer open before destruction of PngHandler.
+  std::unique_ptr<FILE, decltype(&fclose)> png_fp_;
+};
 
-    return result;
+PngHandler::PngHandler(const std::string& name) : error_code_(0), png_fp_(nullptr, fclose) {
+  std::string res_path = android::base::StringPrintf("/res/images/%s.png", name.c_str());
+  png_fp_.reset(fopen(res_path.c_str(), "rbe"));
+  if (!png_fp_) {
+    error_code_ = -1;
+    return;
+  }
 
-  exit:
-    if (result < 0) {
-        png_destroy_read_struct(png_ptr, info_ptr, NULL);
-    }
-    if (fp != NULL) {
-        fclose(fp);
-    }
+  unsigned char header[8];
+  size_t bytesRead = fread(header, 1, sizeof(header), png_fp_.get());
+  if (bytesRead != sizeof(header)) {
+    error_code_ = -2;
+    return;
+  }
 
-    return result;
+  if (png_sig_cmp(header, 0, sizeof(header))) {
+    error_code_ = -3;
+    return;
+  }
+
+  png_ptr_ = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+  if (!png_ptr_) {
+    error_code_ = -4;
+    return;
+  }
+
+  info_ptr_ = png_create_info_struct(png_ptr_);
+  if (!info_ptr_) {
+    error_code_ = -5;
+    return;
+  }
+
+  if (setjmp(png_jmpbuf(png_ptr_))) {
+    error_code_ = -6;
+    return;
+  }
+
+  png_init_io(png_ptr_, png_fp_.get());
+  png_set_sig_bytes(png_ptr_, sizeof(header));
+  png_read_info(png_ptr_, info_ptr_);
+
+  int color_type;
+  int bit_depth;
+  png_get_IHDR(png_ptr_, info_ptr_, &width_, &height_, &bit_depth, &color_type, nullptr, nullptr,
+               nullptr);
+
+  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);
+    error_code_ = -7;
+  }
+}
+
+PngHandler::~PngHandler() {
+  if (png_ptr_) {
+    png_destroy_read_struct(&png_ptr_, &info_ptr_, nullptr);
+  }
 }
 
 // "display" surfaces are transformed into the framebuffer's required
@@ -198,178 +237,152 @@
 }
 
 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 = nullptr;
 
-    *pSurface = NULL;
+  PngHandler png_handler(name);
+  if (!png_handler) return png_handler.error_code();
 
-    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
-    if (result < 0) return result;
+  png_structp png_ptr = png_handler.png_ptr();
+  png_uint_32 width = png_handler.width();
+  png_uint_32 height = png_handler.height();
 
-    surface = init_display_surface(width, height);
-    if (surface == NULL) {
-        result = -8;
-        goto exit;
-    }
+  GRSurface* surface = init_display_surface(width, height);
+  if (!surface) {
+    return -8;
+  }
 
 #if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-    png_set_bgr(png_ptr);
+  png_set_bgr(png_ptr);
 #endif
 
-    p_row = static_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);
+  for (png_uint_32 y = 0; y < height; ++y) {
+    std::vector<unsigned char> p_row(width * 4);
+    png_read_row(png_ptr, p_row.data(), nullptr);
+    transform_rgb_to_draw(p_row.data(), surface->data + y * surface->row_bytes,
+                          png_handler.channels(), width);
+  }
 
-    *pSurface = surface;
+  *pSurface = surface;
 
-  exit:
-    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
-    if (result < 0 && surface != NULL) free(surface);
-    return result;
+  return 0;
 }
 
 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;
+                                     GRSurface*** pSurface) {
+  *pSurface = nullptr;
+  *frames = -1;
 
-    *pSurface = NULL;
-    *frames = -1;
+  PngHandler png_handler(name);
+  if (!png_handler) return png_handler.error_code();
 
-    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
-    if (result < 0) return result;
+  png_structp png_ptr = png_handler.png_ptr();
+  png_uint_32 width = png_handler.width();
+  png_uint_32 height = png_handler.height();
 
-    *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);
+  *frames = 1;
+  *fps = 20;
+  png_textp text;
+  int num_text;
+  if (png_get_text(png_ptr, png_handler.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;
-    }
+  int result = 0;
+  GRSurface** surface = nullptr;
+  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;
-    }
+  if (height % *frames != 0) {
+    printf("bad height (%d) for frame count (%d)\n", height, *frames);
+    result = -9;
+    goto exit;
+  }
 
-    surface = static_cast<GRSurface**>(calloc(*frames, sizeof(GRSurface*)));
-    if (surface == NULL) {
-        result = -8;
-        goto exit;
+  surface = static_cast<GRSurface**>(calloc(*frames, sizeof(GRSurface*)));
+  if (!surface) {
+    result = -8;
+    goto exit;
+  }
+  for (int i = 0; i < *frames; ++i) {
+    surface[i] = init_display_surface(width, height / *frames);
+    if (!surface[i]) {
+      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);
+  png_set_bgr(png_ptr);
 #endif
 
-    p_row = static_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);
+  for (png_uint_32 y = 0; y < height; ++y) {
+    std::vector<unsigned char> p_row(width * 4);
+    png_read_row(png_ptr, p_row.data(), nullptr);
+    int frame = y % *frames;
+    unsigned char* out_row = surface[frame]->data + (y / *frames) * surface[frame]->row_bytes;
+    transform_rgb_to_draw(p_row.data(), out_row, png_handler.channels(), width);
+  }
 
-    *pSurface = surface;
+  *pSurface = 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);
-        }
+  if (result < 0) {
+    if (surface) {
+      for (int i = 0; i < *frames; ++i) {
+        free(surface[i]);
+      }
+      free(surface);
     }
-    return result;
+  }
+  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 = nullptr;
 
-    *pSurface = NULL;
+  PngHandler png_handler(name);
+  if (!png_handler) return png_handler.error_code();
 
-    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
-    if (result < 0) return result;
+  if (png_handler.channels() != 1) {
+    return -7;
+  }
 
-    if (channels != 1) {
-        result = -7;
-        goto exit;
-    }
+  png_structp png_ptr = png_handler.png_ptr();
+  png_uint_32 width = png_handler.width();
+  png_uint_32 height = png_handler.height();
 
-    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;
+  GRSurface* surface = malloc_surface(width * height);
+  if (!surface) {
+    return -8;
+  }
+  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);
+  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);
-    }
+  for (png_uint_32 y = 0; y < height; ++y) {
+    unsigned char* p_row = surface->data + y * surface->row_bytes;
+    png_read_row(png_ptr, p_row, nullptr);
+  }
 
-    *pSurface = surface;
+  *pSurface = surface;
 
-  exit:
-    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
-    if (result < 0 && surface != NULL) free(surface);
-    return result;
+  return 0;
 }
 
 // This function tests if a locale string stored in PNG (prefix) matches
@@ -384,7 +397,7 @@
   // match the locale string without the {script} section.
   // For instance, prefix == "en" matches locale == "en-US", prefix == "sr-Latn" matches locale
   // == "sr-Latn-BA", and prefix == "zh-CN" matches locale == "zh-Hans-CN".
-  if (android::base::StartsWith(locale, prefix.c_str())) {
+  if (android::base::StartsWith(locale, prefix)) {
     return true;
   }
 
@@ -396,75 +409,90 @@
   return std::regex_match(locale, loc_regex);
 }
 
+std::vector<std::string> get_locales_in_png(const std::string& png_name) {
+  PngHandler png_handler(png_name);
+  if (!png_handler) {
+    printf("Failed to open %s, error: %d\n", png_name.c_str(), png_handler.error_code());
+    return {};
+  }
+  if (png_handler.channels() != 1) {
+    printf("Expect input png to have 1 data channel, this file has %d\n", png_handler.channels());
+    return {};
+  }
+
+  std::vector<std::string> result;
+  std::vector<unsigned char> row(png_handler.width());
+  for (png_uint_32 y = 0; y < png_handler.height(); ++y) {
+    png_read_row(png_handler.png_ptr(), row.data(), nullptr);
+    int h = (row[3] << 8) | row[2];
+    std::string loc(reinterpret_cast<char*>(&row[5]));
+    if (!loc.empty()) {
+      result.push_back(loc);
+    }
+    for (int i = 0; i < h; ++i, ++y) {
+      png_read_row(png_handler.png_ptr(), row.data(), nullptr);
+    }
+  }
+
+  return result;
+}
+
 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 = nullptr;
+  if (locale == nullptr) {
+    return 0;
+  }
 
-    *pSurface = NULL;
+  PngHandler png_handler(name);
+  if (!png_handler) return png_handler.error_code();
 
-    if (locale == NULL) {
-        return result;
+  if (png_handler.channels() != 1) {
+    return -7;
+  }
+
+  png_structp png_ptr = png_handler.png_ptr();
+  png_uint_32 width = png_handler.width();
+  png_uint_32 height = png_handler.height();
+
+  for (png_uint_32 y = 0; y < height; ++y) {
+    std::vector<unsigned char> row(width);
+    png_read_row(png_ptr, row.data(), nullptr);
+    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);
+
+      GRSurface* surface = malloc_surface(w * h);
+      if (!surface) {
+        return -8;
+      }
+      surface->width = w;
+      surface->height = h;
+      surface->row_bytes = w;
+      surface->pixel_bytes = 1;
+
+      for (int i = 0; i < h; ++i, ++y) {
+        png_read_row(png_ptr, row.data(), nullptr);
+        memcpy(surface->data + i * w, row.data(), w);
+      }
+
+      *pSurface = surface;
+      break;
     }
 
-    result = open_png(name, &png_ptr, &info_ptr, &width, &height, &channels);
-    if (result < 0) return result;
-
-    if (channels != 1) {
-        result = -7;
-        goto exit;
+    for (int i = 0; i < h; ++i, ++y) {
+      png_read_row(png_ptr, row.data(), nullptr);
     }
+  }
 
-    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 = 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;
+  return 0;
 }
 
 void res_free_surface(GRSurface* surface) {
-    free(surface);
+  free(surface);
 }
diff --git a/otafault/Android.bp b/otafault/Android.bp
new file mode 100644
index 0000000..b39d5be
--- /dev/null
+++ b/otafault/Android.bp
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 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.
+
+cc_library_static {
+    name: "libotafault",
+
+    host_supported: true,
+
+    srcs: [
+        "config.cpp",
+        "ota_io.cpp",
+    ],
+
+    static_libs: [
+        "libbase",
+        "liblog",
+        "libziparchive",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    cflags: [
+        "-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS",
+        "-Wall",
+        "-Werror",
+        "-Wthread-safety",
+        "-Wthread-safety-negative",
+    ],
+
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
+
+cc_test {
+    name: "otafault_test",
+
+    srcs: ["test.cpp"],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+
+    static_executable: true,
+
+    static_libs: [
+        "libotafault",
+        "libziparchive",
+        "libbase",
+        "liblog",
+    ],
+}
diff --git a/otafault/Android.mk b/otafault/Android.mk
deleted file mode 100644
index ec4cdb3..0000000
--- a/otafault/Android.mk
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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)
-
-include $(CLEAR_VARS)
-
-otafault_static_libs := \
-    libziparchive \
-    libz \
-    libselinux \
-    libbase \
-    liblog
-
-LOCAL_CFLAGS := \
-    -Werror \
-    -Wthread-safety \
-    -Wthread-safety-negative \
-    -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS
-
-LOCAL_SRC_FILES := config.cpp ota_io.cpp
-LOCAL_MODULE_TAGS := eng
-LOCAL_MODULE := libotafault
-LOCAL_CLANG := true
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)
-LOCAL_WHOLE_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 := $(otafault_static_libs)
-LOCAL_CFLAGS := -Werror
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_FORCE_STATIC_EXECUTABLE := true
-
-include $(BUILD_EXECUTABLE)
diff --git a/otafault/config.cpp b/otafault/config.cpp
index 8590833..3993948 100644
--- a/otafault/config.cpp
+++ b/otafault/config.cpp
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
+#include "otafault/config.h"
+
 #include <map>
 #include <string>
 
-#include <stdio.h>
-#include <unistd.h>
-
 #include <android-base/stringprintf.h>
 #include <ziparchive/zip_archive.h>
 
-#include "config.h"
-#include "ota_io.h"
+#include "otafault/ota_io.h"
 
 #define OTAIO_MAX_FNAME_SIZE 128
 
@@ -69,7 +67,9 @@
     fname.resize(OTAIO_MAX_FNAME_SIZE);
     ZipString zip_type_path(type_path.c_str());
     ZipEntry entry;
-    int status = FindEntry(archive, zip_type_path, &entry);
+    if (FindEntry(archive, zip_type_path, &entry) != 0) {
+        return {};
+    }
     ExtractToMemory(archive, &entry, reinterpret_cast<uint8_t*>(&fname[0]), OTAIO_MAX_FNAME_SIZE);
     return fname;
 }
diff --git a/otafault/config.h b/otafault/include/otafault/config.h
similarity index 76%
rename from otafault/config.h
rename to otafault/include/otafault/config.h
index 4adbdd1..cc4bfd2 100644
--- a/otafault/config.h
+++ b/otafault/include/otafault/config.h
@@ -15,13 +15,13 @@
  */
 
 /*
- * Read configuration files in the OTA package to determine which files, if any, will trigger errors.
+ * 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.
+ * 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
@@ -29,9 +29,9 @@
  *   .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.
+ * 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_
@@ -39,8 +39,6 @@
 
 #include <string>
 
-#include <stdbool.h>
-
 #include <ziparchive/zip_archive.h>
 
 #define OTAIO_BASE_DIR ".libotafault"
diff --git a/otafault/ota_io.h b/otafault/include/otafault/ota_io.h
similarity index 96%
rename from otafault/ota_io.h
rename to otafault/include/otafault/ota_io.h
index 9428f1b..45e481a 100644
--- a/otafault/ota_io.h
+++ b/otafault/include/otafault/ota_io.h
@@ -23,8 +23,9 @@
 #ifndef _UPDATER_OTA_IO_H_
 #define _UPDATER_OTA_IO_H_
 
+#include <stddef.h>
 #include <stdio.h>
-#include <sys/stat.h>
+#include <sys/stat.h>  // mode_t
 
 #include <memory>
 
diff --git a/otafault/ota_io.cpp b/otafault/ota_io.cpp
index faae527..63ef18e 100644
--- a/otafault/ota_io.cpp
+++ b/otafault/ota_io.cpp
@@ -14,20 +14,23 @@
  * limitations under the License.
  */
 
-#include "ota_io.h"
+#include "otafault/ota_io.h"
 
 #include <errno.h>
 #include <fcntl.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <sys/stat.h>
+#include <sys/types.h>
 #include <unistd.h>
 
 #include <map>
-#include <memory>
 #include <mutex>
+#include <string>
 
 #include <android-base/thread_annotations.h>
-#include "config.h"
+
+#include "otafault/config.h"
 
 static std::mutex filename_mutex;
 static std::map<intptr_t, const char*> filename_cache GUARDED_BY(filename_mutex);
diff --git a/otafault/test.cpp b/otafault/test.cpp
index 6514782..63e2445 100644
--- a/otafault/test.cpp
+++ b/otafault/test.cpp
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-#include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 #include <unistd.h>
 
-#include "ota_io.h"
+#include "otafault/ota_io.h"
 
 int main(int /* argc */, char** /* argv */) {
     int fd = open("testdata/test.file", O_RDWR);
diff --git a/otautil/Android.bp b/otautil/Android.bp
index a2eaa04..75cf691 100644
--- a/otautil/Android.bp
+++ b/otautil/Android.bp
@@ -15,10 +15,14 @@
 cc_library_static {
     name: "libotautil",
 
+    host_supported: true,
+
     srcs: [
         "SysUtil.cpp",
         "DirUtil.cpp",
         "ThermalUtil.cpp",
+        "cache_location.cpp",
+        "rangeset.cpp",
     ],
 
     static_libs: [
@@ -27,7 +31,12 @@
     ],
 
     cflags: [
+        "-D_FILE_OFFSET_BITS=64",
         "-Werror",
         "-Wall",
     ],
+
+    export_include_dirs: [
+        "include",
+    ],
 }
diff --git a/otautil/DirUtil.cpp b/otautil/DirUtil.cpp
index e08e360..61c8328 100644
--- a/otautil/DirUtil.cpp
+++ b/otautil/DirUtil.cpp
@@ -14,205 +14,103 @@
  * limitations under the License.
  */
 
-#include "DirUtil.h"
+#include "otautil/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 <errno.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
 
 #include <string>
 
 #include <selinux/label.h>
 #include <selinux/selinux.h>
 
-typedef enum { DMISSING, DDIR, DILLEGAL } DirStatus;
+enum class DirStatus { DMISSING, DDIR, DILLEGAL };
 
-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;
+static DirStatus dir_status(const std::string& path) {
+  struct stat sb;
+  if (stat(path.c_str(), &sb) == 0) {
+    // Something's there; make sure it's a directory.
+    if (S_ISDIR(sb.st_mode)) {
+      return DirStatus::DDIR;
     }
-    return DMISSING;
+    errno = ENOTDIR;
+    return DirStatus::DILLEGAL;
+  } else if (errno != ENOENT) {
+    // Something went wrong, or something in the path is bad. Can't do anything in this situation.
+    return DirStatus::DILLEGAL;
+  }
+  return DirStatus::DMISSING;
 }
 
-int
-dirCreateHierarchy(const char *path, int mode,
-        const struct utimbuf *timestamp, bool stripFileName,
-        struct selabel_handle *sehnd)
-{
-    DirStatus ds;
+int mkdir_recursively(const std::string& input_path, mode_t mode, bool strip_filename,
+                      const selabel_handle* sehnd) {
+  // Check for an empty string before we bother making any syscalls.
+  if (input_path.empty()) {
+    errno = ENOENT;
+    return -1;
+  }
 
-    /* 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 path = input_path;
+  if (strip_filename) {
+    // Strip everything after the last slash.
+    size_t pos = path.rfind('/');
+    if (pos == std::string::npos) {
+      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('/');
-    }
+    path.resize(pos + 1);
+  } else {
+    // Make sure that the path ends in a slash.
+    path.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 = '/';
-    }
+  // See if it already exists.
+  DirStatus ds = dir_status(path);
+  if (ds == DirStatus::DDIR) {
     return 0;
-}
+  } else if (ds == DirStatus::DILLEGAL) {
+    return -1;
+  }
 
-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) {
+  // Walk up the path from the root and make each level.
+  size_t prev_end = 0;
+  while (prev_end < path.size()) {
+    size_t next_end = path.find('/', prev_end + 1);
+    if (next_end == std::string::npos) {
+      break;
+    }
+    std::string dir_path = path.substr(0, next_end);
+    // Check this part of the path and make a new directory if necessary.
+    switch (dir_status(dir_path)) {
+      case DirStatus::DILLEGAL:
+        // Could happen if some other process/thread is messing with the filesystem.
         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;
+      case DirStatus::DMISSING: {
+        char* secontext = nullptr;
+        if (sehnd) {
+          selabel_lookup(const_cast<selabel_handle*>(sehnd), &secontext, dir_path.c_str(), mode);
+          setfscreatecon(secontext);
         }
-        snprintf(dn, sizeof(dn), "%s/%s", path, de->d_name);
-        if (dirUnlinkHierarchy(dn) < 0) {
-            fail = 1;
-            break;
+        int err = mkdir(dir_path.c_str(), mode);
+        if (secontext) {
+          freecon(secontext);
+          setfscreatecon(nullptr);
         }
-        errno = 0;
+        if (err != 0) {
+          return -1;
+        }
+        break;
+      }
+      default:
+        // Already exists.
+        break;
     }
-    /* 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);
+    prev_end = next_end;
+  }
+  return 0;
 }
diff --git a/otautil/DirUtil.h b/otautil/DirUtil.h
deleted file mode 100644
index 85b83c3..0000000
--- a/otautil/DirUtil.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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/otautil/SysUtil.cpp b/otautil/SysUtil.cpp
index dfa2150..48336ad 100644
--- a/otautil/SysUtil.cpp
+++ b/otautil/SysUtil.cpp
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-#include "SysUtil.h"
+#include "otautil/SysUtil.h"
 
+#include <errno.h>  // TEMP_FAILURE_RETRY
 #include <fcntl.h>
 #include <stdint.h>  // SIZE_MAX
 #include <sys/mman.h>
@@ -100,7 +101,7 @@
   }
 
   // Reserve enough contiguous address space for the whole file.
-  void* reserve = mmap64(nullptr, blocks * blksize, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
+  void* reserve = mmap(nullptr, blocks * blksize, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
   if (reserve == MAP_FAILED) {
     PLOG(ERROR) << "failed to reserve address space";
     return false;
@@ -135,8 +136,8 @@
       break;
     }
 
-    void* range_start = mmap64(next, range_size, PROT_READ, MAP_PRIVATE | MAP_FIXED, fd,
-                               static_cast<off64_t>(start) * blksize);
+    void* range_start = mmap(next, range_size, PROT_READ, MAP_PRIVATE | MAP_FIXED, fd,
+                             static_cast<off_t>(start) * blksize);
     if (range_start == MAP_FAILED) {
       PLOG(ERROR) << "failed to map range " << i << ": " << line;
       success = false;
diff --git a/otautil/ThermalUtil.cpp b/otautil/ThermalUtil.cpp
index 13d3643..5d9bd45 100644
--- a/otautil/ThermalUtil.cpp
+++ b/otautil/ThermalUtil.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "ThermalUtil.h"
+#include "otautil/ThermalUtil.h"
 
 #include <dirent.h>
 #include <stdio.h>
@@ -77,4 +77,4 @@
   }
   LOG(INFO) << "current maximum temperature: " << max_temperature;
   return max_temperature;
-}
\ No newline at end of file
+}
diff --git a/otautil/cache_location.cpp b/otautil/cache_location.cpp
new file mode 100644
index 0000000..8ddefec
--- /dev/null
+++ b/otautil/cache_location.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "otautil/cache_location.h"
+
+constexpr const char kDefaultCacheTempSource[] = "/cache/saved.file";
+constexpr const char kDefaultLastCommandFile[] = "/cache/recovery/last_command";
+constexpr const char kDefaultStashDirectoryBase[] = "/cache/recovery";
+
+CacheLocation& CacheLocation::location() {
+  static CacheLocation cache_location;
+  return cache_location;
+}
+
+CacheLocation::CacheLocation()
+    : cache_temp_source_(kDefaultCacheTempSource),
+      last_command_file_(kDefaultLastCommandFile),
+      stash_directory_base_(kDefaultStashDirectoryBase) {}
diff --git a/otautil/include/otautil/DirUtil.h b/otautil/include/otautil/DirUtil.h
new file mode 100644
index 0000000..85d6c16
--- /dev/null
+++ b/otautil/include/otautil/DirUtil.h
@@ -0,0 +1,39 @@
+/*
+ * 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 OTAUTIL_DIRUTIL_H_
+#define OTAUTIL_DIRUTIL_H_
+
+#include <sys/stat.h>  // mode_t
+
+#include <string>
+
+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 strip_filename is set, everything after the final '/' is stripped before creating the
+// directory
+// hierarchy.
+//
+// Returns 0 on success; returns -1 (and sets errno) on failure (usually if some element of path is
+// not a directory).
+int mkdir_recursively(const std::string& path, mode_t mode, bool strip_filename,
+                      const struct selabel_handle* sehnd);
+
+#endif  // OTAUTIL_DIRUTIL_H_
diff --git a/otautil/SysUtil.h b/otautil/include/otautil/SysUtil.h
similarity index 100%
rename from otautil/SysUtil.h
rename to otautil/include/otautil/SysUtil.h
diff --git a/otautil/ThermalUtil.h b/otautil/include/otautil/ThermalUtil.h
similarity index 100%
rename from otautil/ThermalUtil.h
rename to otautil/include/otautil/ThermalUtil.h
diff --git a/otautil/include/otautil/cache_location.h b/otautil/include/otautil/cache_location.h
new file mode 100644
index 0000000..f2f6638
--- /dev/null
+++ b/otautil/include/otautil/cache_location.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
+#define _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
+
+#include <string>
+
+#include "android-base/macros.h"
+
+// A singleton class to maintain the update related locations. The locations should be only set
+// once at the start of the program.
+class CacheLocation {
+ public:
+  static CacheLocation& location();
+
+  // getter and setter functions.
+  std::string cache_temp_source() const {
+    return cache_temp_source_;
+  }
+  void set_cache_temp_source(const std::string& temp_source) {
+    cache_temp_source_ = temp_source;
+  }
+
+  std::string last_command_file() const {
+    return last_command_file_;
+  }
+  void set_last_command_file(const std::string& last_command) {
+    last_command_file_ = last_command;
+  }
+
+  std::string stash_directory_base() const {
+    return stash_directory_base_;
+  }
+  void set_stash_directory_base(const std::string& base) {
+    stash_directory_base_ = base;
+  }
+
+ private:
+  CacheLocation();
+  DISALLOW_COPY_AND_ASSIGN(CacheLocation);
+
+  // 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 the cached file contains the bits we want and use it
+  // as the source instead.  The default location for the cached source is "/cache/saved.file".
+  std::string cache_temp_source_;
+
+  // Location to save the last command that stashes blocks.
+  std::string last_command_file_;
+
+  // The base directory to write stashes during update.
+  std::string stash_directory_base_;
+};
+
+#endif  // _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
diff --git a/error_code.h b/otautil/include/otautil/error_code.h
similarity index 87%
rename from error_code.h
rename to otautil/include/otautil/error_code.h
index 9fe047c..b0ff42d 100644
--- a/error_code.h
+++ b/otautil/include/otautil/error_code.h
@@ -17,7 +17,7 @@
 #ifndef _ERROR_CODE_H_
 #define _ERROR_CODE_H_
 
-enum ErrorCode {
+enum ErrorCode : int {
   kNoError = -1,
   kLowBattery = 20,
   kZipVerificationFailure,
@@ -25,9 +25,12 @@
   kBootreasonInBlacklist,
   kPackageCompatibilityFailure,
   kScriptExecutionFailure,
+  kMapFileFailure,
+  kForkUpdateBinaryFailure,
+  kUpdateBinaryCommandFailure,
 };
 
-enum CauseCode {
+enum CauseCode : int {
   kNoCause = -1,
   kArgsParsingFailure = 100,
   kStashCreationFailure,
@@ -48,7 +51,7 @@
   kVendorFailure = 200
 };
 
-enum UncryptErrorCode {
+enum UncryptErrorCode : int {
   kUncryptNoError = -1,
   kUncryptErrorPlaceholder = 50,
   kUncryptTimeoutError = 100,
@@ -68,6 +71,8 @@
   kUncryptFileCloseError,
   kUncryptFileRenameError,
   kUncryptPackageMissingError,
+  kUncryptRealpathFindError,
+  kUncryptBlockDeviceFindError,
 };
 
-#endif // _ERROR_CODE_H_
+#endif  // _ERROR_CODE_H_
diff --git a/otautil/include/otautil/print_sha1.h b/otautil/include/otautil/print_sha1.h
new file mode 100644
index 0000000..03a8d29
--- /dev/null
+++ b/otautil/include/otautil/print_sha1.h
@@ -0,0 +1,47 @@
+/*
+ * 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, 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;
+}
+
+[[maybe_unused]] static std::string print_sha1(const uint8_t sha1[SHA_DIGEST_LENGTH]) {
+  return print_sha1(sha1, SHA_DIGEST_LENGTH);
+}
+
+[[maybe_unused]] static std::string short_sha1(const uint8_t sha1[SHA_DIGEST_LENGTH]) {
+  return print_sha1(sha1, 4);
+}
+
+[[maybe_unused]] static std::string print_hex(const uint8_t* bytes, size_t len) {
+  return print_sha1(bytes, len);
+}
+
+#endif  // RECOVERY_PRINT_SHA1_H
diff --git a/otautil/include/otautil/rangeset.h b/otautil/include/otautil/rangeset.h
new file mode 100644
index 0000000..e91d02c
--- /dev/null
+++ b/otautil/include/otautil/rangeset.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#pragma once
+
+#include <stddef.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+using Range = std::pair<size_t, size_t>;
+
+class RangeSet {
+ public:
+  RangeSet() : blocks_(0) {}
+
+  explicit RangeSet(std::vector<Range>&& pairs);
+
+  // Parses the given string into a RangeSet. Returns the parsed RangeSet, or an empty RangeSet on
+  // errors.
+  static RangeSet Parse(const std::string& range_text);
+
+  // Appends the given Range to the current RangeSet.
+  bool PushBack(Range range);
+
+  // Clears all the ranges from the RangeSet.
+  void Clear();
+
+  std::string ToString() const;
+
+  // Gets the block number for the i-th (starting from 0) block in the RangeSet.
+  size_t GetBlockNumber(size_t idx) const;
+
+  // Returns whether the current RangeSet overlaps with other. RangeSet has half-closed half-open
+  // bounds. For example, "3,5" contains blocks 3 and 4. So "3,5" and "5,7" are not overlapped.
+  bool Overlaps(const RangeSet& other) const;
+
+  // Returns a vector of RangeSets that contain the same set of blocks represented by the current
+  // RangeSet. The RangeSets in the vector contain similar number of blocks, with a maximum delta
+  // of 1-block between any two of them. For example, 14 blocks would be split into 4 + 4 + 3 + 3,
+  // as opposed to 4 + 4 + 4 + 2. If the total number of blocks (T) is less than groups, it
+  // returns a vector of T 1-block RangeSets. Otherwise the number of the returned RangeSets must
+  // equal to groups. The current RangeSet remains intact after the split.
+  std::vector<RangeSet> Split(size_t groups) const;
+
+  // Returns the number of Range's in this RangeSet.
+  size_t size() const {
+    return ranges_.size();
+  }
+
+  // Returns the total number of blocks in this RangeSet.
+  size_t blocks() const {
+    return blocks_;
+  }
+
+  std::vector<Range>::const_iterator cbegin() const {
+    return ranges_.cbegin();
+  }
+
+  std::vector<Range>::const_iterator cend() const {
+    return ranges_.cend();
+  }
+
+  std::vector<Range>::iterator begin() {
+    return ranges_.begin();
+  }
+
+  std::vector<Range>::iterator end() {
+    return ranges_.end();
+  }
+
+  std::vector<Range>::const_iterator begin() const {
+    return ranges_.begin();
+  }
+
+  std::vector<Range>::const_iterator end() const {
+    return ranges_.end();
+  }
+
+  // Reverse const iterators for MoveRange().
+  std::vector<Range>::const_reverse_iterator crbegin() const {
+    return ranges_.crbegin();
+  }
+
+  std::vector<Range>::const_reverse_iterator crend() const {
+    return ranges_.crend();
+  }
+
+  // Returns whether the RangeSet is valid (i.e. non-empty).
+  explicit operator bool() const {
+    return !ranges_.empty();
+  }
+
+  const Range& operator[](size_t i) const {
+    return ranges_[i];
+  }
+
+  bool operator==(const RangeSet& other) const {
+    // The orders of Range's matter. "4,1,5,8,10" != "4,8,10,1,5".
+    return (ranges_ == other.ranges_);
+  }
+
+  bool operator!=(const RangeSet& other) const {
+    return ranges_ != other.ranges_;
+  }
+
+ protected:
+  // Actual limit for each value and the total number are both INT_MAX.
+  std::vector<Range> ranges_;
+  size_t blocks_;
+};
+
+// The class is a sorted version of a RangeSet; and it's useful in imgdiff to split the input
+// files when we're handling large zip files. Specifically, we can treat the input file as a
+// continuous RangeSet (i.e. RangeSet("0-99") for a 100 blocks file); and break it down into
+// several smaller chunks based on the zip entries.
+
+// For example, [source: 0-99] can be split into
+// [split_src1: 10-29]; [split_src2: 40-49, 60-69]; [split_src3: 70-89]
+// Here "10-29" simply means block 10th to block 29th with respect to the original input file.
+// Also, note that the split sources should be mutual exclusive, but they don't need to cover
+// every block in the original source.
+class SortedRangeSet : public RangeSet {
+ public:
+  // The block size when working with offset and file length.
+  static constexpr size_t kBlockSize = 4096;
+
+  SortedRangeSet() {}
+
+  // Ranges in the the set should be mutually exclusive; and they're sorted by the start block.
+  explicit SortedRangeSet(std::vector<Range>&& pairs);
+
+  void Insert(const Range& to_insert);
+
+  // Insert the input SortedRangeSet; keep the ranges sorted and merge the overlap ranges.
+  void Insert(const SortedRangeSet& rs);
+
+  // Compute the block range the file occupies, and insert that range.
+  void Insert(size_t start, size_t len);
+
+  using RangeSet::Overlaps;
+
+  bool Overlaps(size_t start, size_t len) const;
+
+  // Given an offset of the file, checks if the corresponding block (by considering the file as
+  // 0-based continuous block ranges) is covered by the SortedRangeSet. If so, returns the offset
+  // within this SortedRangeSet.
+  //
+  // For example, the 4106-th byte of a file is from block 1, assuming a block size of 4096-byte.
+  // The mapped offset within a SortedRangeSet("1-9 15-19") is 10.
+  //
+  // An offset of 65546 falls into the 16-th block in a file. Block 16 is contained as the 10-th
+  // item in SortedRangeSet("1-9 15-19"). So its data can be found at offset 40970 (i.e. 4096 * 10
+  // + 10) in a range represented by this SortedRangeSet.
+  size_t GetOffsetInRangeSet(size_t old_offset) const;
+};
diff --git a/otautil/rangeset.cpp b/otautil/rangeset.cpp
new file mode 100644
index 0000000..96955b9
--- /dev/null
+++ b/otautil/rangeset.cpp
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2017 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 "otautil/rangeset.h"
+
+#include <limits.h>
+#include <stddef.h>
+
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+RangeSet::RangeSet(std::vector<Range>&& pairs) {
+  blocks_ = 0;
+  if (pairs.empty()) {
+    LOG(ERROR) << "Invalid number of tokens";
+    return;
+  }
+
+  for (const auto& range : pairs) {
+    if (!PushBack(range)) {
+      Clear();
+      return;
+    }
+  }
+}
+
+RangeSet RangeSet::Parse(const std::string& range_text) {
+  std::vector<std::string> pieces = android::base::Split(range_text, ",");
+  if (pieces.size() < 3) {
+    LOG(ERROR) << "Invalid range text: " << range_text;
+    return {};
+  }
+
+  size_t num;
+  if (!android::base::ParseUint(pieces[0], &num, static_cast<size_t>(INT_MAX))) {
+    LOG(ERROR) << "Failed to parse the number of tokens: " << range_text;
+    return {};
+  }
+  if (num == 0) {
+    LOG(ERROR) << "Invalid number of tokens: " << range_text;
+    return {};
+  }
+  if (num % 2 != 0) {
+    LOG(ERROR) << "Number of tokens must be even: " << range_text;
+    return {};
+  }
+  if (num != pieces.size() - 1) {
+    LOG(ERROR) << "Mismatching number of tokens: " << range_text;
+    return {};
+  }
+
+  std::vector<Range> pairs;
+  for (size_t i = 0; i < num; i += 2) {
+    size_t first;
+    size_t second;
+    if (!android::base::ParseUint(pieces[i + 1], &first, static_cast<size_t>(INT_MAX)) ||
+        !android::base::ParseUint(pieces[i + 2], &second, static_cast<size_t>(INT_MAX))) {
+      return {};
+    }
+    pairs.emplace_back(first, second);
+  }
+  return RangeSet(std::move(pairs));
+}
+
+bool RangeSet::PushBack(Range range) {
+  if (range.first >= range.second) {
+    LOG(ERROR) << "Empty or negative range: " << range.first << ", " << range.second;
+    return false;
+  }
+  size_t sz = range.second - range.first;
+  if (blocks_ >= SIZE_MAX - sz) {
+    LOG(ERROR) << "RangeSet size overflow";
+    return false;
+  }
+
+  ranges_.push_back(std::move(range));
+  blocks_ += sz;
+  return true;
+}
+
+void RangeSet::Clear() {
+  ranges_.clear();
+  blocks_ = 0;
+}
+
+std::vector<RangeSet> RangeSet::Split(size_t groups) const {
+  if (ranges_.empty() || groups == 0) return {};
+
+  if (blocks_ < groups) {
+    groups = blocks_;
+  }
+
+  // Evenly distribute blocks, with the first few groups possibly containing one more.
+  size_t mean = blocks_ / groups;
+  std::vector<size_t> blocks_per_group(groups, mean);
+  std::fill_n(blocks_per_group.begin(), blocks_ % groups, mean + 1);
+
+  std::vector<RangeSet> result;
+
+  // Forward iterate Ranges and fill up each group with the desired number of blocks.
+  auto it = ranges_.cbegin();
+  Range range = *it;
+  for (const auto& blocks : blocks_per_group) {
+    RangeSet buffer;
+    size_t needed = blocks;
+    while (needed > 0) {
+      size_t range_blocks = range.second - range.first;
+      if (range_blocks > needed) {
+        // Split the current range and don't advance the iterator.
+        buffer.PushBack({ range.first, range.first + needed });
+        range.first = range.first + needed;
+        break;
+      }
+      buffer.PushBack(range);
+      it++;
+      if (it != ranges_.cend()) {
+        range = *it;
+      }
+      needed -= range_blocks;
+    }
+    result.push_back(std::move(buffer));
+  }
+  return result;
+}
+
+std::string RangeSet::ToString() const {
+  if (ranges_.empty()) {
+    return "";
+  }
+  std::string result = std::to_string(ranges_.size() * 2);
+  for (const auto& r : ranges_) {
+    result += android::base::StringPrintf(",%zu,%zu", r.first, r.second);
+  }
+
+  return result;
+}
+
+// Get the block number for the i-th (starting from 0) block in the RangeSet.
+size_t RangeSet::GetBlockNumber(size_t idx) const {
+  CHECK_LT(idx, blocks_) << "Out of bound index " << idx << " (total blocks: " << blocks_ << ")";
+
+  for (const auto& range : ranges_) {
+    if (idx < range.second - range.first) {
+      return range.first + idx;
+    }
+    idx -= (range.second - range.first);
+  }
+
+  CHECK(false) << "Failed to find block number for index " << idx;
+  return 0;  // Unreachable, but to make compiler happy.
+}
+
+// RangeSet has half-closed half-open bounds. For example, "3,5" contains blocks 3 and 4. So "3,5"
+// and "5,7" are not overlapped.
+bool RangeSet::Overlaps(const RangeSet& other) const {
+  for (const auto& range : ranges_) {
+    size_t start = range.first;
+    size_t end = range.second;
+    for (const auto& other_range : other.ranges_) {
+      size_t other_start = other_range.first;
+      size_t other_end = other_range.second;
+      // [start, end) vs [other_start, other_end)
+      if (!(other_start >= end || start >= other_end)) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+// Ranges in the the set should be mutually exclusive; and they're sorted by the start block.
+SortedRangeSet::SortedRangeSet(std::vector<Range>&& pairs) : RangeSet(std::move(pairs)) {
+  std::sort(ranges_.begin(), ranges_.end());
+}
+
+void SortedRangeSet::Insert(const Range& to_insert) {
+  SortedRangeSet rs({ to_insert });
+  Insert(rs);
+}
+
+// Insert the input SortedRangeSet; keep the ranges sorted and merge the overlap ranges.
+void SortedRangeSet::Insert(const SortedRangeSet& rs) {
+  if (rs.size() == 0) {
+    return;
+  }
+  // Merge and sort the two RangeSets.
+  std::vector<Range> temp = std::move(ranges_);
+  std::copy(rs.begin(), rs.end(), std::back_inserter(temp));
+  std::sort(temp.begin(), temp.end());
+
+  Clear();
+  // Trim overlaps and insert the result back to ranges_.
+  Range to_insert = temp.front();
+  for (auto it = temp.cbegin() + 1; it != temp.cend(); it++) {
+    if (it->first <= to_insert.second) {
+      to_insert.second = std::max(to_insert.second, it->second);
+    } else {
+      ranges_.push_back(to_insert);
+      blocks_ += (to_insert.second - to_insert.first);
+      to_insert = *it;
+    }
+  }
+  ranges_.push_back(to_insert);
+  blocks_ += (to_insert.second - to_insert.first);
+}
+
+// Compute the block range the file occupies, and insert that range.
+void SortedRangeSet::Insert(size_t start, size_t len) {
+  Range to_insert{ start / kBlockSize, (start + len - 1) / kBlockSize + 1 };
+  Insert(to_insert);
+}
+
+bool SortedRangeSet::Overlaps(size_t start, size_t len) const {
+  RangeSet rs({ { start / kBlockSize, (start + len - 1) / kBlockSize + 1 } });
+  return Overlaps(rs);
+}
+
+// Given an offset of the file, checks if the corresponding block (by considering the file as
+// 0-based continuous block ranges) is covered by the SortedRangeSet. If so, returns the offset
+// within this SortedRangeSet.
+//
+// For example, the 4106-th byte of a file is from block 1, assuming a block size of 4096-byte.
+// The mapped offset within a SortedRangeSet("1-9 15-19") is 10.
+//
+// An offset of 65546 falls into the 16-th block in a file. Block 16 is contained as the 10-th
+// item in SortedRangeSet("1-9 15-19"). So its data can be found at offset 40970 (i.e. 4096 * 10
+// + 10) in a range represented by this SortedRangeSet.
+size_t SortedRangeSet::GetOffsetInRangeSet(size_t old_offset) const {
+  size_t old_block_start = old_offset / kBlockSize;
+  size_t new_block_start = 0;
+  for (const auto& range : ranges_) {
+    // Find the index of old_block_start.
+    if (old_block_start >= range.second) {
+      new_block_start += (range.second - range.first);
+    } else if (old_block_start >= range.first) {
+      new_block_start += (old_block_start - range.first);
+      return (new_block_start * kBlockSize + old_offset % kBlockSize);
+    } else {
+      CHECK(false) << "block_start " << old_block_start
+                   << " is missing between two ranges: " << this->ToString();
+      return 0;
+    }
+  }
+  CHECK(false) << "block_start " << old_block_start
+               << " exceeds the limit of current RangeSet: " << this->ToString();
+  return 0;
+}
diff --git a/print_sha1.h b/print_sha1.h
deleted file mode 100644
index 1f85895..0000000
--- a/print_sha1.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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, 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);
-}
-
-static std::string print_hex(const uint8_t* bytes, size_t len) {
-    return print_sha1(bytes, len);
-}
-
-#endif  // RECOVERY_PRINT_SHA1_H
diff --git a/recovery-persist.rc b/recovery-persist.rc
index 6761627..135a3c3 100644
--- a/recovery-persist.rc
+++ b/recovery-persist.rc
@@ -1,3 +1,3 @@
 on post-fs-data
     mkdir /data/misc/recovery 0770 system log
-    exec - system log -- /system/bin/recovery-persist
+    exec_background - system log -- /system/bin/recovery-persist
diff --git a/recovery-refresh.rc b/recovery-refresh.rc
index 14b05cc..9fefc81 100644
--- a/recovery-refresh.rc
+++ b/recovery-refresh.rc
@@ -1,2 +1,2 @@
 on post-fs
-    exec - system log -- /system/bin/recovery-refresh
+    exec_background - system log -- /system/bin/recovery-refresh
diff --git a/recovery.cpp b/recovery.cpp
index 07bd7b9..07ec5cf 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -50,9 +50,9 @@
 #include <bootloader_message/bootloader_message.h>
 #include <cutils/android_reboot.h>
 #include <cutils/properties.h> /* for property_list */
-#include <healthd/BatteryMonitor.h>
-#include <private/android_logger.h> /* private pmsg functions */
-#include <private/android_filesystem_config.h>  /* for AID_SYSTEM */
+#include <health2/Health.h>
+#include <private/android_filesystem_config.h> /* for AID_SYSTEM */
+#include <private/android_logger.h>            /* private pmsg functions */
 #include <selinux/android.h>
 #include <selinux/label.h>
 #include <selinux/selinux.h>
@@ -61,13 +61,13 @@
 #include "adb_install.h"
 #include "common.h"
 #include "device.h"
-#include "error_code.h"
 #include "fuse_sdcard_provider.h"
 #include "fuse_sideload.h"
 #include "install.h"
 #include "minadbd/minadbd.h"
 #include "minui/minui.h"
 #include "otautil/DirUtil.h"
+#include "otautil/error_code.h"
 #include "roots.h"
 #include "rotate_logs.h"
 #include "screen_ui.h"
@@ -108,6 +108,7 @@
 static const char *CONVERT_FBE_FILE = "/tmp/convert_fbe/convert_fbe";
 static const char *CACHE_ROOT = "/cache";
 static const char *DATA_ROOT = "/data";
+static const char* METADATA_ROOT = "/metadata";
 static const char *SDCARD_ROOT = "/sdcard";
 static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
 static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
@@ -125,6 +126,10 @@
 static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
 static constexpr const char* DEFAULT_LOCALE = "en-US";
 
+// We define RECOVERY_API_VERSION in Android.mk, which will be picked up by build system and packed
+// into target_files.zip. Assert the version defined in code and in Android.mk are consistent.
+static_assert(kRecoveryApiVersion == RECOVERY_API_VERSION, "Mismatching recovery API versions.");
+
 static std::string locale;
 static bool has_cache = false;
 
@@ -179,19 +184,19 @@
  *    7b. the user reboots (pulling the battery, etc) into the main system
  */
 
-// open a given path, mounting partitions as necessary
-FILE* fopen_path(const char *path, const char *mode) {
-    if (ensure_path_mounted(path) != 0) {
-        LOG(ERROR) << "Can't mount " << path;
-        return NULL;
-    }
+// Open a given path, mounting partitions as necessary.
+FILE* fopen_path(const char* path, const char* mode) {
+  if (ensure_path_mounted(path) != 0) {
+    LOG(ERROR) << "Can't mount " << path;
+    return nullptr;
+  }
 
-    // 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;
+  // 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])) {
+    mkdir_recursively(path, 0777, true, sehandle);
+  }
+  return fopen(path, mode);
 }
 
 // close a file, log an error if the error indicator is set
@@ -594,7 +599,7 @@
   if (is_cache) {
     // Re-create the log dir and write back the log entries.
     if (ensure_path_mounted(CACHE_LOG_DIR) == 0 &&
-        dirCreateHierarchy(CACHE_LOG_DIR, 0777, nullptr, false, sehandle) == 0) {
+        mkdir_recursively(CACHE_LOG_DIR, 0777, false, sehandle) == 0) {
       for (const auto& log : log_files) {
         if (!android::base::WriteStringToFile(log.data, log.name, log.sb.st_mode, log.sb.st_uid,
                                               log.sb.st_gid)) {
@@ -748,11 +753,19 @@
     modified_flash = true;
 
     ui->Print("\n-- Wiping data...\n");
-    bool success =
-        device->PreWipeData() &&
-        erase_volume("/data") &&
-        (has_cache ? erase_volume("/cache") : true) &&
-        device->PostWipeData();
+    bool success = device->PreWipeData();
+    if (success) {
+      success &= erase_volume(DATA_ROOT);
+      if (has_cache) {
+        success &= erase_volume(CACHE_ROOT);
+      }
+      if (volume_for_mount_point(METADATA_ROOT) != nullptr) {
+        success &= erase_volume(METADATA_ROOT);
+      }
+    }
+    if (success) {
+      success &= device->PostWipeData();
+    }
     ui->Print("Data wipe %s.\n", success ? "complete" : "failed");
     return success;
 }
@@ -1187,6 +1200,11 @@
         run_graphics_test();
         break;
 
+      case Device::RUN_LOCALE_TEST: {
+        ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
+        screen_ui->CheckBackgroundTextImages(locale);
+        break;
+      }
       case Device::MOUNT_SYSTEM:
         // 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
@@ -1205,9 +1223,8 @@
   }
 }
 
-static void
-print_property(const char *key, const char *name, void *cookie) {
-    printf("%s=%s\n", key, name);
+static void print_property(const char* key, const char* name, void* /* cookie */) {
+  printf("%s=%s\n", key, name);
 }
 
 static std::string load_locale_from_cache() {
@@ -1241,70 +1258,91 @@
 
 static constexpr char log_characters[] = "VDIWEF";
 
-void UiLogger(android::base::LogId id, android::base::LogSeverity severity,
-               const char* tag, const char* file, unsigned int line,
-               const char* message) {
-    if (severity >= android::base::ERROR && ui != nullptr) {
-        ui->Print("E:%s\n", message);
-    } else {
-        fprintf(stdout, "%c:%s\n", log_characters[severity], message);
-    }
+void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
+              const char* /* tag */, const char* /* file */, unsigned int /* line */,
+              const char* message) {
+  if (severity >= android::base::ERROR && ui != nullptr) {
+    ui->Print("E:%s\n", message);
+  } else {
+    fprintf(stdout, "%c:%s\n", log_characters[severity], message);
+  }
 }
 
 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);
+  using android::hardware::health::V1_0::BatteryStatus;
+  using android::hardware::health::V2_0::Result;
+  using android::hardware::health::V2_0::toString;
+  using android::hardware::health::V2_0::implementation::Health;
 
-    android::BatteryMonitor monitor;
-    monitor.init(&healthd_config);
+  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
+  };
 
-    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);
+  auto health =
+      android::hardware::health::V2_0::implementation::Health::initInstance(&healthd_config);
+
+  int wait_second = 0;
+  while (true) {
+    auto charge_status = BatteryStatus::UNKNOWN;
+    health
+        ->getChargeStatus([&charge_status](auto res, auto out_status) {
+          if (res == Result::SUCCESS) {
+            charge_status = out_status;
+          }
+        })
+        .isOk();  // should not have transport error
+
+    // Treat unknown status as charged.
+    bool charged = (charge_status != BatteryStatus::DISCHARGING &&
+                    charge_status != BatteryStatus::NOT_CHARGING);
+
+    Result res = Result::UNKNOWN;
+    int32_t capacity = INT32_MIN;
+    health
+        ->getCapacity([&res, &capacity](auto out_res, auto out_capacity) {
+          res = out_res;
+          capacity = out_capacity;
+        })
+        .isOk();  // should not have transport error
+
+    ui_print("charge_status %d, charged %d, status %s, capacity %" PRId32 "\n", charge_status,
+             charged, toString(res).c_str(), capacity);
+    // 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 (res == Result::SUCCESS && capacity == 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 (res != Result::SUCCESS) {
+      capacity = 100;
+    }
+    return (charged && capacity >= BATTERY_WITH_CHARGER_OK_PERCENTAGE) ||
+           (!charged && capacity >= BATTERY_OK_PERCENTAGE);
     }
 }
 
+// Set the retry count to |retry_count| in BCB.
 static void set_retry_bootloader_message(int retry_count, const std::vector<std::string>& args) {
   std::vector<std::string> options;
   for (const auto& arg : args) {
@@ -1313,8 +1351,8 @@
     }
   }
 
-  // Increment the retry counter by 1.
-  options.push_back(android::base::StringPrintf("--retry_count=%d", retry_count + 1));
+  // Update the retry counter in BCB.
+  options.push_back(android::base::StringPrintf("--retry_count=%d", retry_count));
   std::string err;
   if (!update_bootloader_message(options, &err)) {
     LOG(ERROR) << err;
@@ -1349,303 +1387,331 @@
 }
 
 int main(int argc, char **argv) {
-    // We don't have logcat yet under recovery; so we'll print error on screen and
-    // log to stdout (which is redirected to recovery.log) as we used to do.
-    android::base::InitLogging(argv, &UiLogger);
+  // We don't have logcat yet under recovery; so we'll print error on screen and
+  // log to stdout (which is redirected to recovery.log) as we used to do.
+  android::base::InitLogging(argv, &UiLogger);
 
-    // 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;
+  // 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);
+  __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) {
-        minadbd_main();
-        return 0;
+  // 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) {
+    minadbd_main();
+    return 0;
+  }
+
+  time_t start = time(nullptr);
+
+  // 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_mount_point(CACHE_ROOT) != nullptr;
+
+  std::vector<std::string> args = get_args(argc, argv);
+  std::vector<char*> args_to_parse(args.size());
+  std::transform(args.cbegin(), args.cend(), args_to_parse.begin(),
+                 [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+
+  const char* update_package = nullptr;
+  bool should_wipe_data = false;
+  bool should_prompt_and_wipe_data = false;
+  bool should_wipe_cache = false;
+  bool should_wipe_ab = false;
+  size_t wipe_package_size = 0;
+  bool show_text = false;
+  bool sideload = false;
+  bool sideload_auto_reboot = false;
+  bool just_exit = false;
+  bool shutdown_after = false;
+  int retry_count = 0;
+  bool security_update = false;
+
+  int arg;
+  int option_index;
+  while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", 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 'p':
+        shutdown_after = true;
+        break;
+      case 'r':
+        reason = optarg;
+        break;
+      case 'e':
+        security_update = true;
+        break;
+      case 0: {
+        std::string option = OPTIONS[option_index].name;
+        if (option == "wipe_ab") {
+          should_wipe_ab = true;
+        } else if (option == "wipe_package_size") {
+          android::base::ParseUint(optarg, &wipe_package_size);
+        } else if (option == "prompt_and_wipe_data") {
+          should_prompt_and_wipe_data = true;
+        }
+        break;
+      }
+      case '?':
+        LOG(ERROR) << "Invalid command argument";
+        continue;
     }
+  }
 
-    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;
-
-    std::vector<std::string> args = get_args(argc, argv);
-    std::vector<char*> args_to_parse(args.size());
-    std::transform(args.cbegin(), args.cend(), args_to_parse.begin(),
-                   [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
-
-    const char *update_package = NULL;
-    bool should_wipe_data = false;
-    bool should_prompt_and_wipe_data = false;
-    bool should_wipe_cache = false;
-    bool should_wipe_ab = false;
-    size_t wipe_package_size = 0;
-    bool show_text = false;
-    bool sideload = false;
-    bool sideload_auto_reboot = false;
-    bool just_exit = false;
-    bool shutdown_after = false;
-    int retry_count = 0;
-    bool security_update = false;
-
-    int arg;
-    int option_index;
-    while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", 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 'p': shutdown_after = true; break;
-        case 'r': reason = optarg; break;
-        case 'e': security_update = true; break;
-        case 0: {
-            std::string option = OPTIONS[option_index].name;
-            if (option == "wipe_ab") {
-                should_wipe_ab = true;
-            } else if (option == "wipe_package_size") {
-                android::base::ParseUint(optarg, &wipe_package_size);
-            } else if (option == "prompt_and_wipe_data") {
-                should_prompt_and_wipe_data = true;
-            }
-            break;
-        }
-        case '?':
-            LOG(ERROR) << "Invalid command argument";
-            continue;
-        }
+  if (locale.empty()) {
+    if (has_cache) {
+      locale = load_locale_from_cache();
     }
 
     if (locale.empty()) {
-        if (has_cache) {
-            locale = load_locale_from_cache();
-        }
-
-        if (locale.empty()) {
-            locale = DEFAULT_LOCALE;
-        }
+      locale = DEFAULT_LOCALE;
     }
+  }
 
-    printf("locale is [%s]\n", locale.c_str());
-    printf("stage is [%s]\n", stage.c_str());
-    printf("reason is [%s]\n", reason);
+  printf("locale is [%s]\n", locale.c_str());
+  printf("stage is [%s]\n", stage.c_str());
+  printf("reason is [%s]\n", reason);
 
-    Device* device = make_device();
-    if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
-        printf("Quiescent recovery mode.\n");
-        ui = new StubRecoveryUI();
+  Device* device = make_device();
+  if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
+    printf("Quiescent recovery mode.\n");
+    ui = new StubRecoveryUI();
+  } else {
+    ui = device->GetUI();
+
+    if (!ui->Init(locale)) {
+      printf("Failed to initialize UI, use stub UI instead.\n");
+      ui = new StubRecoveryUI();
+    }
+  }
+
+  // Set background string to "installing security update" for security update,
+  // otherwise set it to "installing system update".
+  ui->SetSystemUpdateText(security_update);
+
+  int st_cur, st_max;
+  if (!stage.empty() && sscanf(stage.c_str(), "%d/%d", &st_cur, &st_max) == 2) {
+    ui->SetStage(st_cur, st_max);
+  }
+
+  ui->SetBackground(RecoveryUI::NONE);
+  if (show_text) ui->ShowText(true);
+
+  sehandle = selinux_android_file_context_handle();
+  selinux_android_set_sehandle(sehandle);
+  if (!sehandle) {
+    ui->Print("Warning: No file_contexts\n");
+  }
+
+  device->StartRecovery();
+
+  printf("Command:");
+  for (const auto& arg : args) {
+    printf(" \"%s\"", arg.c_str());
+  }
+  printf("\n\n");
+
+  property_list(print_property, nullptr);
+  printf("\n");
+
+  ui->Print("Supported API: %d\n", kRecoveryApiVersion);
+
+  int status = INSTALL_SUCCESS;
+
+  if (update_package != nullptr) {
+    // 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);
+      // Log the error code to last_install when installation skips due to
+      // low battery.
+      log_failure_code(kLowBattery, update_package);
+      status = INSTALL_SKIPPED;
+    } else if (bootreason_in_blacklist()) {
+      // Skip update-on-reboot when bootreason is kernel_panic or similar
+      ui->Print("bootreason is in the blacklist; skip OTA installation\n");
+      log_failure_code(kBootreasonInBlacklist, update_package);
+      status = INSTALL_SKIPPED;
     } else {
-        ui = device->GetUI();
-
-        if (!ui->Init(locale)) {
-            printf("Failed to initialize UI, use stub UI instead.\n");
-            ui = new StubRecoveryUI();
-        }
-    }
-
-    // Set background string to "installing security update" for security update,
-    // otherwise set it to "installing system update".
-    ui->SetSystemUpdateText(security_update);
-
-    int st_cur, st_max;
-    if (!stage.empty() && sscanf(stage.c_str(), "%d/%d", &st_cur, &st_max) == 2) {
-        ui->SetStage(st_cur, st_max);
-    }
-
-    ui->SetBackground(RecoveryUI::NONE);
-    if (show_text) ui->ShowText(true);
-
-    sehandle = selinux_android_file_context_handle();
-    selinux_android_set_sehandle(sehandle);
-    if (!sehandle) {
-        ui->Print("Warning: No file_contexts\n");
-    }
-
-    device->StartRecovery();
-
-    printf("Command:");
-    for (const auto& arg : args) {
-        printf(" \"%s\"", arg.c_str());
-    }
-    printf("\n\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);
-            // Log the error code to last_install when installation skips due to
-            // low battery.
-            log_failure_code(kLowBattery, update_package);
-            status = INSTALL_SKIPPED;
-        } else if (bootreason_in_blacklist()) {
-            // Skip update-on-reboot when bootreason is kernel_panic or similar
-            ui->Print("bootreason is in the blacklist; skip OTA installation\n");
-            log_failure_code(kBootreasonInBlacklist, update_package);
-            status = INSTALL_SKIPPED;
-        } else {
-            status = install_package(update_package, &should_wipe_cache,
-                                     TEMPORARY_INSTALL_FILE, true, retry_count);
-            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 RETRY_LIMIT
-                // times before we abandon this OTA update.
-                if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
-                    copy_logs();
-                    set_retry_bootloader_message(retry_count, args);
-                    // Print retry count on screen.
-                    ui->Print("Retry attempt %d\n", retry_count);
-
-                    // Reboot and retry the update
-                    if (!reboot("reboot,recovery")) {
-                        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(device)) {
-            status = INSTALL_ERROR;
-        }
-    } else if (should_prompt_and_wipe_data) {
-        ui->ShowText(true);
-        ui->SetBackground(RecoveryUI::ERROR);
-        if (!prompt_and_wipe_data(device)) {
-            status = INSTALL_ERROR;
-        }
-        ui->ShowText(false);
-    } else if (should_wipe_cache) {
-        if (!wipe_cache(false, device)) {
-            status = INSTALL_ERROR;
-        }
-    } else if (should_wipe_ab) {
-        if (!wipe_ab_device(wipe_package_size)) {
-            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(&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) {
-      // If this is an eng or userdebug build, automatically turn on the text display if no command
-      // is specified. Note that this should be called before setting the background to avoid
-      // flickering the background image.
-      if (is_ro_debuggable()) {
-        ui->ShowText(true);
+      // It's a fresh update. Initialize the retry_count in the BCB to 1; therefore we can later
+      // identify the interrupted update due to unexpected reboots.
+      if (retry_count == 0) {
+        set_retry_bootloader_message(retry_count + 1, args);
       }
-      status = INSTALL_NONE;  // No command specified
-      ui->SetBackground(RecoveryUI::NO_COMMAND);
-    }
 
-    if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
-        ui->SetBackground(RecoveryUI::ERROR);
-        if (!ui->IsTextVisible()) {
-            sleep(5);
+      status = install_package(update_package, &should_wipe_cache, TEMPORARY_INSTALL_FILE, true,
+                               retry_count);
+      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 RETRY_LIMIT
+        // times before we abandon this OTA update.
+        if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
+          copy_logs();
+          retry_count += 1;
+          set_retry_bootloader_message(retry_count, args);
+          // Print retry count on screen.
+          ui->Print("Retry attempt %d\n", retry_count);
+
+          // Reboot and retry the update
+          if (!reboot("reboot,recovery")) {
+            ui->Print("Reboot failed\n");
+          } else {
+            while (true) {
+              pause();
+            }
+          }
         }
-    }
-
-    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
-    // 1. If the recovery menu is visible, prompt and wait for commands.
-    // 2. If the state is INSTALL_NONE, wait for commands. (i.e. In user build, manually reboot into
-    //    recovery to sideload a package.)
-    // 3. sideload_auto_reboot is an option only available in user-debug build, reboot the device
-    //    without waiting.
-    // 4. In all other cases, reboot the device. Therefore, normal users will observe the device
-    //    reboot after it shows the "error" screen for 5s.
-    if ((status == INSTALL_NONE && !sideload_auto_reboot) || ui->IsTextVisible()) {
-        Device::BuiltinAction temp = prompt_and_wait(device, status);
-        if (temp != Device::NO_ACTION) {
-            after = temp;
+        // 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);
         }
+      }
     }
-
-    // Save logs and clean up before rebooting or shutting down.
-    finish_recovery();
-
-    switch (after) {
-        case Device::SHUTDOWN:
-            ui->Print("Shutting down...\n");
-            android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
-            break;
-
-        case Device::REBOOT_BOOTLOADER:
-            ui->Print("Rebooting to bootloader...\n");
-            android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
-            break;
-
-        default:
-            ui->Print("Rebooting...\n");
-            reboot("reboot,");
-            break;
+  } else if (should_wipe_data) {
+    if (!wipe_data(device)) {
+      status = INSTALL_ERROR;
     }
-    while (true) {
-        pause();
+  } else if (should_prompt_and_wipe_data) {
+    ui->ShowText(true);
+    ui->SetBackground(RecoveryUI::ERROR);
+    if (!prompt_and_wipe_data(device)) {
+      status = INSTALL_ERROR;
     }
-    // Should be unreachable.
-    return EXIT_SUCCESS;
+    ui->ShowText(false);
+  } else if (should_wipe_cache) {
+    if (!wipe_cache(false, device)) {
+      status = INSTALL_ERROR;
+    }
+  } else if (should_wipe_ab) {
+    if (!wipe_ab_device(wipe_package_size)) {
+      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(&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) {
+    // If this is an eng or userdebug build, automatically turn on the text display if no command
+    // is specified. Note that this should be called before setting the background to avoid
+    // flickering the background image.
+    if (is_ro_debuggable()) {
+      ui->ShowText(true);
+    }
+    status = INSTALL_NONE;  // No command specified
+    ui->SetBackground(RecoveryUI::NO_COMMAND);
+  }
+
+  if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
+    ui->SetBackground(RecoveryUI::ERROR);
+    if (!ui->IsTextVisible()) {
+      sleep(5);
+    }
+  }
+
+  Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
+  // 1. If the recovery menu is visible, prompt and wait for commands.
+  // 2. If the state is INSTALL_NONE, wait for commands. (i.e. In user build, manually reboot into
+  //    recovery to sideload a package.)
+  // 3. sideload_auto_reboot is an option only available in user-debug build, reboot the device
+  //    without waiting.
+  // 4. In all other cases, reboot the device. Therefore, normal users will observe the device
+  //    reboot after it shows the "error" screen for 5s.
+  if ((status == INSTALL_NONE && !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");
+      android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
+      break;
+
+    case Device::REBOOT_BOOTLOADER:
+      ui->Print("Rebooting to bootloader...\n");
+      android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
+      break;
+
+    default:
+      ui->Print("Rebooting...\n");
+      reboot("reboot,");
+      break;
+  }
+  while (true) {
+    pause();
+  }
+  // Should be unreachable.
+  return EXIT_SUCCESS;
 }
diff --git a/res-hdpi/images/erasing_text.png b/res-hdpi/images/erasing_text.png
index 0982544..34c56a9 100644
--- a/res-hdpi/images/erasing_text.png
+++ b/res-hdpi/images/erasing_text.png
Binary files differ
diff --git a/res-hdpi/images/error_text.png b/res-hdpi/images/error_text.png
index 3a06f6e..2a96053 100644
--- a/res-hdpi/images/error_text.png
+++ b/res-hdpi/images/error_text.png
Binary files differ
diff --git a/res-hdpi/images/installing_security_text.png b/res-hdpi/images/installing_security_text.png
index b1acd23..97e1f11 100644
--- a/res-hdpi/images/installing_security_text.png
+++ b/res-hdpi/images/installing_security_text.png
Binary files differ
diff --git a/res-hdpi/images/installing_text.png b/res-hdpi/images/installing_text.png
index f0f5d8b..1d591eb 100644
--- a/res-hdpi/images/installing_text.png
+++ b/res-hdpi/images/installing_text.png
Binary files differ
diff --git a/res-hdpi/images/no_command_text.png b/res-hdpi/images/no_command_text.png
index def5036..977fcfa 100644
--- a/res-hdpi/images/no_command_text.png
+++ b/res-hdpi/images/no_command_text.png
Binary files differ
diff --git a/res-mdpi/images/erasing_text.png b/res-mdpi/images/erasing_text.png
index 82b4461..dcd0ea6 100644
--- a/res-mdpi/images/erasing_text.png
+++ b/res-mdpi/images/erasing_text.png
Binary files differ
diff --git a/res-mdpi/images/error_text.png b/res-mdpi/images/error_text.png
index adb4513..2152dad 100644
--- a/res-mdpi/images/error_text.png
+++ b/res-mdpi/images/error_text.png
Binary files differ
diff --git a/res-mdpi/images/installing_security_text.png b/res-mdpi/images/installing_security_text.png
index 54e5564..d1ac4ca 100644
--- a/res-mdpi/images/installing_security_text.png
+++ b/res-mdpi/images/installing_security_text.png
Binary files differ
diff --git a/res-mdpi/images/installing_text.png b/res-mdpi/images/installing_text.png
index d423318..c9b6d71 100644
--- a/res-mdpi/images/installing_text.png
+++ b/res-mdpi/images/installing_text.png
Binary files differ
diff --git a/res-mdpi/images/no_command_text.png b/res-mdpi/images/no_command_text.png
index cd77ff4..f77ad13 100644
--- a/res-mdpi/images/no_command_text.png
+++ b/res-mdpi/images/no_command_text.png
Binary files differ
diff --git a/res-xhdpi/images/erasing_text.png b/res-xhdpi/images/erasing_text.png
index 333edbe..e22b274 100644
--- a/res-xhdpi/images/erasing_text.png
+++ b/res-xhdpi/images/erasing_text.png
Binary files differ
diff --git a/res-xhdpi/images/error_text.png b/res-xhdpi/images/error_text.png
index e262584..e4c27e1 100644
--- a/res-xhdpi/images/error_text.png
+++ b/res-xhdpi/images/error_text.png
Binary files differ
diff --git a/res-xhdpi/images/installing_security_text.png b/res-xhdpi/images/installing_security_text.png
index e0f0f3e..7ba12b6 100644
--- a/res-xhdpi/images/installing_security_text.png
+++ b/res-xhdpi/images/installing_security_text.png
Binary files differ
diff --git a/res-xhdpi/images/installing_text.png b/res-xhdpi/images/installing_text.png
index a7e67f5..567988e 100644
--- a/res-xhdpi/images/installing_text.png
+++ b/res-xhdpi/images/installing_text.png
Binary files differ
diff --git a/res-xhdpi/images/no_command_text.png b/res-xhdpi/images/no_command_text.png
index 13aef7b..a682abb 100644
--- a/res-xhdpi/images/no_command_text.png
+++ b/res-xhdpi/images/no_command_text.png
Binary files differ
diff --git a/res-xxhdpi/images/erasing_text.png b/res-xxhdpi/images/erasing_text.png
index 80e7c47..6cc953b 100644
--- a/res-xxhdpi/images/erasing_text.png
+++ b/res-xxhdpi/images/erasing_text.png
Binary files differ
diff --git a/res-xxhdpi/images/error_text.png b/res-xxhdpi/images/error_text.png
index 32a1965..0d5cea8 100644
--- a/res-xxhdpi/images/error_text.png
+++ b/res-xxhdpi/images/error_text.png
Binary files differ
diff --git a/res-xxhdpi/images/installing_security_text.png b/res-xxhdpi/images/installing_security_text.png
index c53c9ac..5d10598 100644
--- a/res-xxhdpi/images/installing_security_text.png
+++ b/res-xxhdpi/images/installing_security_text.png
Binary files differ
diff --git a/res-xxhdpi/images/installing_text.png b/res-xxhdpi/images/installing_text.png
index 38b18d2..6e94fa2 100644
--- a/res-xxhdpi/images/installing_text.png
+++ b/res-xxhdpi/images/installing_text.png
Binary files differ
diff --git a/res-xxhdpi/images/no_command_text.png b/res-xxhdpi/images/no_command_text.png
index a0666d8..40ab484 100644
--- a/res-xxhdpi/images/no_command_text.png
+++ b/res-xxhdpi/images/no_command_text.png
Binary files differ
diff --git a/res-xxxhdpi/images/erasing_text.png b/res-xxxhdpi/images/erasing_text.png
index 4f7b37b..cc73099 100644
--- a/res-xxxhdpi/images/erasing_text.png
+++ b/res-xxxhdpi/images/erasing_text.png
Binary files differ
diff --git a/res-xxxhdpi/images/error_text.png b/res-xxxhdpi/images/error_text.png
index 052bf21..fea3cfc 100644
--- a/res-xxxhdpi/images/error_text.png
+++ b/res-xxxhdpi/images/error_text.png
Binary files differ
diff --git a/res-xxxhdpi/images/installing_security_text.png b/res-xxxhdpi/images/installing_security_text.png
index a9e739b..ed77d08 100644
--- a/res-xxxhdpi/images/installing_security_text.png
+++ b/res-xxxhdpi/images/installing_security_text.png
Binary files differ
diff --git a/res-xxxhdpi/images/installing_text.png b/res-xxxhdpi/images/installing_text.png
index 2d19486..9659106 100644
--- a/res-xxxhdpi/images/installing_text.png
+++ b/res-xxxhdpi/images/installing_text.png
Binary files differ
diff --git a/res-xxxhdpi/images/no_command_text.png b/res-xxxhdpi/images/no_command_text.png
index ee0c238..4e6f363 100644
--- a/res-xxxhdpi/images/no_command_text.png
+++ b/res-xxxhdpi/images/no_command_text.png
Binary files differ
diff --git a/roots.cpp b/roots.cpp
index 29f55b9..184e799 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -16,174 +16,208 @@
 
 #include "roots.h"
 
-#include <errno.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdint.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 <ctype.h>
-#include <fcntl.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
 
 #include <android-base/logging.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
+#include <cryptfs.h>
 #include <ext4_utils/wipe.h>
 #include <fs_mgr.h>
 
-#include "common.h"
 #include "mounts.h"
-#include "cryptfs.h"
 
-static struct fstab *fstab = NULL;
+static struct fstab* fstab = nullptr;
 
-extern struct selabel_handle *sehandle;
+extern struct selabel_handle* sehandle;
 
-void load_volume_table()
-{
-    int i;
-    int ret;
+void load_volume_table() {
+  fstab = fs_mgr_read_fstab_default();
+  if (!fstab) {
+    LOG(ERROR) << "Failed to read default fstab";
+    return;
+  }
 
-    fstab = fs_mgr_read_fstab_default();
-    if (!fstab) {
-        LOG(ERROR) << "failed to read default fstab";
-        return;
-    }
+  int ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
+  if (ret == -1) {
+    LOG(ERROR) << "Failed to add /tmp entry to fstab";
+    fs_mgr_free_fstab(fstab);
+    fstab = nullptr;
+    return;
+  }
 
-    ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
-    if (ret < 0 ) {
-        LOG(ERROR) << "failed to add /tmp entry to fstab";
-        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");
+  printf("recovery filesystem table\n");
+  printf("=========================\n");
+  for (int i = 0; i < fstab->num_entries; ++i) {
+    const 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);
+Volume* volume_for_mount_point(const std::string& mount_point) {
+  return fs_mgr_get_entry_for_mount_point(fstab, mount_point);
+}
+
+// Finds the volume specified by the given path. fs_mgr_get_entry_for_mount_point() does exact match
+// only, so it attempts the prefixes recursively (e.g. "/cache/recovery/last_log",
+// "/cache/recovery", "/cache", "/" for a given path of "/cache/recovery/last_log") and returns the
+// first match or nullptr.
+static Volume* volume_for_path(const char* path) {
+  if (path == nullptr || path[0] == '\0') return nullptr;
+  std::string str(path);
+  while (true) {
+    Volume* result = fs_mgr_get_entry_for_mount_point(fstab, str);
+    if (result != nullptr || str == "/") {
+      return result;
+    }
+    size_t slash = str.find_last_of('/');
+    if (slash == std::string::npos) return nullptr;
+    if (slash == 0) {
+      str = "/";
+    } else {
+      str = str.substr(0, slash);
+    }
+  }
+  return nullptr;
 }
 
 // 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) {
-        LOG(ERROR) << "unknown volume for path [" << path << "]";
-        return -1;
-    }
-    if (strcmp(v->fs_type, "ramdisk") == 0) {
-        // the ramdisk is always mounted.
-        return 0;
-    }
-
-    if (!scan_mounted_volumes()) {
-        LOG(ERROR) << "failed to scan mounted volumes";
-        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)) {
-            LOG(ERROR) << "failed to mount " << mount_point << " (" << strerror(errno)
-                       << ") , formatting.....";
-            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 {
-                PLOG(ERROR) << "failed to format " << mount_point;
-                return -1;
-            }
-        }
-
-        if (result == -1) {
-            PLOG(ERROR) << "failed to mount " << mount_point;
-            return -1;
-        }
-        return 0;
-    }
-
-    LOG(ERROR) << "unknown fs_type \"" << v->fs_type << "\" for " << mount_point;
+  Volume* v = volume_for_path(path);
+  if (v == nullptr) {
+    LOG(ERROR) << "unknown volume for path [" << path << "]";
     return -1;
+  }
+  if (strcmp(v->fs_type, "ramdisk") == 0) {
+    // The ramdisk is always mounted.
+    return 0;
+  }
+
+  if (!scan_mounted_volumes()) {
+    LOG(ERROR) << "Failed to scan mounted volumes";
+    return -1;
+  }
+
+  if (!mount_point) {
+    mount_point = v->mount_point;
+  }
+
+  const MountedVolume* mv = find_mounted_volume_by_mount_point(mount_point);
+  if (mv != nullptr) {
+    // 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)) {
+      PLOG(ERROR) << "Failed to mount " << mount_point << "; formatting";
+      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 {
+        PLOG(ERROR) << "Failed to format " << mount_point;
+        return -1;
+      }
+    }
+
+    if (result == -1) {
+      PLOG(ERROR) << "Failed to mount " << mount_point;
+      return -1;
+    }
+    return 0;
+  }
+
+  LOG(ERROR) << "unknown fs_type \"" << v->fs_type << "\" for " << mount_point;
+  return -1;
 }
 
 int ensure_path_mounted(const char* path) {
-    // Mount at the default mount point.
-    return ensure_path_mounted_at(path, nullptr);
+  // 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) {
-        LOG(ERROR) << "unknown volume for path [" << path << "]";
-        return -1;
-    }
-    if (strcmp(v->fs_type, "ramdisk") == 0) {
-        // the ramdisk is always mounted; you can't unmount it.
-        return -1;
-    }
+  const Volume* v = volume_for_path(path);
+  if (v == nullptr) {
+    LOG(ERROR) << "unknown volume for path [" << 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()) {
-        LOG(ERROR) << "failed to scan mounted volumes";
-        return -1;
-    }
+  if (!scan_mounted_volumes()) {
+    LOG(ERROR) << "Failed to scan mounted volumes";
+    return -1;
+  }
 
-    MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
-    if (mv == NULL) {
-        // volume is already unmounted
-        return 0;
-    }
+  MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
+  if (mv == nullptr) {
+    // Volume is already unmounted.
+    return 0;
+  }
 
-    return unmount_mounted_volume(mv);
+  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(EXIT_FAILURE);
-    }
-    waitpid(child, &status, 0);
-    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
-        LOG(ERROR) << path << " failed with status " << WEXITSTATUS(status);
-    }
-    return WEXITSTATUS(status);
+static int exec_cmd(const std::vector<std::string>& args) {
+  CHECK_NE(static_cast<size_t>(0), args.size());
+
+  std::vector<char*> argv(args.size());
+  std::transform(args.cbegin(), args.cend(), argv.begin(),
+                 [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+  argv.push_back(nullptr);
+
+  pid_t child;
+  if ((child = fork()) == 0) {
+    execv(argv[0], argv.data());
+    _exit(EXIT_FAILURE);
+  }
+
+  int status;
+  waitpid(child, &status, 0);
+  if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+    LOG(ERROR) << args[0] << " failed with status " << WEXITSTATUS(status);
+  }
+  return WEXITSTATUS(status);
 }
 
-static ssize_t get_file_size(int fd, uint64_t reserve_len) {
+static int64_t get_file_size(int fd, uint64_t reserve_len) {
   struct stat buf;
   int ret = fstat(fd, &buf);
   if (ret) return 0;
 
-  ssize_t computed_size;
+  int64_t computed_size;
   if (S_ISREG(buf.st_mode)) {
     computed_size = buf.st_size - reserve_len;
   } else if (S_ISBLK(buf.st_mode)) {
-    computed_size = get_block_device_size(fd) - reserve_len;
+    uint64_t block_device_size = get_block_device_size(fd);
+    if (block_device_size < reserve_len ||
+        block_device_size > std::numeric_limits<int64_t>::max()) {
+      computed_size = 0;
+    } else {
+      computed_size = block_device_size - reserve_len;
+    }
   } else {
     computed_size = 0;
   }
@@ -192,136 +226,144 @@
 }
 
 int format_volume(const char* volume, const char* directory) {
-    Volume* v = volume_for_path(volume);
-    if (v == NULL) {
-        LOG(ERROR) << "unknown volume \"" << volume << "\"";
-        return -1;
-    }
-    if (strcmp(v->fs_type, "ramdisk") == 0) {
-        // you can't format the ramdisk.
-        LOG(ERROR) << "can't format_volume \"" << volume << "\"";
-        return -1;
-    }
-    if (strcmp(v->mount_point, volume) != 0) {
-        LOG(ERROR) << "can't give path \"" << volume << "\" to format_volume";
-        return -1;
-    }
-
-    if (ensure_path_unmounted(volume) != 0) {
-        LOG(ERROR) << "format_volume failed to unmount \"" << 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] == '/') {
-            LOG(INFO) << "wiping " << v->key_loc;
-            int fd = open(v->key_loc, O_WRONLY | O_CREAT, 0644);
-            if (fd < 0) {
-                LOG(ERROR) << "format_volume: failed to open " << 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) {
-          android::base::unique_fd fd(open(v->blk_device, O_RDONLY));
-          if (fd < 0) {
-            PLOG(ERROR) << "get_file_size: failed to open " << v->blk_device;
-            return -1;
-          }
-          length = get_file_size(fd.get(), CRYPT_FOOTER_OFFSET);
-          if (length <= 0) {
-            LOG(ERROR) << "get_file_size: invalid size " << length << " for " << v->blk_device;
-            return -1;
-          }
-        }
-        int result;
-        if (strcmp(v->fs_type, "ext4") == 0) {
-          static constexpr int block_size = 4096;
-          int raid_stride = v->logical_blk_size / block_size;
-          int raid_stripe_width = v->erase_blk_size / block_size;
-
-          // stride should be the max of 8kb and logical block size
-          if (v->logical_blk_size != 0 && v->logical_blk_size < 8192) {
-            raid_stride = 8192 / block_size;
-          }
-
-          const char* mke2fs_argv[] = { "/sbin/mke2fs_static",
-                                        "-F",
-                                        "-t",
-                                        "ext4",
-                                        "-b",
-                                        nullptr,
-                                        nullptr,
-                                        nullptr,
-                                        nullptr,
-                                        nullptr,
-                                        nullptr };
-
-          int i = 5;
-          std::string block_size_str = std::to_string(block_size);
-          mke2fs_argv[i++] = block_size_str.c_str();
-
-          std::string ext_args;
-          if (v->erase_blk_size != 0 && v->logical_blk_size != 0) {
-            ext_args = android::base::StringPrintf("stride=%d,stripe-width=%d", raid_stride,
-                                                   raid_stripe_width);
-            mke2fs_argv[i++] = "-E";
-            mke2fs_argv[i++] = ext_args.c_str();
-          }
-
-          mke2fs_argv[i++] = v->blk_device;
-
-          std::string size_str = std::to_string(length / block_size);
-          if (length != 0) {
-            mke2fs_argv[i++] = size_str.c_str();
-          }
-
-          result = exec_cmd(mke2fs_argv[0], const_cast<char**>(mke2fs_argv));
-          if (result == 0 && directory != nullptr) {
-            const char* e2fsdroid_argv[] = { "/sbin/e2fsdroid_static",
-                                             "-e",
-                                             "-f",
-                                             directory,
-                                             "-a",
-                                             volume,
-                                             v->blk_device,
-                                             nullptr };
-
-            result = exec_cmd(e2fsdroid_argv[0], const_cast<char**>(e2fsdroid_argv));
-          }
-        } else {   /* Has to be f2fs because we checked earlier. */
-            char *num_sectors = nullptr;
-            if (length >= 512 && asprintf(&num_sectors, "%zd", length / 512) <= 0) {
-                LOG(ERROR) << "format_volume: failed to create " << v->fs_type
-                           << " command for " << 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, nullptr};
-
-            result = exec_cmd(f2fs_path, (char* const*)f2fs_argv);
-            free(num_sectors);
-        }
-        if (result != 0) {
-            PLOG(ERROR) << "format_volume: make " << v->fs_type << " failed on " << v->blk_device;
-            return -1;
-        }
-        return 0;
-    }
-
+  const Volume* v = volume_for_path(volume);
+  if (v == nullptr) {
+    LOG(ERROR) << "unknown volume \"" << volume << "\"";
+    return -1;
+  }
+  if (strcmp(v->fs_type, "ramdisk") == 0) {
+    LOG(ERROR) << "can't format_volume \"" << volume << "\"";
+    return -1;
+  }
+  if (strcmp(v->mount_point, volume) != 0) {
+    LOG(ERROR) << "can't give path \"" << volume << "\" to format_volume";
+    return -1;
+  }
+  if (ensure_path_unmounted(volume) != 0) {
+    LOG(ERROR) << "format_volume: Failed to unmount \"" << v->mount_point << "\"";
+    return -1;
+  }
+  if (strcmp(v->fs_type, "ext4") != 0 && strcmp(v->fs_type, "f2fs") != 0) {
     LOG(ERROR) << "format_volume: fs_type \"" << v->fs_type << "\" unsupported";
     return -1;
+  }
+
+  // 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 != nullptr && v->key_loc[0] == '/') {
+    LOG(INFO) << "Wiping " << v->key_loc;
+    int fd = open(v->key_loc, O_WRONLY | O_CREAT, 0644);
+    if (fd == -1) {
+      PLOG(ERROR) << "format_volume: Failed to open " << v->key_loc;
+      return -1;
+    }
+    wipe_block_device(fd, get_file_size(fd));
+    close(fd);
+  }
+
+  int64_t length = 0;
+  if (v->length > 0) {
+    length = v->length;
+  } else if (v->length < 0 ||
+             (v->key_loc != nullptr && strcmp(v->key_loc, "footer") == 0)) {
+    android::base::unique_fd fd(open(v->blk_device, O_RDONLY));
+    if (fd == -1) {
+      PLOG(ERROR) << "format_volume: failed to open " << v->blk_device;
+      return -1;
+    }
+    length =
+        get_file_size(fd.get(), v->length ? -v->length : CRYPT_FOOTER_OFFSET);
+    if (length <= 0) {
+      LOG(ERROR) << "get_file_size: invalid size " << length << " for "
+                 << v->blk_device;
+      return -1;
+    }
+  }
+
+  if (strcmp(v->fs_type, "ext4") == 0) {
+    static constexpr int kBlockSize = 4096;
+    std::vector<std::string> mke2fs_args = {
+      "/sbin/mke2fs_static", "-F", "-t", "ext4", "-b", std::to_string(kBlockSize),
+    };
+
+    int raid_stride = v->logical_blk_size / kBlockSize;
+    int raid_stripe_width = v->erase_blk_size / kBlockSize;
+    // stride should be the max of 8KB and logical block size
+    if (v->logical_blk_size != 0 && v->logical_blk_size < 8192) {
+      raid_stride = 8192 / kBlockSize;
+    }
+    if (v->erase_blk_size != 0 && v->logical_blk_size != 0) {
+      mke2fs_args.push_back("-E");
+      mke2fs_args.push_back(
+          android::base::StringPrintf("stride=%d,stripe-width=%d", raid_stride, raid_stripe_width));
+    }
+    mke2fs_args.push_back(v->blk_device);
+    if (length != 0) {
+      mke2fs_args.push_back(std::to_string(length / kBlockSize));
+    }
+
+    int result = exec_cmd(mke2fs_args);
+    if (result == 0 && directory != nullptr) {
+      std::vector<std::string> e2fsdroid_args = {
+        "/sbin/e2fsdroid_static",
+        "-e",
+        "-f",
+        directory,
+        "-a",
+        volume,
+        v->blk_device,
+      };
+      result = exec_cmd(e2fsdroid_args);
+    }
+
+    if (result != 0) {
+      PLOG(ERROR) << "format_volume: Failed to make ext4 on " << v->blk_device;
+      return -1;
+    }
+    return 0;
+  }
+
+  // Has to be f2fs because we checked earlier.
+  static constexpr int kSectorSize = 4096;
+  std::string cmd("/sbin/mkfs.f2fs");
+  // clang-format off
+  std::vector<std::string> make_f2fs_cmd = {
+    cmd,
+    "-d1",
+    "-f",
+    "-O", "encrypt",
+    "-O", "quota",
+    "-O", "verity",
+    "-w", std::to_string(kSectorSize),
+    v->blk_device,
+  };
+  // clang-format on
+  if (length >= kSectorSize) {
+    make_f2fs_cmd.push_back(std::to_string(length / kSectorSize));
+  }
+
+  int result = exec_cmd(make_f2fs_cmd);
+  if (result == 0 && directory != nullptr) {
+    cmd = "/sbin/sload.f2fs";
+    // clang-format off
+    std::vector<std::string> sload_f2fs_cmd = {
+      cmd,
+      "-f", directory,
+      "-t", volume,
+      v->blk_device,
+    };
+    // clang-format on
+    result = exec_cmd(sload_f2fs_cmd);
+  }
+  if (result != 0) {
+    PLOG(ERROR) << "format_volume: Failed " << cmd << " on " << v->blk_device;
+    return -1;
+  }
+  return 0;
 }
 
 int format_volume(const char* volume) {
-    return format_volume(volume, NULL);
+  return format_volume(volume, nullptr);
 }
 
 int setup_install_mounts() {
@@ -339,12 +381,12 @@
 
     if (strcmp(v->mount_point, "/tmp") == 0 || strcmp(v->mount_point, "/cache") == 0) {
       if (ensure_path_mounted(v->mount_point) != 0) {
-        LOG(ERROR) << "failed to mount " << v->mount_point;
+        LOG(ERROR) << "Failed to mount " << v->mount_point;
         return -1;
       }
     } else {
       if (ensure_path_unmounted(v->mount_point) != 0) {
-        LOG(ERROR) << "failed to unmount " << v->mount_point;
+        LOG(ERROR) << "Failed to unmount " << v->mount_point;
         return -1;
       }
     }
diff --git a/roots.h b/roots.h
index 542f03b..46bb77e 100644
--- a/roots.h
+++ b/roots.h
@@ -17,13 +17,15 @@
 #ifndef RECOVERY_ROOTS_H_
 #define RECOVERY_ROOTS_H_
 
+#include <string>
+
 typedef struct fstab_rec Volume;
 
 // 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);
+// Return the Volume* record for this mount point (or nullptr).
+Volume* volume_for_mount_point(const std::string& mount_point);
 
 // Make sure that the volume 'path' is on is mounted.  Returns 0 on
 // success (volume is mounted).
diff --git a/rotate_logs.cpp b/rotate_logs.cpp
index fc22021..da00879 100644
--- a/rotate_logs.cpp
+++ b/rotate_logs.cpp
@@ -31,85 +31,77 @@
 static const std::string LAST_KMSG_FILTER = "recovery/last_kmsg";
 static const std::string LAST_LOG_FILTER = "recovery/last_log";
 
-ssize_t logbasename(
-        log_id_t /* logId */,
-        char /* prio */,
-        const char *filename,
-        const char * /* buf */, size_t len,
-        void *arg) {
-    bool* doRotate  = static_cast<bool*>(arg);
-    if (LAST_KMSG_FILTER.find(filename) != std::string::npos ||
-            LAST_LOG_FILTER.find(filename) != std::string::npos) {
-        *doRotate = true;
-    }
-    return len;
+ssize_t logbasename(log_id_t /* id */, char /* prio */, const char* filename, const char* /* buf */,
+                    size_t len, void* arg) {
+  bool* do_rotate = static_cast<bool*>(arg);
+  if (LAST_KMSG_FILTER.find(filename) != std::string::npos ||
+      LAST_LOG_FILTER.find(filename) != std::string::npos) {
+    *do_rotate = true;
+  }
+  return len;
 }
 
-ssize_t logrotate(
-        log_id_t logId,
-        char prio,
-        const char *filename,
-        const char *buf, size_t len,
-        void *arg) {
-    bool* doRotate  = static_cast<bool*>(arg);
-    if (!*doRotate) {
-        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
-    }
+ssize_t logrotate(log_id_t id, char prio, const char* filename, const char* buf, size_t len,
+                  void* arg) {
+  bool* do_rotate = static_cast<bool*>(arg);
+  if (!*do_rotate) {
+    return __android_log_pmsg_file_write(id, prio, filename, buf, len);
+  }
 
-    std::string name(filename);
-    size_t dot = name.find_last_of('.');
-    std::string sub = name.substr(0, dot);
+  std::string name(filename);
+  size_t dot = name.find_last_of('.');
+  std::string sub = name.substr(0, dot);
 
-    if (LAST_KMSG_FILTER.find(sub) == std::string::npos &&
-            LAST_LOG_FILTER.find(sub) == std::string::npos) {
-        return __android_log_pmsg_file_write(logId, prio, filename, buf, len);
-    }
+  if (LAST_KMSG_FILTER.find(sub) == std::string::npos &&
+      LAST_LOG_FILTER.find(sub) == std::string::npos) {
+    return __android_log_pmsg_file_write(id, prio, filename, buf, len);
+  }
 
-    // filename rotation
-    if (dot == std::string::npos) {
-        name += ".1";
+  // filename rotation
+  if (dot == std::string::npos) {
+    name += ".1";
+  } else {
+    std::string number = name.substr(dot + 1);
+    if (!isdigit(number[0])) {
+      name += ".1";
     } else {
-        std::string number = name.substr(dot + 1);
-        if (!isdigit(number[0])) {
-            name += ".1";
-        } else {
-            size_t i;
-            if (!android::base::ParseUint(number, &i)) {
-                LOG(ERROR) << "failed to parse uint in " << number;
-                return -1;
-            }
-            name = sub + "." + std::to_string(i + 1);
-        }
+      size_t i;
+      if (!android::base::ParseUint(number, &i)) {
+        LOG(ERROR) << "failed to parse uint in " << number;
+        return -1;
+      }
+      name = sub + "." + std::to_string(i + 1);
     }
+  }
 
-    return __android_log_pmsg_file_write(logId, prio, name.c_str(), buf, len);
+  return __android_log_pmsg_file_write(id, prio, name.c_str(), buf, len);
 }
 
 // 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.
 void rotate_logs(const char* last_log_file, const char* last_kmsg_file) {
-    // Logs should only be rotated once.
-    static bool rotated = false;
-    if (rotated) {
-        return;
-    }
-    rotated = true;
+  // Logs should only be rotated once.
+  static bool rotated = false;
+  if (rotated) {
+    return;
+  }
+  rotated = true;
 
-    for (int i = KEEP_LOG_COUNT - 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());
+  for (int i = KEEP_LOG_COUNT - 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());
+  }
 }
diff --git a/rotate_logs.h b/rotate_logs.h
index 809c213..007c33d 100644
--- a/rotate_logs.h
+++ b/rotate_logs.h
@@ -17,24 +17,18 @@
 #ifndef _ROTATE_LOGS_H
 #define _ROTATE_LOGS_H
 
-#include <string>
+#include <stddef.h>
+#include <sys/types.h>
 
-#include <private/android_logger.h> /* private pmsg functions */
+#include <log/log_id.h>
 
-constexpr int KEEP_LOG_COUNT = 10;
+static constexpr int KEEP_LOG_COUNT = 10;
 
-ssize_t logbasename(log_id_t /* logId */,
-        char /* prio */,
-        const char *filename,
-        const char * /* buf */, size_t len,
-        void *arg);
+ssize_t logbasename(log_id_t id, char prio, const char* filename, const char* buf, size_t len,
+                    void* arg);
 
-ssize_t logrotate(
-        log_id_t logId,
-        char prio,
-        const char *filename,
-        const char *buf, size_t len,
-        void *arg);
+ssize_t logrotate(log_id_t id, char prio, const char* filename, const char* buf, size_t len,
+                  void* arg);
 
 // Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max.
 // Similarly rename last_kmsg -> last_kmsg.1 -> ... -> last_kmsg.$max.
diff --git a/screen_ui.cpp b/screen_ui.cpp
index b8f6ea2..c8fb5aa 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include "screen_ui.h"
+
 #include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -29,18 +31,19 @@
 #include <time.h>
 #include <unistd.h>
 
+#include <memory>
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 #include <android-base/logging.h>
 #include <android-base/properties.h>
-#include <android-base/strings.h>
 #include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <minui/minui.h>
 
 #include "common.h"
 #include "device.h"
-#include "minui/minui.h"
-#include "screen_ui.h"
 #include "ui.h"
 
 // Return the current time as a double (including fractions of a second).
@@ -54,7 +57,7 @@
     : kMarginWidth(RECOVERY_UI_MARGIN_WIDTH),
       kMarginHeight(RECOVERY_UI_MARGIN_HEIGHT),
       kAnimationFps(RECOVERY_UI_ANIMATION_FPS),
-      density_(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
+      kDensity(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
       currentIcon(NONE),
       progressBarType(EMPTY),
       progressScopeStart(0),
@@ -66,7 +69,6 @@
       text_(nullptr),
       text_col_(0),
       text_row_(0),
-      text_top_(0),
       show_text(false),
       show_text_ever(false),
       menu_headers_(nullptr),
@@ -80,6 +82,8 @@
       intro_done(false),
       stage(-1),
       max_stage(-1),
+      locale_(""),
+      rtl_locale_(false),
       updateMutex(PTHREAD_MUTEX_INITIALIZER) {}
 
 GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
@@ -105,7 +109,7 @@
 }
 
 int ScreenRecoveryUI::PixelsFromDp(int dp) const {
-  return dp * density_;
+  return dp * kDensity;
 }
 
 // Here's the intended layout:
@@ -145,8 +149,8 @@
   int elements_sum = gr_get_height(loopFrames[0]) + PixelsFromDp(kLayouts[layout_][ICON]) +
                      gr_get_height(installing_text) + PixelsFromDp(kLayouts[layout_][TEXT]) +
                      gr_get_height(progressBarFill);
-  int bottom_gap = (gr_fb_height() - elements_sum) / 2;
-  return gr_fb_height() - bottom_gap - gr_get_height(progressBarFill);
+  int bottom_gap = (ScreenHeight() - elements_sum) / 2;
+  return ScreenHeight() - bottom_gap - gr_get_height(progressBarFill);
 }
 
 // Clear the screen and draw the currently selected background icon (if any).
@@ -155,25 +159,24 @@
   pagesIdentical = false;
   gr_color(0, 0, 0, 255);
   gr_clear();
-
   if (currentIcon != NONE) {
     if (max_stage != -1) {
       int stage_height = gr_get_height(stageMarkerEmpty);
       int stage_width = gr_get_width(stageMarkerEmpty);
-      int x = (gr_fb_width() - max_stage * gr_get_width(stageMarkerEmpty)) / 2;
-      int y = gr_fb_height() - stage_height;
+      int x = (ScreenWidth() - max_stage * gr_get_width(stageMarkerEmpty)) / 2;
+      int y = ScreenHeight() - stage_height - kMarginHeight;
       for (int i = 0; i < max_stage; ++i) {
         GRSurface* stage_surface = (i < stage) ? stageMarkerFill : stageMarkerEmpty;
-        gr_blit(stage_surface, 0, 0, stage_width, stage_height, x, y);
+        DrawSurface(stage_surface, 0, 0, stage_width, stage_height, x, y);
         x += stage_width;
       }
     }
 
     GRSurface* text_surface = GetCurrentText();
-    int text_x = (gr_fb_width() - gr_get_width(text_surface)) / 2;
+    int text_x = (ScreenWidth() - gr_get_width(text_surface)) / 2;
     int text_y = GetTextBaseline();
     gr_color(255, 255, 255, 255);
-    gr_texticon(text_x, text_y, text_surface);
+    DrawTextIcon(text_x, text_y, text_surface);
   }
 }
 
@@ -184,21 +187,21 @@
     GRSurface* frame = GetCurrentFrame();
     int frame_width = gr_get_width(frame);
     int frame_height = gr_get_height(frame);
-    int frame_x = (gr_fb_width() - frame_width) / 2;
+    int frame_x = (ScreenWidth() - frame_width) / 2;
     int frame_y = GetAnimationBaseline();
-    gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
+    DrawSurface(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
   }
 
   if (progressBarType != EMPTY) {
     int width = gr_get_width(progressBarEmpty);
     int height = gr_get_height(progressBarEmpty);
 
-    int progress_x = (gr_fb_width() - width) / 2;
+    int progress_x = (ScreenWidth() - width) / 2;
     int progress_y = GetProgressBaseline();
 
     // Erase behind the progress bar (in case this was a progress-only update)
     gr_color(0, 0, 0, 255);
-    gr_fill(progress_x, progress_y, width, height);
+    DrawFill(progress_x, progress_y, width, height);
 
     if (progressBarType == DETERMINATE) {
       float p = progressScopeStart + progress * progressScopeSize;
@@ -207,19 +210,19 @@
       if (rtl_locale_) {
         // Fill the progress bar from right to left.
         if (pos > 0) {
-          gr_blit(progressBarFill, width - pos, 0, pos, height, progress_x + width - pos,
-                  progress_y);
+          DrawSurface(progressBarFill, width - pos, 0, pos, height, progress_x + width - pos,
+                      progress_y);
         }
         if (pos < width - 1) {
-          gr_blit(progressBarEmpty, 0, 0, width - pos, height, progress_x, progress_y);
+          DrawSurface(progressBarEmpty, 0, 0, width - pos, height, progress_x, progress_y);
         }
       } else {
         // Fill the progress bar from left to right.
         if (pos > 0) {
-          gr_blit(progressBarFill, 0, 0, pos, height, progress_x, progress_y);
+          DrawSurface(progressBarFill, 0, 0, pos, height, progress_x, progress_y);
         }
         if (pos < width - 1) {
-          gr_blit(progressBarEmpty, pos, 0, width - pos, height, progress_x + pos, progress_y);
+          DrawSurface(progressBarEmpty, pos, 0, width - pos, height, progress_x + pos, progress_y);
         }
       }
     }
@@ -256,8 +259,96 @@
   }
 }
 
+void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string>& locales_entries,
+                                                   size_t sel) {
+  SetLocale(locales_entries[sel]);
+  std::vector<std::string> text_name = { "erasing_text", "error_text", "installing_text",
+                                         "installing_security_text", "no_command_text" };
+  std::unordered_map<std::string, std::unique_ptr<GRSurface, decltype(&free)>> surfaces;
+  for (const auto& name : text_name) {
+    GRSurface* text_image = nullptr;
+    LoadLocalizedBitmap(name.c_str(), &text_image);
+    if (!text_image) {
+      Print("Failed to load %s\n", name.c_str());
+      return;
+    }
+    surfaces.emplace(name, std::unique_ptr<GRSurface, decltype(&free)>(text_image, &free));
+  }
+
+  pthread_mutex_lock(&updateMutex);
+  gr_color(0, 0, 0, 255);
+  gr_clear();
+
+  int text_y = kMarginHeight;
+  int text_x = kMarginWidth;
+  int line_spacing = gr_sys_font()->char_height;  // Put some extra space between images.
+  // Write the header and descriptive texts.
+  SetColor(INFO);
+  std::string header = "Show background text image";
+  text_y += DrawTextLine(text_x, text_y, header.c_str(), true);
+  std::string locale_selection = android::base::StringPrintf(
+      "Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel, locales_entries.size());
+  const char* instruction[] = { locale_selection.c_str(),
+                                "Use volume up/down to switch locales and power to exit.",
+                                nullptr };
+  text_y += DrawWrappedTextLines(text_x, text_y, instruction);
+
+  // Iterate through the text images and display them in order for the current locale.
+  for (const auto& p : surfaces) {
+    text_y += line_spacing;
+    SetColor(LOG);
+    text_y += DrawTextLine(text_x, text_y, p.first.c_str(), false);
+    gr_color(255, 255, 255, 255);
+    gr_texticon(text_x, text_y, p.second.get());
+    text_y += gr_get_height(p.second.get());
+  }
+  // Update the whole screen.
+  gr_flip();
+  pthread_mutex_unlock(&updateMutex);
+}
+
+void ScreenRecoveryUI::CheckBackgroundTextImages(const std::string& saved_locale) {
+  // Load a list of locales embedded in one of the resource files.
+  std::vector<std::string> locales_entries = get_locales_in_png("installing_text");
+  if (locales_entries.empty()) {
+    Print("Failed to load locales from the resource files\n");
+    return;
+  }
+  size_t selected = 0;
+  SelectAndShowBackgroundText(locales_entries, selected);
+
+  FlushKeys();
+  while (true) {
+    int key = WaitKey();
+    if (key == KEY_POWER || key == KEY_ENTER) {
+      break;
+    } else if (key == KEY_UP || key == KEY_VOLUMEUP) {
+      selected = (selected == 0) ? locales_entries.size() - 1 : selected - 1;
+      SelectAndShowBackgroundText(locales_entries, selected);
+    } else if (key == KEY_DOWN || key == KEY_VOLUMEDOWN) {
+      selected = (selected == locales_entries.size() - 1) ? 0 : selected + 1;
+      SelectAndShowBackgroundText(locales_entries, selected);
+    }
+  }
+
+  SetLocale(saved_locale);
+}
+
+int ScreenRecoveryUI::ScreenWidth() const {
+  return gr_fb_width();
+}
+
+int ScreenRecoveryUI::ScreenHeight() const {
+  return gr_fb_height();
+}
+
+void ScreenRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                                   int dy) const {
+  gr_blit(surface, sx, sy, w, h, dx, dy);
+}
+
 int ScreenRecoveryUI::DrawHorizontalRule(int y) const {
-  gr_fill(0, y + 4, gr_fb_width(), y + 6);
+  gr_fill(0, y + 4, ScreenWidth(), y + 6);
   return 8;
 }
 
@@ -265,6 +356,14 @@
   gr_fill(x, y, x + width, y + height);
 }
 
+void ScreenRecoveryUI::DrawFill(int x, int y, int w, int h) const {
+  gr_fill(x, y, w, h);
+}
+
+void ScreenRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
+  gr_texticon(x, y, surface);
+}
+
 int ScreenRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
   gr_text(gr_sys_font(), x, y, line, bold);
   return char_height_ + 4;
@@ -353,7 +452,7 @@
       if (i == menu_sel) {
         // Draw the highlight bar.
         SetColor(IsLongPress() ? MENU_SEL_BG_ACTIVE : MENU_SEL_BG);
-        DrawHighlightBar(0, y - 2, gr_fb_width(), char_height_ + 4);
+        DrawHighlightBar(0, y - 2, ScreenWidth(), char_height_ + 4);
         // Bold white text for the selected item.
         SetColor(MENU_SEL_FG);
         y += DrawTextLine(x, y, menu_[i].c_str(), true);
@@ -368,9 +467,9 @@
   // 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_;
+  int row = text_row_;
   size_t count = 0;
-  for (int ty = gr_fb_height() - kMarginHeight - char_height_; ty >= y && count < text_rows_;
+  for (int ty = ScreenHeight() - kMarginHeight - char_height_; ty >= y && count < text_rows_;
        ty -= char_height_, ++count) {
     DrawTextLine(kMarginWidth, ty, text_[row], false);
     --row;
@@ -490,13 +589,14 @@
   }
 
   gr_font_size(gr_sys_font(), &char_width_, &char_height_);
-  text_rows_ = (gr_fb_height() - kMarginHeight * 2) / char_height_;
-  text_cols_ = (gr_fb_width() - kMarginWidth * 2) / char_width_;
+  text_rows_ = (ScreenHeight() - kMarginHeight * 2) / char_height_;
+  text_cols_ = (ScreenWidth() - kMarginWidth * 2) / char_width_;
   return true;
 }
 
 bool ScreenRecoveryUI::Init(const std::string& locale) {
   RecoveryUI::Init(locale);
+
   if (!InitTextParams()) {
     return false;
   }
@@ -510,7 +610,9 @@
   file_viewer_text_ = Alloc2d(text_rows_, text_cols_ + 1);
 
   text_col_ = text_row_ = 0;
-  text_top_ = 1;
+
+  // Set up the locale info.
+  SetLocale(locale);
 
   LoadBitmap("icon_error", &error_icon);
 
@@ -643,7 +745,6 @@
         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;
     }
@@ -673,8 +774,6 @@
   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);
 }
@@ -683,7 +782,6 @@
   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);
   }
@@ -750,7 +848,6 @@
   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_;
@@ -762,7 +859,6 @@
   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,
@@ -841,3 +937,23 @@
   // will change color to indicate a successful long press.
   Redraw();
 }
+
+void ScreenRecoveryUI::SetLocale(const std::string& new_locale) {
+  locale_ = new_locale;
+  rtl_locale_ = false;
+
+  if (!new_locale.empty()) {
+    size_t underscore = new_locale.find('_');
+    // lang has the language prefix prior to '_', or full string if '_' doesn't exist.
+    std::string lang = new_locale.substr(0, underscore);
+
+    // A bit cheesy: keep an explicit list of supported RTL languages.
+    if (lang == "ar" ||  // Arabic
+        lang == "fa" ||  // Persian (Farsi)
+        lang == "he" ||  // Hebrew (new language code)
+        lang == "iw" ||  // Hebrew (old language code)
+        lang == "ur") {  // Urdu
+      rtl_locale_ = true;
+    }
+  }
+}
diff --git a/screen_ui.h b/screen_ui.h
index 8231a2b..f05761c 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -32,6 +32,17 @@
 // (shows an icon + a progress bar, text logging, menu, etc.)
 class ScreenRecoveryUI : public RecoveryUI {
  public:
+  enum UIElement {
+    HEADER,
+    MENU,
+    MENU_SEL_BG,
+    MENU_SEL_BG_ACTIVE,
+    MENU_SEL_FG,
+    LOG,
+    TEXT_FILL,
+    INFO
+  };
+
   ScreenRecoveryUI();
 
   bool Init(const std::string& locale) override;
@@ -67,18 +78,12 @@
 
   void Redraw();
 
-  enum UIElement {
-    HEADER,
-    MENU,
-    MENU_SEL_BG,
-    MENU_SEL_BG_ACTIVE,
-    MENU_SEL_FG,
-    LOG,
-    TEXT_FILL,
-    INFO
-  };
   void SetColor(UIElement e) const;
 
+  // Check the background text image. Use volume up/down button to cycle through the locales
+  // embedded in the png file, and power button to go back to recovery main menu.
+  void CheckBackgroundTextImages(const std::string& saved_locale);
+
  protected:
   // The margin that we don't want to use for showing texts (e.g. round screen, or screen with
   // rounded corners).
@@ -89,7 +94,58 @@
   const int kAnimationFps;
 
   // The scale factor from dp to pixels. 1.0 for mdpi, 4.0 for xxxhdpi.
-  const float density_;
+  const float kDensity;
+
+  virtual bool InitTextParams();
+
+  virtual void draw_background_locked();
+  virtual void draw_foreground_locked();
+  virtual void draw_screen_locked();
+  virtual void update_screen_locked();
+  virtual void update_progress_locked();
+
+  GRSurface* GetCurrentFrame() const;
+  GRSurface* GetCurrentText() const;
+
+  static void* ProgressThreadStartRoutine(void* data);
+  void ProgressThreadLoop();
+
+  virtual void ShowFile(FILE*);
+  virtual void PrintV(const char*, bool, va_list);
+  void PutChar(char);
+  void ClearText();
+
+  void LoadAnimation();
+  void LoadBitmap(const char* filename, GRSurface** surface);
+  void LoadLocalizedBitmap(const char* filename, GRSurface** surface);
+
+  int PixelsFromDp(int dp) const;
+  virtual int GetAnimationBaseline() const;
+  virtual int GetProgressBaseline() const;
+  virtual int GetTextBaseline() const;
+
+  // Returns pixel width of draw buffer.
+  virtual int ScreenWidth() const;
+  // Returns pixel height of draw buffer.
+  virtual int ScreenHeight() const;
+
+  // Draws a highlight bar at (x, y) - (x + width, y + height).
+  virtual void DrawHighlightBar(int x, int y, int width, int height) const;
+  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
+  virtual int DrawHorizontalRule(int y) const;
+  // Draws a line of text. Returns the offset it should be moving along Y-axis.
+  virtual int DrawTextLine(int x, int y, const char* line, bool bold) const;
+  // Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
+  virtual void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const;
+  // Draws rectangle at (x, y) - (x + w, y + h).
+  virtual void DrawFill(int x, int y, int w, int h) const;
+  // Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
+  virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
+  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
+  int DrawTextLines(int x, int y, const char* const* lines) const;
+  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
+  // Returns the offset it should be moving along Y-axis.
+  int DrawWrappedTextLines(int x, int y, const char* const* lines) const;
 
   Icon currentIcon;
 
@@ -123,7 +179,7 @@
 
   // Log text overlay, displayed when a magic key is pressed.
   char** text_;
-  size_t text_col_, text_row_, text_top_;
+  size_t text_col_, text_row_;
 
   bool show_text;
   bool show_text_ever;  // has show_text ever been true?
@@ -150,47 +206,18 @@
   int char_width_;
   int char_height_;
 
+  // The locale that's used to show the rendered texts.
+  std::string locale_;
+  bool rtl_locale_;
+
   pthread_mutex_t updateMutex;
 
-  virtual bool InitTextParams();
+ private:
+  void SetLocale(const std::string&);
 
-  virtual void draw_background_locked();
-  virtual void draw_foreground_locked();
-  virtual void draw_screen_locked();
-  virtual void update_screen_locked();
-  virtual void update_progress_locked();
-
-  GRSurface* GetCurrentFrame() const;
-  GRSurface* GetCurrentText() const;
-
-  static void* ProgressThreadStartRoutine(void* data);
-  void ProgressThreadLoop();
-
-  virtual void ShowFile(FILE*);
-  virtual void PrintV(const char*, bool, va_list);
-  void PutChar(char);
-  void ClearText();
-
-  void LoadAnimation();
-  void LoadBitmap(const char* filename, GRSurface** surface);
-  void LoadLocalizedBitmap(const char* filename, GRSurface** surface);
-
-  int PixelsFromDp(int dp) const;
-  virtual int GetAnimationBaseline() const;
-  virtual int GetProgressBaseline() const;
-  virtual int GetTextBaseline() const;
-
-  // Draws a highlight bar at (x, y) - (x + width, y + height).
-  virtual void DrawHighlightBar(int x, int y, int width, int height) const;
-  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
-  virtual int DrawHorizontalRule(int y) const;
-  // Draws a line of text. Returns the offset it should be moving along Y-axis.
-  virtual int DrawTextLine(int x, int y, const char* line, bool bold) const;
-  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
-  int DrawTextLines(int x, int y, const char* const* lines) const;
-  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
-  // Returns the offset it should be moving along Y-axis.
-  int DrawWrappedTextLines(int x, int y, const char* const* lines) const;
+  // Display the background texts for "erasing", "error", "no_command" and "installing" for the
+  // selected locale.
+  void SelectAndShowBackgroundText(const std::vector<std::string>& locales_entries, size_t sel);
 };
 
 #endif  // RECOVERY_UI_H
diff --git a/stub_ui.h b/stub_ui.h
index 85dbcfd..1f6b29a 100644
--- a/stub_ui.h
+++ b/stub_ui.h
@@ -24,18 +24,18 @@
  public:
   StubRecoveryUI() = default;
 
-  void SetBackground(Icon icon) override {}
-  void SetSystemUpdateText(bool security_update) override {}
+  void SetBackground(Icon /* icon */) override {}
+  void SetSystemUpdateText(bool /* security_update */) override {}
 
   // progress indicator
-  void SetProgressType(ProgressType type) override {}
-  void ShowProgress(float portion, float seconds) override {}
-  void SetProgress(float fraction) override {}
+  void SetProgressType(ProgressType /* type */) override {}
+  void ShowProgress(float /* portion */, float /* seconds */) override {}
+  void SetProgress(float /* fraction */) override {}
 
-  void SetStage(int current, int max) override {}
+  void SetStage(int /* current */, int /* max */) override {}
 
   // text log
-  void ShowText(bool visible) override {}
+  void ShowText(bool /* visible */) override {}
   bool IsTextVisible() override {
     return false;
   }
@@ -50,12 +50,12 @@
     vprintf(fmt, ap);
     va_end(ap);
   }
-  void PrintOnScreenOnly(const char* fmt, ...) override {}
-  void ShowFile(const char* filename) override {}
+  void PrintOnScreenOnly(const char* /* fmt */, ...) override {}
+  void ShowFile(const char* /* filename */) override {}
 
   // menu display
-  void StartMenu(const char* const* headers, const char* const* items,
-                 int initial_selection) override {}
+  void StartMenu(const char* const* /* headers */, const char* const* /* items */,
+                 int /* initial_selection */) override {}
   int SelectMenu(int sel) override {
     return sel;
   }
diff --git a/tests/Android.mk b/tests/Android.mk
index f2497b8..b3584fe 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -18,7 +18,7 @@
 
 # Unit tests
 include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_MODULE := recovery_unit_test
 LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_STATIC_LIBRARIES := \
@@ -30,7 +30,8 @@
     libutils \
     libz \
     libselinux \
-    libbase
+    libbase \
+    libBionicGtestMain
 
 LOCAL_SRC_FILES := \
     unit/asn1_decoder_test.cpp \
@@ -46,11 +47,12 @@
 
 # Manual tests
 include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_MODULE := recovery_manual_test
 LOCAL_STATIC_LIBRARIES := \
     libminui \
-    libbase
+    libbase \
+    libBionicGtestMain
 
 LOCAL_SRC_FILES := manual/recovery_test.cpp
 LOCAL_SHARED_LIBRARIES := \
@@ -81,6 +83,7 @@
 # Component tests
 include $(CLEAR_VARS)
 LOCAL_CFLAGS := \
+    -Wall \
     -Werror \
     -D_FILE_OFFSET_BITS=64
 
@@ -142,9 +145,9 @@
     libdivsufsort \
     libdivsufsort64 \
     libfs_mgr \
-    liblog \
     libvintf_recovery \
     libvintf \
+    libhidl-gen-utils \
     libtinyxml2 \
     libselinux \
     libext4_utils \
@@ -153,6 +156,7 @@
     libcrypto \
     libbz \
     libziparchive \
+    liblog \
     libutils \
     libz \
     libbase \
@@ -162,6 +166,7 @@
     libsquashfs_utils \
     libcutils \
     libbrotli \
+    libBionicGtestMain \
     $(tune2fs_static_libraries)
 
 testdata_files := $(call find-subdir-files, testdata/*)
@@ -191,7 +196,7 @@
 
 # Host tests
 include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Werror
+LOCAL_CFLAGS := -Wall -Werror
 LOCAL_MODULE := recovery_host_test
 LOCAL_MODULE_HOST_OS := linux
 LOCAL_C_INCLUDES := bootable/recovery
@@ -200,16 +205,19 @@
 LOCAL_STATIC_LIBRARIES := \
     libimgdiff \
     libimgpatch \
+    libotautil \
     libbsdiff \
     libbspatch \
     libziparchive \
     libutils \
     libbase \
     libcrypto \
+    libbrotli \
     libbz \
     libdivsufsort64 \
     libdivsufsort \
-    libz
+    libz \
+    libBionicGtestMain
 LOCAL_SHARED_LIBRARIES := \
     liblog
 include $(BUILD_HOST_NATIVE_TEST)
diff --git a/tests/common/test_constants.h b/tests/common/test_constants.h
index f6b6922..514818e 100644
--- a/tests/common/test_constants.h
+++ b/tests/common/test_constants.h
@@ -19,6 +19,8 @@
 
 #include <stdlib.h>
 
+#include <string>
+
 // Zip entries in ziptest_valid.zip.
 static const std::string kATxtContents("abcdefghabcdefgh\n");
 static const std::string kBTxtContents("abcdefgh\n");
@@ -30,10 +32,14 @@
 // echo -n -e "abcdefgh\n" | sha1sum
 static const std::string kBTxtSha1Sum("e414af7161c9554089f4106d6f1797ef14a73666");
 
-static const char* data_root = getenv("ANDROID_DATA");
-
 static std::string from_testdata_base(const std::string& fname) {
-  return std::string(data_root) + "/nativetest/recovery/testdata/" + fname;
+#ifdef __ANDROID__
+  static std::string data_root = getenv("ANDROID_DATA");
+#else
+  static std::string data_root = std::string(getenv("ANDROID_PRODUCT_OUT")) + "/data";
+#endif
+
+  return data_root + "/nativetest/recovery/testdata/" + fname;
 }
 
 #endif  // _OTA_TEST_CONSTANTS_H
diff --git a/tests/component/applypatch_test.cpp b/tests/component/applypatch_test.cpp
index 016fed9..61e06ad 100644
--- a/tests/component/applypatch_test.cpp
+++ b/tests/component/applypatch_test.cpp
@@ -30,12 +30,16 @@
 #include <android-base/file.h>
 #include <android-base/stringprintf.h>
 #include <android-base/test_utils.h>
+#include <bsdiff/bsdiff.h>
 #include <openssl/sha.h>
 
 #include "applypatch/applypatch.h"
 #include "applypatch/applypatch_modes.h"
 #include "common/test_constants.h"
-#include "print_sha1.h"
+#include "otautil/cache_location.h"
+#include "otautil/print_sha1.h"
+
+using namespace std::string_literals;
 
 static void sha1sum(const std::string& fname, std::string* sha1, size_t* fsize = nullptr) {
   ASSERT_NE(nullptr, sha1);
@@ -53,34 +57,20 @@
 }
 
 static void mangle_file(const std::string& fname) {
-  std::string content;
-  content.reserve(1024);
+  std::string content(1024, '\0');
   for (size_t i = 0; i < 1024; i++) {
     content[i] = rand() % 256;
   }
   ASSERT_TRUE(android::base::WriteStringToFile(content, fname));
 }
 
-static bool file_cmp(const std::string& f1, const std::string& f2) {
-  std::string c1;
-  android::base::ReadFileToString(f1, &c1);
-  std::string c2;
-  android::base::ReadFileToString(f2, &c2);
-  return c1 == c2;
-}
-
 class ApplyPatchTest : public ::testing::Test {
  public:
-  static void SetUpTestCase() {
+  virtual void SetUp() override {
     // set up files
     old_file = from_testdata_base("old.file");
     new_file = from_testdata_base("new.file");
-    patch_file = from_testdata_base("patch.bsdiff");
-    rand_file = "/cache/applypatch_test_rand.file";
-    cache_file = "/cache/saved.file";
-
-    // write stuff to rand_file
-    ASSERT_TRUE(android::base::WriteStringToFile("hello", rand_file));
+    nonexistent_file = from_testdata_base("nonexistent.file");
 
     // set up SHA constants
     sha1sum(old_file, &old_sha1, &old_size);
@@ -90,56 +80,35 @@
     bad_sha1_b = android::base::StringPrintf("%040x", rand());
   }
 
-  static std::string old_file;
-  static std::string new_file;
-  static std::string rand_file;
-  static std::string cache_file;
-  static std::string patch_file;
+  std::string old_file;
+  std::string new_file;
+  std::string nonexistent_file;
 
-  static std::string old_sha1;
-  static std::string new_sha1;
-  static std::string bad_sha1_a;
-  static std::string bad_sha1_b;
+  std::string old_sha1;
+  std::string new_sha1;
+  std::string bad_sha1_a;
+  std::string bad_sha1_b;
 
-  static size_t old_size;
-  static size_t new_size;
+  size_t old_size;
+  size_t new_size;
 };
 
-static void cp(const std::string& src, const std::string& tgt) {
-  std::string cmd = "cp " + src + " " + tgt;
-  system(cmd.c_str());
-}
-
-static void backup_old() {
-  cp(ApplyPatchTest::old_file, ApplyPatchTest::cache_file);
-}
-
-static void restore_old() {
-  cp(ApplyPatchTest::cache_file, ApplyPatchTest::old_file);
-}
-
 class ApplyPatchCacheTest : public ApplyPatchTest {
- public:
-  virtual void SetUp() {
-    backup_old();
-  }
-
-  virtual void TearDown() {
-    restore_old();
+ protected:
+  void SetUp() override {
+    ApplyPatchTest::SetUp();
+    CacheLocation::location().set_cache_temp_source(old_file);
   }
 };
 
-std::string ApplyPatchTest::old_file;
-std::string ApplyPatchTest::new_file;
-std::string ApplyPatchTest::rand_file;
-std::string ApplyPatchTest::patch_file;
-std::string ApplyPatchTest::cache_file;
-std::string ApplyPatchTest::old_sha1;
-std::string ApplyPatchTest::new_sha1;
-std::string ApplyPatchTest::bad_sha1_a;
-std::string ApplyPatchTest::bad_sha1_b;
-size_t ApplyPatchTest::old_size;
-size_t ApplyPatchTest::new_size;
+class ApplyPatchModesTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    CacheLocation::location().set_cache_temp_source(cache_source.path);
+  }
+
+  TemporaryFile cache_source;
+};
 
 TEST_F(ApplyPatchTest, CheckModeSkip) {
   std::vector<std::string> sha1s;
@@ -197,43 +166,31 @@
   ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
 }
 
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSingle) {
-  mangle_file(old_file);
-  std::vector<std::string> sha1s = { old_sha1 };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
+TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceSingle) {
+  TemporaryFile temp_file;
+  mangle_file(temp_file.path);
+  std::vector<std::string> sha1s_single = { old_sha1 };
+  ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_single));
+  ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_single));
 }
 
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedMultiple) {
-  mangle_file(old_file);
-  std::vector<std::string> sha1s = { bad_sha1_a, old_sha1, bad_sha1_b };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
+TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceMultiple) {
+  TemporaryFile temp_file;
+  mangle_file(temp_file.path);
+  std::vector<std::string> sha1s_multiple = { bad_sha1_a, old_sha1, bad_sha1_b };
+  ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_multiple));
+  ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_multiple));
 }
 
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedFailure) {
-  mangle_file(old_file);
-  std::vector<std::string> sha1s = { bad_sha1_a, bad_sha1_b };
-  ASSERT_NE(0, applypatch_check(&old_file[0], sha1s));
+TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceFailure) {
+  TemporaryFile temp_file;
+  mangle_file(temp_file.path);
+  std::vector<std::string> sha1s_failure = { bad_sha1_a, bad_sha1_b };
+  ASSERT_NE(0, applypatch_check(temp_file.path, sha1s_failure));
+  ASSERT_NE(0, applypatch_check(nonexistent_file.c_str(), sha1s_failure));
 }
 
-TEST_F(ApplyPatchCacheTest, CheckCacheMissingSingle) {
-  unlink(&old_file[0]);
-  std::vector<std::string> sha1s = { old_sha1 };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchCacheTest, CheckCacheMissingMultiple) {
-  unlink(&old_file[0]);
-  std::vector<std::string> sha1s = { bad_sha1_a, old_sha1, bad_sha1_b };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchCacheTest, CheckCacheMissingFailure) {
-  unlink(&old_file[0]);
-  std::vector<std::string> sha1s = { bad_sha1_a, bad_sha1_b };
-  ASSERT_NE(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST(ApplyPatchModesTest, InvalidArgs) {
+TEST_F(ApplyPatchModesTest, InvalidArgs) {
   // At least two args (including the filename).
   ASSERT_EQ(2, applypatch_modes(1, (const char* []){ "applypatch" }));
 
@@ -241,7 +198,7 @@
   ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-x" }));
 }
 
-TEST(ApplyPatchModesTest, PatchModeEmmcTarget) {
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
   std::string boot_img = from_testdata_base("boot.img");
   size_t boot_img_size;
   std::string boot_img_sha1;
@@ -311,7 +268,55 @@
   ASSERT_EQ(0, applypatch_modes(args3.size(), args3.data()));
 }
 
-TEST(ApplyPatchModesTest, PatchModeInvalidArgs) {
+// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) {
+  std::string boot_img_file = from_testdata_base("boot.img");
+  std::string boot_img_sha1;
+  size_t boot_img_size;
+  sha1sum(boot_img_file, &boot_img_sha1, &boot_img_size);
+
+  std::string recovery_img_file = from_testdata_base("recovery.img");
+  std::string recovery_img_sha1;
+  size_t recovery_img_size;
+  sha1sum(recovery_img_file, &recovery_img_sha1, &recovery_img_size);
+
+  // Generate the bsdiff patch of recovery-from-boot.p.
+  std::string src_content;
+  ASSERT_TRUE(android::base::ReadFileToString(boot_img_file, &src_content));
+
+  std::string tgt_content;
+  ASSERT_TRUE(android::base::ReadFileToString(recovery_img_file, &tgt_content));
+
+  TemporaryFile patch_file;
+  ASSERT_EQ(0,
+            bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(),
+                           reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(),
+                           patch_file.path, nullptr));
+
+  // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
+  std::string src_file_arg =
+      "EMMC:" + boot_img_file + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1;
+  TemporaryFile tgt_file;
+  std::string tgt_file_arg = "EMMC:"s + tgt_file.path;
+  std::string recovery_img_size_arg = std::to_string(recovery_img_size);
+  std::string patch_arg = boot_img_sha1 + ":" + patch_file.path;
+  std::vector<const char*> args = { "applypatch",
+                                    src_file_arg.c_str(),
+                                    tgt_file_arg.c_str(),
+                                    recovery_img_sha1.c_str(),
+                                    recovery_img_size_arg.c_str(),
+                                    patch_arg.c_str() };
+  ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
+
+  // Double check the patched recovery image.
+  std::string tgt_file_sha1;
+  size_t tgt_file_size;
+  sha1sum(tgt_file.path, &tgt_file_sha1, &tgt_file_size);
+  ASSERT_EQ(recovery_img_size, tgt_file_size);
+  ASSERT_EQ(recovery_img_sha1, tgt_file_sha1);
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) {
   // Invalid bonus file.
   ASSERT_NE(0, applypatch_modes(3, (const char* []){ "applypatch", "-b", "/doesntexist" }));
 
@@ -372,11 +377,11 @@
   ASSERT_NE(0, applypatch_modes(args6.size(), args6.data()));
 }
 
-TEST(ApplyPatchModesTest, CheckModeInvalidArgs) {
+TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
   // Insufficient args.
   ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-c" }));
 }
 
-TEST(ApplyPatchModesTest, ShowLicenses) {
+TEST_F(ApplyPatchModesTest, ShowLicenses) {
   ASSERT_EQ(0, applypatch_modes(2, (const char* []){ "applypatch", "-l" }));
 }
diff --git a/tests/component/bootloader_message_test.cpp b/tests/component/bootloader_message_test.cpp
index b38bc71..6cc59a4 100644
--- a/tests/component/bootloader_message_test.cpp
+++ b/tests/component/bootloader_message_test.cpp
@@ -18,53 +18,12 @@
 #include <vector>
 
 #include <android-base/strings.h>
+#include <android-base/test_utils.h>
 #include <bootloader_message/bootloader_message.h>
 #include <gtest/gtest.h>
 
-class BootloaderMessageTest : public ::testing::Test {
- protected:
-  BootloaderMessageTest() : has_misc(true) {}
-
-  virtual void SetUp() override {
-    std::string err;
-    has_misc = !get_bootloader_message_blk_device(&err).empty();
-  }
-
-  virtual void TearDown() override {
-    // Clear the BCB.
-    if (has_misc) {
-      std::string err;
-      ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
-    }
-  }
-
-  bool has_misc;
-};
-
-TEST_F(BootloaderMessageTest, clear_bootloader_message) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
-
-  // Clear the BCB.
-  std::string err;
-  ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
-
-  // Verify the content.
-  bootloader_message boot;
-  ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
-
-  // All the bytes should be cleared.
-  ASSERT_EQ(std::string(sizeof(boot), '\0'),
-            std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)));
-}
-
-TEST_F(BootloaderMessageTest, read_and_write_bootloader_message) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
+TEST(BootloaderMessageTest, read_and_write_bootloader_message) {
+  TemporaryFile temp_misc;
 
   // Write the BCB.
   bootloader_message boot = {};
@@ -73,90 +32,71 @@
   strlcpy(boot.status, "status1", sizeof(boot.status));
 
   std::string err;
-  ASSERT_TRUE(write_bootloader_message(boot, &err)) << "Failed to write BCB: " << err;
+  ASSERT_TRUE(write_bootloader_message_to(boot, temp_misc.path, &err))
+      << "Failed to write BCB: " << err;
 
   // Read and verify.
   bootloader_message boot_verify;
-  ASSERT_TRUE(read_bootloader_message(&boot_verify, &err)) << "Failed to read BCB: " << err;
+  ASSERT_TRUE(read_bootloader_message_from(&boot_verify, temp_misc.path, &err))
+      << "Failed to read BCB: " << err;
 
   ASSERT_EQ(std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)),
             std::string(reinterpret_cast<const char*>(&boot_verify), sizeof(boot_verify)));
 }
 
-TEST_F(BootloaderMessageTest, write_bootloader_message_options) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
-
+TEST(BootloaderMessageTest, update_bootloader_message_in_struct) {
   // Write the options to BCB.
   std::vector<std::string> options = { "option1", "option2" };
-  std::string err;
-  ASSERT_TRUE(write_bootloader_message(options, &err)) << "Failed to write BCB: " << err;
 
-  // Inject some bytes into boot, which should be overwritten while reading.
-  bootloader_message boot;
+  bootloader_message boot = {};
+  // Inject some bytes into boot.
   strlcpy(boot.recovery, "random message", sizeof(boot.recovery));
+  strlcpy(boot.status, "status bytes", sizeof(boot.status));
+  strlcpy(boot.stage, "stage bytes", sizeof(boot.stage));
   strlcpy(boot.reserved, "reserved bytes", sizeof(boot.reserved));
 
-  ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
 
   // Verify that command and recovery fields should be set.
   ASSERT_EQ("boot-recovery", std::string(boot.command));
   std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n";
   ASSERT_EQ(expected, std::string(boot.recovery));
 
-  // The rest should be cleared.
-  ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status)));
-  ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage)));
-  ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'),
-            std::string(boot.reserved, sizeof(boot.reserved)));
+  // The rest should be intact.
+  ASSERT_EQ("status bytes", std::string(boot.status));
+  ASSERT_EQ("stage bytes", std::string(boot.stage));
+  ASSERT_EQ("reserved bytes", std::string(boot.reserved));
 }
 
-TEST_F(BootloaderMessageTest, write_bootloader_message_options_empty) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
-
+TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_empty) {
   // Write empty vector.
   std::vector<std::string> options;
-  std::string err;
-  ASSERT_TRUE(write_bootloader_message(options, &err)) << "Failed to write BCB: " << err;
 
   // Read and verify.
-  bootloader_message boot;
-  ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+  bootloader_message boot = {};
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
 
   // command and recovery fields should be set.
   ASSERT_EQ("boot-recovery", std::string(boot.command));
   ASSERT_EQ("recovery\n", std::string(boot.recovery));
 
-  // The rest should be cleared.
+  // The rest should be empty.
   ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status)));
   ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage)));
   ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'),
             std::string(boot.reserved, sizeof(boot.reserved)));
 }
 
-TEST_F(BootloaderMessageTest, write_bootloader_message_options_long) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
-
+TEST(BootloaderMessageTest, update_bootloader_message_recovery_options_long) {
   // Write super long message.
   std::vector<std::string> options;
   for (int i = 0; i < 100; i++) {
     options.push_back("option: " + std::to_string(i));
   }
 
-  std::string err;
-  ASSERT_TRUE(write_bootloader_message(options, &err)) << "Failed to write BCB: " << err;
-
   // Read and verify.
-  bootloader_message boot;
-  ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+  bootloader_message boot = {};
+  ASSERT_TRUE(update_bootloader_message_in_struct(&boot, options));
 
   // Make sure it's long enough.
   std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n";
@@ -167,40 +107,10 @@
   ASSERT_EQ(expected.substr(0, sizeof(boot.recovery) - 1), std::string(boot.recovery));
   ASSERT_EQ('\0', boot.recovery[sizeof(boot.recovery) - 1]);
 
-  // The rest should be cleared.
+  // The rest should be empty.
   ASSERT_EQ(std::string(sizeof(boot.status), '\0'), std::string(boot.status, sizeof(boot.status)));
   ASSERT_EQ(std::string(sizeof(boot.stage), '\0'), std::string(boot.stage, sizeof(boot.stage)));
   ASSERT_EQ(std::string(sizeof(boot.reserved), '\0'),
             std::string(boot.reserved, sizeof(boot.reserved)));
 }
 
-TEST_F(BootloaderMessageTest, update_bootloader_message) {
-  if (!has_misc) {
-    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-    return;
-  }
-
-  // Inject some bytes into boot, which should be not overwritten later.
-  bootloader_message boot;
-  strlcpy(boot.recovery, "random message", sizeof(boot.recovery));
-  strlcpy(boot.reserved, "reserved bytes", sizeof(boot.reserved));
-  std::string err;
-  ASSERT_TRUE(write_bootloader_message(boot, &err)) << "Failed to write BCB: " << err;
-
-  // Update the BCB message.
-  std::vector<std::string> options = { "option1", "option2" };
-  ASSERT_TRUE(update_bootloader_message(options, &err)) << "Failed to update BCB: " << err;
-
-  bootloader_message boot_verify;
-  ASSERT_TRUE(read_bootloader_message(&boot_verify, &err)) << "Failed to read BCB: " << err;
-
-  // Verify that command and recovery fields should be set.
-  ASSERT_EQ("boot-recovery", std::string(boot_verify.command));
-  std::string expected = "recovery\n" + android::base::Join(options, "\n") + "\n";
-  ASSERT_EQ(expected, std::string(boot_verify.recovery));
-
-  // The rest should be intact.
-  ASSERT_EQ(std::string(boot.status), std::string(boot_verify.status));
-  ASSERT_EQ(std::string(boot.stage), std::string(boot_verify.stage));
-  ASSERT_EQ(std::string(boot.reserved), std::string(boot_verify.reserved));
-}
diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp
index bf25aeb..6c23def 100644
--- a/tests/component/imgdiff_test.cpp
+++ b/tests/component/imgdiff_test.cpp
@@ -16,17 +16,24 @@
 
 #include <stdio.h>
 
+#include <algorithm>
 #include <string>
+#include <tuple>
 #include <vector>
 
 #include <android-base/file.h>
 #include <android-base/memory.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
 #include <android-base/test_utils.h>
 #include <applypatch/imgdiff.h>
+#include <applypatch/imgdiff_image.h>
 #include <applypatch/imgpatch.h>
 #include <gtest/gtest.h>
 #include <ziparchive/zip_writer.h>
 
+#include "common/test_constants.h"
+
 using android::base::get_unaligned;
 
 // Sanity check for the given imgdiff patch header.
@@ -75,15 +82,20 @@
   if (num_deflate != nullptr) *num_deflate = deflate;
 }
 
+static void GenerateTarget(const std::string& src, const std::string& patch, std::string* patched) {
+  patched->clear();
+  ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(),
+                               reinterpret_cast<const unsigned char*>(patch.data()), patch.size(),
+                               [&](const unsigned char* data, size_t len) {
+                                 patched->append(reinterpret_cast<const char*>(data), len);
+                                 return len;
+                               }));
+}
+
 static void verify_patched_image(const std::string& src, const std::string& patch,
                                  const std::string& tgt) {
   std::string patched;
-  ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(),
-                               reinterpret_cast<const unsigned char*>(patch.data()), patch.size(),
-                               [&patched](const unsigned char* data, size_t len) {
-                                 patched.append(reinterpret_cast<const char*>(data), len);
-                                 return len;
-                               }));
+  GenerateTarget(src, patch, &patched);
   ASSERT_EQ(tgt, patched);
 }
 
@@ -138,7 +150,7 @@
 TEST(ImgdiffTest, zip_mode_smoke_store) {
   // Construct src and tgt zip files.
   TemporaryFile src_file;
-  FILE* src_file_ptr = fdopen(src_file.fd, "wb");
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
   ZipWriter src_writer(src_file_ptr);
   ASSERT_EQ(0, src_writer.StartEntry("file1.txt", 0));  // Store mode.
   const std::string src_content("abcdefg");
@@ -148,7 +160,7 @@
   ASSERT_EQ(0, fclose(src_file_ptr));
 
   TemporaryFile tgt_file;
-  FILE* tgt_file_ptr = fdopen(tgt_file.fd, "wb");
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
   ZipWriter tgt_writer(tgt_file_ptr);
   ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", 0));  // Store mode.
   const std::string tgt_content("abcdefgxyz");
@@ -187,7 +199,7 @@
 TEST(ImgdiffTest, zip_mode_smoke_compressed) {
   // Construct src and tgt zip files.
   TemporaryFile src_file;
-  FILE* src_file_ptr = fdopen(src_file.fd, "wb");
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
   ZipWriter src_writer(src_file_ptr);
   ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
   const std::string src_content("abcdefg");
@@ -197,7 +209,7 @@
   ASSERT_EQ(0, fclose(src_file_ptr));
 
   TemporaryFile tgt_file;
-  FILE* tgt_file_ptr = fdopen(tgt_file.fd, "wb");
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
   ZipWriter tgt_writer(tgt_file_ptr);
   ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
   const std::string tgt_content("abcdefgxyz");
@@ -236,7 +248,7 @@
 TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) {
   // Construct src and tgt zip files.
   TemporaryFile src_file;
-  FILE* src_file_ptr = fdopen(src_file.fd, "wb");
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
   ZipWriter src_writer(src_file_ptr);
   ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
   const std::string src_content("abcdefg");
@@ -246,7 +258,7 @@
   ASSERT_EQ(0, fclose(src_file_ptr));
 
   TemporaryFile tgt_file;
-  FILE* tgt_file_ptr = fdopen(tgt_file.fd, "wb");
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
   ZipWriter tgt_writer(tgt_file_ptr);
   ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
   const std::string tgt_content("abcdefgxyz");
@@ -623,3 +635,438 @@
                                 reinterpret_cast<const unsigned char*>(patch.data()), patch.size(),
                                 [](const unsigned char* /*data*/, size_t len) { return len; }));
 }
+
+static void construct_store_entry(const std::vector<std::tuple<std::string, size_t, char>>& info,
+                                  ZipWriter* writer) {
+  for (auto& t : info) {
+    // Create t(1) blocks of t(2), and write the data to t(0)
+    ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), 0));
+    const std::string content(std::get<1>(t) * 4096, std::get<2>(t));
+    ASSERT_EQ(0, writer->WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, writer->FinishEntry());
+  }
+}
+
+static void construct_deflate_entry(const std::vector<std::tuple<std::string, size_t, size_t>>& info,
+                                    ZipWriter* writer, const std::string& data) {
+  for (auto& t : info) {
+    // t(0): entry_name; t(1): block offset; t(2) length in blocks.
+    ASSERT_EQ(0, writer->StartEntry(std::get<0>(t).c_str(), ZipWriter::kCompress));
+    ASSERT_EQ(0, writer->WriteBytes(data.data() + std::get<1>(t) * 4096, std::get<2>(t) * 4096));
+    ASSERT_EQ(0, writer->FinishEntry());
+  }
+}
+
+// Look for the source and patch pieces in debug_dir. Generate a target piece from each pair.
+// Concatenate all the target pieces and match against the orignal one. Used pieces in debug_dir
+// will be cleaned up.
+static void GenerateAndCheckSplitTarget(const std::string& debug_dir, size_t count,
+                                        const std::string& tgt) {
+  std::string patched;
+  for (size_t i = 0; i < count; i++) {
+    std::string split_src_path = android::base::StringPrintf("%s/src-%zu", debug_dir.c_str(), i);
+    std::string split_src;
+    ASSERT_TRUE(android::base::ReadFileToString(split_src_path, &split_src));
+    ASSERT_EQ(0, unlink(split_src_path.c_str()));
+
+    std::string split_patch_path =
+        android::base::StringPrintf("%s/patch-%zu", debug_dir.c_str(), i);
+    std::string split_patch;
+    ASSERT_TRUE(android::base::ReadFileToString(split_patch_path, &split_patch));
+    ASSERT_EQ(0, unlink(split_patch_path.c_str()));
+
+    std::string split_tgt;
+    GenerateTarget(split_src, split_patch, &split_tgt);
+    patched += split_tgt;
+  }
+
+  // Verify we can get back the original target image.
+  ASSERT_EQ(tgt, patched);
+}
+
+std::vector<ImageChunk> ConstructImageChunks(
+    const std::vector<uint8_t>& content, const std::vector<std::tuple<std::string, size_t>>& info) {
+  std::vector<ImageChunk> chunks;
+  size_t start = 0;
+  for (const auto& t : info) {
+    size_t length = std::get<1>(t);
+    chunks.emplace_back(CHUNK_NORMAL, start, &content, length, std::get<0>(t));
+    start += length;
+  }
+
+  return chunks;
+}
+
+TEST(ImgdiffTest, zip_mode_split_image_smoke) {
+  std::vector<uint8_t> content;
+  content.reserve(4096 * 50);
+  uint8_t n = 0;
+  generate_n(back_inserter(content), 4096 * 50, [&n]() { return n++ / 4096; });
+
+  ZipModeImage tgt_image(false, 4096 * 10);
+  std::vector<ImageChunk> tgt_chunks = ConstructImageChunks(content, { { "a", 100 },
+                                                                       { "b", 4096 * 2 },
+                                                                       { "c", 4096 * 3 },
+                                                                       { "d", 300 },
+                                                                       { "e-0", 4096 * 10 },
+                                                                       { "e-1", 4096 * 5 },
+                                                                       { "CD", 200 } });
+  tgt_image.Initialize(std::move(tgt_chunks),
+                       std::vector<uint8_t>(content.begin(), content.begin() + 82520));
+
+  tgt_image.DumpChunks();
+
+  ZipModeImage src_image(true, 4096 * 10);
+  std::vector<ImageChunk> src_chunks = ConstructImageChunks(content, { { "b", 4096 * 3 },
+                                                                       { "c-0", 4096 * 10 },
+                                                                       { "c-1", 4096 * 2 },
+                                                                       { "a", 4096 * 5 },
+                                                                       { "e-0", 4096 * 10 },
+                                                                       { "e-1", 10000 },
+                                                                       { "CD", 5000 } });
+  src_image.Initialize(std::move(src_chunks),
+                       std::vector<uint8_t>(content.begin(), content.begin() + 137880));
+
+  std::vector<ZipModeImage> split_tgt_images;
+  std::vector<ZipModeImage> split_src_images;
+  std::vector<SortedRangeSet> split_src_ranges;
+
+  ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images,
+                                           &split_src_images, &split_src_ranges);
+
+  // src_piece 1: a 5 blocks, b 3 blocks
+  // src_piece 2: c-0 10 blocks
+  // src_piece 3: d 0 block, e-0 10 blocks
+  // src_piece 4: e-1 2 blocks; CD 2 blocks
+  ASSERT_EQ(split_tgt_images.size(), split_src_images.size());
+  ASSERT_EQ(static_cast<size_t>(4), split_tgt_images.size());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[0].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[0][0].DataLengthForPatch());
+  ASSERT_EQ("4,0,3,15,20", split_src_ranges[0].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[1].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(12288), split_tgt_images[1][0].DataLengthForPatch());
+  ASSERT_EQ("2,3,13", split_src_ranges[1].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[2].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(40960), split_tgt_images[2][0].DataLengthForPatch());
+  ASSERT_EQ("2,20,30", split_src_ranges[2].ToString());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_tgt_images[3].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(16984), split_tgt_images[3][0].DataLengthForPatch());
+  ASSERT_EQ("2,30,34", split_src_ranges[3].ToString());
+}
+
+TEST(ImgdiffTest, zip_mode_store_large_apk) {
+  // Construct src and tgt zip files with limit = 10 blocks.
+  //     src              tgt
+  //  12 blocks 'd'    3 blocks  'a'
+  //  8 blocks  'c'    3 blocks  'b'
+  //  3 blocks  'b'    8 blocks  'c' (exceeds limit)
+  //  3 blocks  'a'    12 blocks 'd' (exceeds limit)
+  //                   3 blocks  'e'
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+  construct_store_entry(
+      { { "a", 3, 'a' }, { "b", 3, 'b' }, { "c", 8, 'c' }, { "d", 12, 'd' }, { "e", 3, 'e' } },
+      &tgt_writer);
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "d", 12, 'd' }, { "c", 8, 'c' }, { "b", 3, 'b' }, { "a", 3, 'a' } },
+                        &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 4 pieces of patch. (Roughly 3'a',3'b'; 8'c'; 10'd'; 2'd'3'e')
+  GenerateAndCheckSplitTarget(debug_dir.path, 4, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_deflate_large_apk) {
+  // Src and tgt zip files are constructed as follows.
+  //     src               tgt
+  //  22 blocks, "d"    4  blocks,  "a"
+  //  5 blocks,  "b"    4  blocks,  "b"
+  //  3 blocks,  "a"    8  blocks,  "c" (exceeds limit)
+  //  1 block,   "g"    20 blocks,  "d" (exceeds limit)
+  //  8 blocks,  "c"    2  blocks,  "e"
+  //  1 block,   "f"    1  block ,  "f"
+  std::string tgt_path = from_testdata_base("deflate_tgt.zip");
+  std::string src_path = from_testdata_base("deflate_src.zip");
+
+  ZipModeImage src_image(true, 10 * 4096);
+  ZipModeImage tgt_image(false, 10 * 4096);
+  ASSERT_TRUE(src_image.Initialize(src_path));
+  ASSERT_TRUE(tgt_image.Initialize(tgt_path));
+  ASSERT_TRUE(ZipModeImage::CheckAndProcessChunks(&tgt_image, &src_image));
+
+  src_image.DumpChunks();
+  tgt_image.DumpChunks();
+
+  std::vector<ZipModeImage> split_tgt_images;
+  std::vector<ZipModeImage> split_src_images;
+  std::vector<SortedRangeSet> split_src_ranges;
+  ZipModeImage::SplitZipModeImageWithLimit(tgt_image, src_image, &split_tgt_images,
+                                           &split_src_images, &split_src_ranges);
+
+  // Expected split images with limit = 10 blocks.
+  // src_piece 0: a 3 blocks, b 5 blocks
+  // src_piece 1: c 8 blocks
+  // src_piece 2: d-0 10 block
+  // src_piece 3: d-1 10 blocks
+  // src_piece 4: e 1 block, CD
+  ASSERT_EQ(split_tgt_images.size(), split_src_images.size());
+  ASSERT_EQ(static_cast<size_t>(5), split_tgt_images.size());
+
+  ASSERT_EQ(static_cast<size_t>(2), split_src_images[0].NumOfChunks());
+  ASSERT_EQ("a", split_src_images[0][0].GetEntryName());
+  ASSERT_EQ("b", split_src_images[0][1].GetEntryName());
+
+  ASSERT_EQ(static_cast<size_t>(1), split_src_images[1].NumOfChunks());
+  ASSERT_EQ("c", split_src_images[1][0].GetEntryName());
+
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[2].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[3].NumOfChunks());
+  ASSERT_EQ(static_cast<size_t>(0), split_src_images[4].NumOfChunks());
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  ASSERT_TRUE(ZipModeImage::GeneratePatches(split_tgt_images, split_src_images, split_src_ranges,
+                                            patch_file.path, split_info_file.path, debug_dir.path));
+
+  // Verify the content of split info.
+  // Expect 5 pieces of patch. ["a","b"; "c"; "d-0"; "d-1"; "e"]
+  std::string split_info_string;
+  android::base::ReadFileToString(split_info_file.path, &split_info_string);
+  std::vector<std::string> info_list =
+      android::base::Split(android::base::Trim(split_info_string), "\n");
+
+  ASSERT_EQ(static_cast<size_t>(7), info_list.size());
+  ASSERT_EQ("2", android::base::Trim(info_list[0]));
+  ASSERT_EQ("5", android::base::Trim(info_list[1]));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_path, &tgt));
+  ASSERT_EQ(static_cast<size_t>(160385), tgt.size());
+  std::vector<std::string> tgt_file_ranges = {
+    "36864 2,22,31", "32768 2,31,40", "40960 2,0,11", "40960 2,11,21", "8833 4,21,22,40,41",
+  };
+
+  for (size_t i = 0; i < 5; i++) {
+    struct stat st;
+    std::string path = android::base::StringPrintf("%s/patch-%zu", debug_dir.path, i);
+    ASSERT_EQ(0, stat(path.c_str(), &st));
+    ASSERT_EQ(std::to_string(st.st_size) + " " + tgt_file_ranges[i],
+              android::base::Trim(info_list[i + 2]));
+  }
+
+  GenerateAndCheckSplitTarget(debug_dir.path, 5, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_no_match_source) {
+  // Generate 20 blocks of random data.
+  std::string random_data;
+  random_data.reserve(4096 * 20);
+  generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; });
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_deflate_entry({ { "a", 0, 4 }, { "b", 5, 5 }, { "c", 11, 5 } }, &tgt_writer,
+                          random_data);
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // We don't have a matching source entry.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "d", 1, 'd' } }, &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=10", debug_dir_arg.c_str(), split_info_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 1 pieces of patch due to no matching source entry.
+  GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_enough_limit) {
+  // Generate 20 blocks of random data.
+  std::string random_data;
+  random_data.reserve(4096 * 20);
+  generate_n(back_inserter(random_data), 4096 * 20, []() { return rand() % 256; });
+
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_deflate_entry({ { "a", 0, 10 }, { "b", 10, 5 } }, &tgt_writer, random_data);
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  // Construct 10 blocks of source.
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_deflate_entry({ { "a", 1, 10 } }, &src_writer, random_data);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch with a limit of 20 blocks.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff", "-z", "--block-limit=20", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect 1 piece of patch since limit is larger than the zip file size.
+  GenerateAndCheckSplitTarget(debug_dir.path, 1, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_apk_small_target_chunk) {
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  // The first entry is less than 4096 bytes, followed immediately by an entry that has a very
+  // large counterpart in the source file. Therefore the first entry will be patched separately.
+  std::string small_chunk("a", 2000);
+  ASSERT_EQ(0, tgt_writer.StartEntry("a", 0));
+  ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  construct_store_entry(
+      {
+          { "b", 12, 'b' }, { "c", 3, 'c' },
+      },
+      &tgt_writer);
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry({ { "a", 1, 'a' }, { "b", 13, 'b' }, { "c", 1, 'c' } }, &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff",     "-z",          "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect three split src images:
+  // src_piece 0: a 1 blocks
+  // src_piece 1: b-0 10 blocks
+  // src_piece 2: b-1 3 blocks, c 1 blocks, CD
+  GenerateAndCheckSplitTarget(debug_dir.path, 3, tgt);
+}
+
+TEST(ImgdiffTest, zip_mode_large_apk_skipped_small_target_chunk) {
+  TemporaryFile tgt_file;
+  FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+  ZipWriter tgt_writer(tgt_file_ptr);
+
+  construct_store_entry(
+      {
+          { "a", 11, 'a' },
+      },
+      &tgt_writer);
+
+  // Construct a tiny target entry of 1 byte, which will be skipped due to the tail alignment of
+  // the previous entry.
+  std::string small_chunk("b", 1);
+  ASSERT_EQ(0, tgt_writer.StartEntry("b", 0));
+  ASSERT_EQ(0, tgt_writer.WriteBytes(small_chunk.data(), small_chunk.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+
+  ASSERT_EQ(0, tgt_writer.Finish());
+  ASSERT_EQ(0, fclose(tgt_file_ptr));
+
+  TemporaryFile src_file;
+  FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+  ZipWriter src_writer(src_file_ptr);
+  construct_store_entry(
+      {
+          { "a", 11, 'a' }, { "b", 11, 'b' },
+      },
+      &src_writer);
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  TemporaryFile split_info_file;
+  TemporaryDir debug_dir;
+  std::string split_info_arg = android::base::StringPrintf("--split-info=%s", split_info_file.path);
+  std::string debug_dir_arg = android::base::StringPrintf("--debug-dir=%s", debug_dir.path);
+  std::vector<const char*> args = {
+    "imgdiff",     "-z",          "--block-limit=10", split_info_arg.c_str(), debug_dir_arg.c_str(),
+    src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+
+  // Expect two split src images:
+  // src_piece 0: a-0 10 blocks
+  // src_piece 1: a-0 1 block, CD
+  GenerateAndCheckSplitTarget(debug_dir.path, 2, tgt);
+}
diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp
index 968196f..d19d788 100644
--- a/tests/component/install_test.cpp
+++ b/tests/component/install_test.cpp
@@ -19,6 +19,7 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <string>
 #include <vector>
 
@@ -36,7 +37,7 @@
 
 TEST(InstallTest, verify_package_compatibility_no_entry) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   // The archive must have something to be opened correctly.
   ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0));
@@ -53,7 +54,7 @@
 
 TEST(InstallTest, verify_package_compatibility_invalid_entry) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("compatibility.zip", 0));
   ASSERT_EQ(0, writer.FinishEntry());
@@ -69,7 +70,7 @@
 
 TEST(InstallTest, read_metadata_from_package_smoke) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored));
   const std::string content("abcdefg");
@@ -86,7 +87,7 @@
   CloseArchive(zip);
 
   TemporaryFile temp_file2;
-  FILE* zip_file2 = fdopen(temp_file2.fd, "w");
+  FILE* zip_file2 = fdopen(temp_file2.release(), "w");
   ZipWriter writer2(zip_file2);
   ASSERT_EQ(0, writer2.StartEntry("META-INF/com/android/metadata", kCompressDeflated));
   ASSERT_EQ(0, writer2.WriteBytes(content.data(), content.size()));
@@ -103,7 +104,7 @@
 
 TEST(InstallTest, read_metadata_from_package_no_entry) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("dummy_entry", kCompressStored));
   ASSERT_EQ(0, writer.FinishEntry());
@@ -119,7 +120,7 @@
 
 TEST(InstallTest, verify_package_compatibility_with_libvintf_malformed_xml) {
   TemporaryFile compatibility_zip_file;
-  FILE* compatibility_zip = fdopen(compatibility_zip_file.fd, "w");
+  FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w");
   ZipWriter compatibility_zip_writer(compatibility_zip);
   ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated));
   std::string malformed_xml = "malformed";
@@ -129,7 +130,7 @@
   ASSERT_EQ(0, fclose(compatibility_zip));
 
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored));
   std::string compatibility_zip_content;
@@ -164,7 +165,7 @@
   ASSERT_TRUE(
       android::base::ReadFileToString(system_manifest_xml_path, &system_manifest_xml_content));
   TemporaryFile compatibility_zip_file;
-  FILE* compatibility_zip = fdopen(compatibility_zip_file.fd, "w");
+  FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w");
   ZipWriter compatibility_zip_writer(compatibility_zip);
   ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated));
   ASSERT_EQ(0, compatibility_zip_writer.WriteBytes(system_manifest_xml_content.data(),
@@ -174,7 +175,7 @@
   ASSERT_EQ(0, fclose(compatibility_zip));
 
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored));
   std::string compatibility_zip_content;
@@ -198,10 +199,10 @@
   CloseArchive(zip);
 }
 
-TEST(InstallTest, update_binary_command_smoke) {
 #ifdef AB_OTA_UPDATER
+static void VerifyAbUpdateBinaryCommand(const std::string& serialno, bool success = true) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored));
   ASSERT_EQ(0, writer.FinishEntry());
@@ -215,11 +216,13 @@
   ASSERT_NE("", device);
   std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
   ASSERT_NE("", timestamp);
-  std::string metadata = android::base::Join(
-      std::vector<std::string>{
-          "ota-type=AB", "pre-device=" + device, "post-timestamp=" + timestamp,
-      },
-      "\n");
+
+  std::vector<std::string> meta{ "ota-type=AB", "pre-device=" + device,
+                                 "post-timestamp=" + timestamp };
+  if (!serialno.empty()) {
+    meta.push_back("serialno=" + serialno);
+  }
+  std::string metadata = android::base::Join(meta, "\n");
   ASSERT_EQ(0, writer.WriteBytes(metadata.data(), metadata.size()));
   ASSERT_EQ(0, writer.FinishEntry());
   ASSERT_EQ(0, writer.Finish());
@@ -234,17 +237,28 @@
   std::string package = "/path/to/update.zip";
   std::string binary_path = "/sbin/update_engine_sideload";
   std::vector<std::string> cmd;
-  ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
-  ASSERT_EQ(5U, cmd.size());
-  ASSERT_EQ(binary_path, cmd[0]);
-  ASSERT_EQ("--payload=file://" + package, cmd[1]);
-  ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]);
-  ASSERT_EQ("--headers=" + properties, cmd[3]);
-  ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
+  if (success) {
+    ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+    ASSERT_EQ(5U, cmd.size());
+    ASSERT_EQ(binary_path, cmd[0]);
+    ASSERT_EQ("--payload=file://" + package, cmd[1]);
+    ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]);
+    ASSERT_EQ("--headers=" + properties, cmd[3]);
+    ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
+  } else {
+    ASSERT_EQ(INSTALL_ERROR, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+  }
   CloseArchive(zip);
+}
+#endif  // AB_OTA_UPDATER
+
+TEST(InstallTest, update_binary_command_smoke) {
+#ifdef AB_OTA_UPDATER
+  // Empty serialno will pass the verification.
+  VerifyAbUpdateBinaryCommand({});
 #else
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
   ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored));
@@ -289,7 +303,7 @@
 TEST(InstallTest, update_binary_command_invalid) {
 #ifdef AB_OTA_UPDATER
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   // Missing payload_properties.txt.
   ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored));
@@ -320,7 +334,7 @@
   CloseArchive(zip);
 #else
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.fd, "w");
+  FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
   // The archive must have something to be opened correctly.
   ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0));
@@ -340,3 +354,34 @@
   CloseArchive(zip);
 #endif  // AB_OTA_UPDATER
 }
+
+#ifdef AB_OTA_UPDATER
+TEST(InstallTest, update_binary_command_multiple_serialno) {
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+
+  // Single matching serialno will pass the verification.
+  VerifyAbUpdateBinaryCommand(serialno);
+
+  static constexpr char alphabet[] =
+      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+  auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; };
+
+  // Generate 900 random serial numbers.
+  std::string random_serial;
+  for (size_t i = 0; i < 900; i++) {
+    generate_n(back_inserter(random_serial), serialno.size(), generator);
+    random_serial.append("|");
+  }
+  // Random serialnos should fail the verification.
+  VerifyAbUpdateBinaryCommand(random_serial, false);
+
+  std::string long_serial = random_serial + serialno + "|";
+  for (size_t i = 0; i < 99; i++) {
+    generate_n(back_inserter(long_serial), serialno.size(), generator);
+    long_serial.append("|");
+  }
+  // String with the matching serialno should pass the verification.
+  VerifyAbUpdateBinaryCommand(long_serial);
+}
+#endif  // AB_OTA_UPDATER
diff --git a/tests/component/sideload_test.cpp b/tests/component/sideload_test.cpp
index 40cfc69..b7109fc 100644
--- a/tests/component/sideload_test.cpp
+++ b/tests/component/sideload_test.cpp
@@ -16,6 +16,12 @@
 
 #include <unistd.h>
 
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 
 #include "fuse_sideload.h"
@@ -26,11 +32,67 @@
 
 TEST(SideloadTest, run_fuse_sideload_wrong_parameters) {
   provider_vtab vtab;
-  vtab.close = [](void*) {};
+  vtab.close = [](void) {};
 
-  ASSERT_EQ(-1, run_fuse_sideload(&vtab, nullptr, 4096, 4095));
-  ASSERT_EQ(-1, run_fuse_sideload(&vtab, nullptr, 4096, (1 << 22) + 1));
+  ASSERT_EQ(-1, run_fuse_sideload(vtab, 4096, 4095));
+  ASSERT_EQ(-1, run_fuse_sideload(vtab, 4096, (1 << 22) + 1));
 
   // Too many blocks.
-  ASSERT_EQ(-1, run_fuse_sideload(&vtab, nullptr, ((1 << 18) + 1) * 4096, 4096));
+  ASSERT_EQ(-1, run_fuse_sideload(vtab, ((1 << 18) + 1) * 4096, 4096));
+}
+
+TEST(SideloadTest, run_fuse_sideload) {
+  const std::vector<std::string> blocks = {
+    std::string(2048, 'a') + std::string(2048, 'b'),
+    std::string(2048, 'c') + std::string(2048, 'd'),
+    std::string(2048, 'e') + std::string(2048, 'f'),
+    std::string(2048, 'g') + std::string(2048, 'h'),
+  };
+  const std::string content = android::base::Join(blocks, "");
+  ASSERT_EQ(16384U, content.size());
+
+  provider_vtab vtab;
+  vtab.close = [](void) {};
+  vtab.read_block = [&blocks](uint32_t block, uint8_t* buffer, uint32_t fetch_size) {
+    if (block >= 4) return -1;
+    blocks[block].copy(reinterpret_cast<char*>(buffer), fetch_size);
+    return 0;
+  };
+
+  TemporaryDir mount_point;
+  pid_t pid = fork();
+  if (pid == 0) {
+    ASSERT_EQ(0, run_fuse_sideload(vtab, 16384, 4096, mount_point.path));
+    _exit(EXIT_SUCCESS);
+  }
+
+  std::string package = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_FILENAME;
+  int status;
+  static constexpr int kSideloadInstallTimeout = 10;
+  for (int i = 0; i < kSideloadInstallTimeout; ++i) {
+    ASSERT_NE(-1, waitpid(pid, &status, WNOHANG));
+
+    struct stat sb;
+    if (stat(package.c_str(), &sb) == 0) {
+      break;
+    }
+
+    if (errno == ENOENT && i < kSideloadInstallTimeout - 1) {
+      sleep(1);
+      continue;
+    }
+    FAIL() << "Timed out waiting for the fuse-provided package.";
+  }
+
+  std::string content_via_fuse;
+  ASSERT_TRUE(android::base::ReadFileToString(package, &content_via_fuse));
+  ASSERT_EQ(content, content_via_fuse);
+
+  std::string exit_flag = std::string(mount_point.path) + "/" + FUSE_SIDELOAD_HOST_EXIT_FLAG;
+  struct stat sb;
+  ASSERT_EQ(0, stat(exit_flag.c_str(), &sb));
+
+  waitpid(pid, &status, 0);
+  ASSERT_EQ(0, WEXITSTATUS(status));
+  ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
 }
diff --git a/tests/component/uncrypt_test.cpp b/tests/component/uncrypt_test.cpp
index 3925236..55baca2 100644
--- a/tests/component/uncrypt_test.cpp
+++ b/tests/component/uncrypt_test.cpp
@@ -20,6 +20,7 @@
 #include <sys/un.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <string>
 
 #include <android-base/file.h>
@@ -38,43 +39,49 @@
 static const std::string INIT_SVC_UNCRYPT = "init.svc.uncrypt";
 static constexpr int SOCKET_CONNECTION_MAX_RETRY = 30;
 
+static void StopService() {
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "setup-bcb"));
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "clear-bcb"));
+  ASSERT_TRUE(android::base::SetProperty("ctl.stop", "uncrypt"));
+
+  bool success = false;
+  for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
+    std::string setup_bcb = android::base::GetProperty(INIT_SVC_SETUP_BCB, "");
+    std::string clear_bcb = android::base::GetProperty(INIT_SVC_CLEAR_BCB, "");
+    std::string uncrypt = android::base::GetProperty(INIT_SVC_UNCRYPT, "");
+    GTEST_LOG_(INFO) << "setup-bcb: [" << setup_bcb << "] clear-bcb: [" << clear_bcb
+                     << "] uncrypt: [" << uncrypt << "]";
+    if (setup_bcb != "running" && clear_bcb != "running" && uncrypt != "running") {
+      success = true;
+      break;
+    }
+    sleep(1);
+  }
+
+  ASSERT_TRUE(success) << "uncrypt service is not available.";
+}
+
 class UncryptTest : public ::testing::Test {
  protected:
   UncryptTest() : has_misc(true) {}
 
-  virtual void SetUp() override {
-    ASSERT_TRUE(android::base::SetProperty("ctl.stop", "setup-bcb"));
-    ASSERT_TRUE(android::base::SetProperty("ctl.stop", "clear-bcb"));
-    ASSERT_TRUE(android::base::SetProperty("ctl.stop", "uncrypt"));
-
-    bool success = false;
-    for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
-      std::string setup_bcb = android::base::GetProperty(INIT_SVC_SETUP_BCB, "");
-      std::string clear_bcb = android::base::GetProperty(INIT_SVC_CLEAR_BCB, "");
-      std::string uncrypt = android::base::GetProperty(INIT_SVC_UNCRYPT, "");
-      LOG(INFO) << "setup-bcb: [" << setup_bcb << "] clear-bcb: [" << clear_bcb << "] uncrypt: ["
-                << uncrypt << "]";
-      if (setup_bcb != "running" && clear_bcb != "running" && uncrypt != "running") {
-        success = true;
-        break;
-      }
-      sleep(1);
-    }
-
-    ASSERT_TRUE(success) << "uncrypt service is not available.";
-
+  void SetUp() override {
     std::string err;
     has_misc = !get_bootloader_message_blk_device(&err).empty();
   }
 
+  void TearDown() override {
+    // Clear the BCB.
+    if (has_misc) {
+      std::string err;
+      ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
+    }
+  }
+
   void SetupOrClearBcb(bool isSetup, const std::string& message,
                        const std::string& message_in_bcb) const {
-    if (!has_misc) {
-      GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
-      return;
-    }
-
-    // Trigger the setup-bcb service.
+    // Restart the setup-bcb service.
+    StopService();
     ASSERT_TRUE(android::base::SetProperty("ctl.start", isSetup ? "setup-bcb" : "clear-bcb"));
 
     // Test tends to be flaky if proceeding immediately ("Transport endpoint is not connected").
@@ -144,27 +151,49 @@
     }
   }
 
+  void VerifyBootloaderMessage(const std::string& expected) {
+    std::string err;
+    bootloader_message boot;
+    ASSERT_TRUE(read_bootloader_message(&boot, &err)) << "Failed to read BCB: " << err;
+
+    // Check that we have all the expected bytes.
+    ASSERT_EQ(expected, std::string(reinterpret_cast<const char*>(&boot), sizeof(boot)));
+  }
+
   bool has_misc;
 };
 
 TEST_F(UncryptTest, setup_bcb) {
+  if (!has_misc) {
+    GTEST_LOG_(INFO) << "Test skipped due to no /misc partition found on the device.";
+    return;
+  }
+
+  std::string random_data;
+  random_data.reserve(sizeof(bootloader_message));
+  generate_n(back_inserter(random_data), sizeof(bootloader_message), []() { return rand() % 128; });
+
+  bootloader_message boot;
+  memcpy(&boot, random_data.c_str(), random_data.size());
+
+  std::string err;
+  ASSERT_TRUE(write_bootloader_message(boot, &err)) << "Failed to write BCB: " << err;
+  VerifyBootloaderMessage(random_data);
+
+  ASSERT_TRUE(clear_bootloader_message(&err)) << "Failed to clear BCB: " << err;
+  VerifyBootloaderMessage(std::string(sizeof(bootloader_message), '\0'));
+
   std::string message = "--update_message=abc value";
   std::string message_in_bcb = "recovery\n--update_message=abc value\n";
   SetupOrClearBcb(true, message, message_in_bcb);
-}
 
-TEST_F(UncryptTest, clear_bcb) {
   SetupOrClearBcb(false, "", "");
-}
 
-TEST_F(UncryptTest, setup_bcb_wipe_ab) {
   TemporaryFile wipe_package;
   ASSERT_TRUE(android::base::WriteStringToFile(std::string(345, 'a'), wipe_package.path));
 
   // It's expected to store a wipe package in /misc, with the package size passed to recovery.
-  std::string message =
-      "--wipe_ab\n--wipe_package="s + wipe_package.path + "\n--reason=wipePackage"s;
-  std::string message_in_bcb =
-      "recovery\n--wipe_ab\n--wipe_package_size=345\n--reason=wipePackage\n";
+  message = "--wipe_ab\n--wipe_package="s + wipe_package.path + "\n--reason=wipePackage"s;
+  message_in_bcb = "recovery\n--wipe_ab\n--wipe_package_size=345\n--reason=wipePackage\n";
   SetupOrClearBcb(true, message, message_in_bcb);
 }
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index b04e118..1544bb2 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -46,7 +46,6 @@
     return;
   }
 
-  // The care map file can have only two or four lines.
   TemporaryFile temp_file;
   std::string content = "system\n2,0,1";
   ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
@@ -58,7 +57,7 @@
 }
 
 TEST_F(UpdateVerifierTest, verify_image_wrong_lines) {
-  // The care map file can have only two or four lines.
+  // The care map file can have only 2 / 4 / 6 lines.
   TemporaryFile temp_file;
   ASSERT_FALSE(verify_image(temp_file.path));
 
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index 6c341c1..5bfd7cb 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -23,6 +23,7 @@
 #include <algorithm>
 #include <memory>
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 #include <android-base/file.h>
@@ -32,16 +33,17 @@
 #include <android-base/test_utils.h>
 #include <bootloader_message/bootloader_message.h>
 #include <brotli/encode.h>
-#include <bsdiff.h>
+#include <bsdiff/bsdiff.h>
 #include <gtest/gtest.h>
 #include <ziparchive/zip_archive.h>
 #include <ziparchive/zip_writer.h>
 
 #include "common/test_constants.h"
 #include "edify/expr.h"
-#include "error_code.h"
 #include "otautil/SysUtil.h"
-#include "print_sha1.h"
+#include "otautil/cache_location.h"
+#include "otautil/error_code.h"
+#include "otautil/print_sha1.h"
 #include "updater/blockimg.h"
 #include "updater/install.h"
 #include "updater/updater.h"
@@ -74,6 +76,23 @@
   ASSERT_EQ(cause_code, state.cause_code);
 }
 
+static void BuildUpdatePackage(const std::unordered_map<std::string, std::string>& entries,
+                               int fd) {
+  FILE* zip_file_ptr = fdopen(fd, "wb");
+  ZipWriter zip_writer(zip_file_ptr);
+
+  for (const auto& entry : entries) {
+    ASSERT_EQ(0, zip_writer.StartEntry(entry.first.c_str(), 0));
+    if (!entry.second.empty()) {
+      ASSERT_EQ(0, zip_writer.WriteBytes(entry.second.data(), entry.second.size()));
+    }
+    ASSERT_EQ(0, zip_writer.FinishEntry());
+  }
+
+  ASSERT_EQ(0, zip_writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file_ptr));
+}
+
 static std::string get_sha1(const std::string& content) {
   uint8_t digest[SHA_DIGEST_LENGTH];
   SHA1(reinterpret_cast<const uint8_t*>(content.c_str()), content.size(), digest);
@@ -86,7 +105,16 @@
     RegisterBuiltins();
     RegisterInstallFunctions();
     RegisterBlockImageFunctions();
+
+    // Mock the location of last_command_file.
+    CacheLocation::location().set_cache_temp_source(temp_saved_source_.path);
+    CacheLocation::location().set_last_command_file(temp_last_command_.path);
+    CacheLocation::location().set_stash_directory_base(temp_stash_base_.path);
   }
+
+  TemporaryFile temp_saved_source_;
+  TemporaryFile temp_last_command_;
+  TemporaryDir temp_stash_base_;
 };
 
 TEST_F(UpdaterTest, getprop) {
@@ -383,7 +411,7 @@
 
   TemporaryFile tf;
   UpdaterInfo updater_info;
-  updater_info.cmd_pipe = fdopen(tf.fd, "w");
+  updater_info.cmd_pipe = fdopen(tf.release(), "w");
   expect(".52", "set_progress(\".52\")", kNoCause, &updater_info);
   fflush(updater_info.cmd_pipe);
 
@@ -392,6 +420,7 @@
   ASSERT_EQ(android::base::StringPrintf("set_progress %f\n", .52), cmd);
   // recovery-updater protocol expects 2 tokens ("set_progress <frac>").
   ASSERT_EQ(2U, android::base::Split(cmd, " ").size());
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
 }
 
 TEST_F(UpdaterTest, show_progress) {
@@ -407,7 +436,7 @@
 
   TemporaryFile tf;
   UpdaterInfo updater_info;
-  updater_info.cmd_pipe = fdopen(tf.fd, "w");
+  updater_info.cmd_pipe = fdopen(tf.release(), "w");
   expect(".52", "show_progress(\".52\", \"10\")", kNoCause, &updater_info);
   fflush(updater_info.cmd_pipe);
 
@@ -416,32 +445,22 @@
   ASSERT_EQ(android::base::StringPrintf("progress %f %d\n", .52, 10), cmd);
   // recovery-updater protocol expects 3 tokens ("progress <frac> <secs>").
   ASSERT_EQ(3U, android::base::Split(cmd, " ").size());
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
 }
 
-TEST_F(UpdaterTest, block_image_update) {
-  // Create a zip file with new_data and patch_data.
-  TemporaryFile zip_file;
-  FILE* zip_file_ptr = fdopen(zip_file.fd, "wb");
-  ZipWriter zip_writer(zip_file_ptr);
-
-  // Add a dummy new data.
-  ASSERT_EQ(0, zip_writer.StartEntry("new_data", 0));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-
-  // Generate and add the patch data.
+TEST_F(UpdaterTest, block_image_update_patch_data) {
   std::string src_content = std::string(4096, 'a') + std::string(4096, 'c');
   std::string tgt_content = std::string(4096, 'b') + std::string(4096, 'd');
+
+  // Generate the patch data.
   TemporaryFile patch_file;
   ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()),
       src_content.size(), reinterpret_cast<const uint8_t*>(tgt_content.data()),
       tgt_content.size(), patch_file.path, nullptr));
   std::string patch_content;
   ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content));
-  ASSERT_EQ(0, zip_writer.StartEntry("patch_data", 0));
-  ASSERT_EQ(0, zip_writer.WriteBytes(patch_content.data(), patch_content.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
 
-  // Add two transfer lists. The first one contains a bsdiff; and we expect the update to succeed.
+  // Create the transfer list that contains a bsdiff.
   std::string src_hash = get_sha1(src_content);
   std::string tgt_hash = get_sha1(tgt_content);
   std::vector<std::string> transfer_list = {
@@ -454,27 +473,16 @@
                                 src_hash.c_str(), tgt_hash.c_str(), src_hash.c_str()),
     "free " + src_hash,
   };
-  ASSERT_EQ(0, zip_writer.StartEntry("transfer_list", 0));
-  std::string commands = android::base::Join(transfer_list, '\n');
-  ASSERT_EQ(0, zip_writer.WriteBytes(commands.data(), commands.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
 
-  // Stash and free some blocks, then fail the 2nd update intentionally.
-  std::vector<std::string> fail_transfer_list = {
-    "4",
-    "2",
-    "0",
-    "2",
-    "stash " + tgt_hash + " 2,0,2",
-    "free " + tgt_hash,
-    "fail",
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", "" },
+    { "patch_data", patch_content },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
   };
-  ASSERT_EQ(0, zip_writer.StartEntry("fail_transfer_list", 0));
-  std::string fail_commands = android::base::Join(fail_transfer_list, '\n');
-  ASSERT_EQ(0, zip_writer.WriteBytes(fail_commands.data(), fail_commands.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  ASSERT_EQ(0, zip_writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file_ptr));
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
 
   MemMapping map;
   ASSERT_TRUE(map.MapFile(zip_file.path));
@@ -485,11 +493,11 @@
   UpdaterInfo updater_info;
   updater_info.package_zip = handle;
   TemporaryFile temp_pipe;
-  updater_info.cmd_pipe = fopen(temp_pipe.path, "wbe");
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
   updater_info.package_zip_addr = map.addr;
   updater_info.package_zip_len = map.length;
 
-  // Execute the commands in the 1st transfer list.
+  // Execute the commands in the transfer list.
   TemporaryFile update_file;
   ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
   std::string script = "block_image_update(\"" + std::string(update_file.path) +
@@ -500,57 +508,28 @@
   ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
   ASSERT_EQ(tgt_hash, get_sha1(updated_content));
 
-  // Expect the 2nd update to fail, but expect the stashed blocks to be freed.
-  script = "block_image_update(\"" + std::string(update_file.path) +
-      R"(", package_extract_file("fail_transfer_list"), "new_data", "patch_data"))";
-  expect("", script.c_str(), kNoCause, &updater_info);
-  // Updater generates the stash name based on the input file name.
-  std::string name_digest = get_sha1(update_file.path);
-  std::string stash_base = "/cache/recovery/" + name_digest;
-  ASSERT_EQ(0, access(stash_base.c_str(), F_OK));
-  ASSERT_EQ(-1, access((stash_base + tgt_hash).c_str(), F_OK));
-  ASSERT_EQ(0, rmdir(stash_base.c_str()));
-
   ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
   CloseArchive(handle);
 }
 
-TEST_F(UpdaterTest, new_data_short_write) {
-  // Create a zip file with new_data.
-  TemporaryFile zip_file;
-  FILE* zip_file_ptr = fdopen(zip_file.fd, "wb");
-  ZipWriter zip_writer(zip_file_ptr);
-
-  // Add the empty new data.
-  ASSERT_EQ(0, zip_writer.StartEntry("empty_new_data", 0));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  // Add the short written new data.
-  ASSERT_EQ(0, zip_writer.StartEntry("short_new_data", 0));
-  std::string new_data_short = std::string(10, 'a');
-  ASSERT_EQ(0, zip_writer.WriteBytes(new_data_short.data(), new_data_short.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  // Add the data of exactly one block.
-  ASSERT_EQ(0, zip_writer.StartEntry("exact_new_data", 0));
-  std::string new_data_exact = std::string(4096, 'a');
-  ASSERT_EQ(0, zip_writer.WriteBytes(new_data_exact.data(), new_data_exact.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  // Add a dummy patch data.
-  ASSERT_EQ(0, zip_writer.StartEntry("patch_data", 0));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-
+TEST_F(UpdaterTest, block_image_update_fail) {
+  std::string src_content(4096 * 2, 'e');
+  std::string src_hash = get_sha1(src_content);
+  // Stash and free some blocks, then fail the update intentionally.
   std::vector<std::string> transfer_list = {
-    "4",
-    "1",
-    "0",
-    "0",
-    "new 2,0,1",
+    "4", "2", "0", "2", "stash " + src_hash + " 2,0,2", "free " + src_hash, "fail",
   };
-  ASSERT_EQ(0, zip_writer.StartEntry("transfer_list", 0));
-  std::string commands = android::base::Join(transfer_list, '\n');
-  ASSERT_EQ(0, zip_writer.WriteBytes(commands.data(), commands.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  ASSERT_EQ(0, zip_writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file_ptr));
+
+  // Add a new data of 10 bytes to test the deadlock.
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", std::string(10, 0) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
 
   MemMapping map;
   ASSERT_TRUE(map.MapFile(zip_file.path));
@@ -561,7 +540,95 @@
   UpdaterInfo updater_info;
   updater_info.package_zip = handle;
   TemporaryFile temp_pipe;
-  updater_info.cmd_pipe = fopen(temp_pipe.path, "wbe");
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  TemporaryFile update_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  // Expect the stashed blocks to be freed.
+  std::string script = "block_image_update(\"" + std::string(update_file.path) +
+                       R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
+  expect("", script.c_str(), kNoCause, &updater_info);
+  // Updater generates the stash name based on the input file name.
+  std::string name_digest = get_sha1(update_file.path);
+  std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest;
+  ASSERT_EQ(0, access(stash_base.c_str(), F_OK));
+  ASSERT_EQ(-1, access((stash_base + src_hash).c_str(), F_OK));
+  ASSERT_EQ(0, rmdir(stash_base.c_str()));
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, new_data_over_write) {
+  std::vector<std::string> transfer_list = {
+    "4", "1", "0", "0", "new 2,0,1",
+  };
+
+  // Write 4096 + 100 bytes of new data.
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", std::string(4196, 0) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  TemporaryFile update_file;
+  std::string script = "block_image_update(\"" + std::string(update_file.path) +
+                       R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
+  expect("t", script.c_str(), kNoCause, &updater_info);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, new_data_short_write) {
+  std::vector<std::string> transfer_list = {
+    "4",
+    "1",
+    "0",
+    "0",
+    "new 2,0,1",
+  };
+
+  std::unordered_map<std::string, std::string> entries = {
+    { "empty_new_data", "" },
+    { "short_new_data", std::string(10, 'a') },
+    { "exact_new_data", std::string(4096, 'a') },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
   updater_info.package_zip_addr = map.addr;
   updater_info.package_zip_len = map.length;
 
@@ -579,18 +646,12 @@
   std::string script_exact_data = "block_image_update(\"" + std::string(update_file.path) +
       R"(", package_extract_file("transfer_list"), "exact_new_data", "patch_data"))";
   expect("t", script_exact_data.c_str(), kNoCause, &updater_info);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
   CloseArchive(handle);
 }
 
 TEST_F(UpdaterTest, brotli_new_data) {
-  // Create a zip file with new_data.
-  TemporaryFile zip_file;
-  FILE* zip_file_ptr = fdopen(zip_file.fd, "wb");
-  ZipWriter zip_writer(zip_file_ptr);
-
-  // Add a brotli compressed new data entry.
-  ASSERT_EQ(0, zip_writer.StartEntry("new.dat.br", 0));
-
   auto generator = []() { return rand() % 128; };
   // Generate 100 blocks of random data.
   std::string brotli_new_data;
@@ -598,16 +659,12 @@
   generate_n(back_inserter(brotli_new_data), 4096 * 100, generator);
 
   size_t encoded_size = BrotliEncoderMaxCompressedSize(brotli_new_data.size());
-  std::vector<uint8_t> encoded_data(encoded_size);
+  std::string encoded_data(encoded_size, 0);
   ASSERT_TRUE(BrotliEncoderCompress(
       BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, brotli_new_data.size(),
-      reinterpret_cast<const uint8_t*>(brotli_new_data.data()), &encoded_size, encoded_data.data()));
-
-  ASSERT_EQ(0, zip_writer.WriteBytes(encoded_data.data(), encoded_size));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  // Add a dummy patch data.
-  ASSERT_EQ(0, zip_writer.StartEntry("patch_data", 0));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
+      reinterpret_cast<const uint8_t*>(brotli_new_data.data()), &encoded_size,
+      reinterpret_cast<uint8_t*>(const_cast<char*>(encoded_data.data()))));
+  encoded_data.resize(encoded_size);
 
   // Write a few small chunks of new data, then a large chunk, and finally a few small chunks.
   // This helps us to catch potential short writes.
@@ -623,12 +680,15 @@
     "new 2,98,99",
     "new 2,99,100",
   };
-  ASSERT_EQ(0, zip_writer.StartEntry("transfer_list", 0));
-  std::string commands = android::base::Join(transfer_list, '\n');
-  ASSERT_EQ(0, zip_writer.WriteBytes(commands.data(), commands.size()));
-  ASSERT_EQ(0, zip_writer.FinishEntry());
-  ASSERT_EQ(0, zip_writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file_ptr));
+
+  std::unordered_map<std::string, std::string> entries = {
+    { "new.dat.br", std::move(encoded_data) },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
 
   MemMapping map;
   ASSERT_TRUE(map.MapFile(zip_file.path));
@@ -639,7 +699,7 @@
   UpdaterInfo updater_info;
   updater_info.package_zip = handle;
   TemporaryFile temp_pipe;
-  updater_info.cmd_pipe = fopen(temp_pipe.path, "wb");
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wb");
   updater_info.package_zip_addr = map.addr;
   updater_info.package_zip_len = map.length;
 
@@ -653,5 +713,221 @@
   std::string updated_content;
   ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
   ASSERT_EQ(brotli_new_data, updated_content);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, last_command_update) {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+
+  std::string block1 = std::string(4096, '1');
+  std::string block2 = std::string(4096, '2');
+  std::string block3 = std::string(4096, '3');
+  std::string block1_hash = get_sha1(block1);
+  std::string block2_hash = get_sha1(block2);
+  std::string block3_hash = get_sha1(block3);
+
+  // Compose the transfer list to fail the first update.
+  std::vector<std::string> transfer_list_fail = {
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+    "fail",
+  };
+
+  // Mimic a resumed update with the same transfer commands.
+  std::vector<std::string> transfer_list_continue = {
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+    "move " + block1_hash + " 2,2,3 1 2,0,1",
+  };
+
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list_fail", android::base::Join(transfer_list_fail, '\n') },
+    { "transfer_list_continue", android::base::Join(transfer_list_continue, '\n') },
+  };
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  std::string src_content = block1 + block2 + block3;
+  TemporaryFile update_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  std::string script =
+      "block_image_update(\"" + std::string(update_file.path) +
+      R"(", package_extract_file("transfer_list_fail"), "new_data", "patch_data"))";
+  expect("", script.c_str(), kNoCause, &updater_info);
+
+  // Expect last_command to contain the last stash command.
+  std::string last_command_content;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content));
+  EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content);
+  std::string updated_contents;
+  ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents));
+  ASSERT_EQ(block1 + block1 + block3, updated_contents);
+
+  // Resume the update, expect the first 'move' to be skipped but the second 'move' to be executed.
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  std::string script_second_update =
+      "block_image_update(\"" + std::string(update_file.path) +
+      R"(", package_extract_file("transfer_list_continue"), "new_data", "patch_data"))";
+  expect("t", script_second_update.c_str(), kNoCause, &updater_info);
+  ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents));
+  ASSERT_EQ(block1 + block2 + block1, updated_contents);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, last_command_update_unresumable) {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+
+  std::string block1 = std::string(4096, '1');
+  std::string block2 = std::string(4096, '2');
+  std::string block1_hash = get_sha1(block1);
+  std::string block2_hash = get_sha1(block2);
+
+  // Construct an unresumable update with source blocks mismatch.
+  std::vector<std::string> transfer_list_unresumable = {
+    "4", "2", "0", "2", "stash " + block1_hash + " 2,0,1", "move " + block2_hash + " 2,1,2 1 2,0,1",
+  };
+
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list_unresumable", android::base::Join(transfer_list_unresumable, '\n') },
+  };
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  // Set up the last_command_file
+  ASSERT_TRUE(
+      android::base::WriteStringToFile("0\nstash " + block1_hash + " 2,0,1", last_command_file));
+
+  // The last_command_file will be deleted if the update encounters an unresumable failure
+  // later.
+  std::string src_content = block1 + block1;
+  TemporaryFile update_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  std::string script =
+      "block_image_update(\"" + std::string(update_file.path) +
+      R"(", package_extract_file("transfer_list_unresumable"), "new_data", "patch_data"))";
+  expect("", script.c_str(), kNoCause, &updater_info);
+  ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK));
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
+TEST_F(UpdaterTest, last_command_verify) {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+
+  std::string block1 = std::string(4096, '1');
+  std::string block2 = std::string(4096, '2');
+  std::string block3 = std::string(4096, '3');
+  std::string block1_hash = get_sha1(block1);
+  std::string block2_hash = get_sha1(block2);
+  std::string block3_hash = get_sha1(block3);
+
+  std::vector<std::string> transfer_list_verify = {
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block1_hash + " 2,0,1 1 2,0,1",
+    "move " + block1_hash + " 2,1,2 1 2,0,1",
+    "stash " + block3_hash + " 2,2,3",
+  };
+
+  std::unordered_map<std::string, std::string> entries = {
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list_verify", android::base::Join(transfer_list_verify, '\n') },
+  };
+
+  // Build the update package.
+  TemporaryFile zip_file;
+  BuildUpdatePackage(entries, zip_file.release());
+
+  MemMapping map;
+  ASSERT_TRUE(map.MapFile(zip_file.path));
+  ZipArchiveHandle handle;
+  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+  // Set up the handler, command_pipe, patch offset & length.
+  UpdaterInfo updater_info;
+  updater_info.package_zip = handle;
+  TemporaryFile temp_pipe;
+  updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+  updater_info.package_zip_addr = map.addr;
+  updater_info.package_zip_len = map.length;
+
+  std::string src_content = block1 + block1 + block3;
+  TemporaryFile update_file;
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+
+  ASSERT_TRUE(
+      android::base::WriteStringToFile("2\nstash " + block3_hash + " 2,2,3", last_command_file));
+
+  // Expect the verification to succeed and the last_command_file is intact.
+  std::string script_verify =
+      "block_image_verify(\"" + std::string(update_file.path) +
+      R"(", package_extract_file("transfer_list_verify"), "new_data","patch_data"))";
+  expect("t", script_verify.c_str(), kNoCause, &updater_info);
+
+  std::string last_command_content;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content));
+  EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content);
+
+  // Expect the verification to succeed but last_command_file to be deleted; because the target
+  // blocks don't have the expected contents for the second move command.
+  src_content = block1 + block2 + block3;
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  expect("t", script_verify.c_str(), kNoCause, &updater_info);
+  ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK));
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
   CloseArchive(handle);
 }
diff --git a/tests/manual/recovery_test.cpp b/tests/manual/recovery_test.cpp
index 92c6ef2..64e3b59 100644
--- a/tests/manual/recovery_test.cpp
+++ b/tests/manual/recovery_test.cpp
@@ -209,7 +209,7 @@
     ASSERT_GT(height, y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
     char* loc = reinterpret_cast<char*>(&row[5]);
     if (matches_locale(loc, kLocale.c_str())) {
-      EXPECT_TRUE(android::base::StartsWith(loc, kLocale.c_str()));
+      EXPECT_TRUE(android::base::StartsWith(loc, kLocale));
       break;
     } else {
       for (int i = 0; i < h; ++i, ++y) {
diff --git a/tests/testdata/deflate_src.zip b/tests/testdata/deflate_src.zip
new file mode 100644
index 0000000..bdb2b32
--- /dev/null
+++ b/tests/testdata/deflate_src.zip
Binary files differ
diff --git a/tests/testdata/deflate_tgt.zip b/tests/testdata/deflate_tgt.zip
new file mode 100644
index 0000000..2a21760
--- /dev/null
+++ b/tests/testdata/deflate_tgt.zip
Binary files differ
diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp
index 5e2ae4f..7f85d13 100644
--- a/tests/unit/dirutil_test.cpp
+++ b/tests/unit/dirutil_test.cpp
@@ -26,23 +26,23 @@
 
 TEST(DirUtilTest, create_invalid) {
   // Requesting to create an empty dir is invalid.
-  ASSERT_EQ(-1, dirCreateHierarchy("", 0755, nullptr, false, nullptr));
+  ASSERT_EQ(-1, mkdir_recursively("", 0755, false, nullptr));
   ASSERT_EQ(ENOENT, errno);
 
   // Requesting to strip the name with no slash present.
-  ASSERT_EQ(-1, dirCreateHierarchy("abc", 0755, nullptr, true, nullptr));
+  ASSERT_EQ(-1, mkdir_recursively("abc", 0755, true, nullptr));
   ASSERT_EQ(ENOENT, errno);
 
   // Creating a dir that already exists.
   TemporaryDir td;
-  ASSERT_EQ(0, dirCreateHierarchy(td.path, 0755, nullptr, false, nullptr));
+  ASSERT_EQ(0, mkdir_recursively(td.path, 0755, false, nullptr));
 
   // "///" is a valid dir.
-  ASSERT_EQ(0, dirCreateHierarchy("///", 0755, nullptr, false, nullptr));
+  ASSERT_EQ(0, mkdir_recursively("///", 0755, false, nullptr));
 
   // Request to create a dir, but a file with the same name already exists.
   TemporaryFile tf;
-  ASSERT_EQ(-1, dirCreateHierarchy(tf.path, 0755, nullptr, false, nullptr));
+  ASSERT_EQ(-1, mkdir_recursively(tf.path, 0755, false, nullptr));
   ASSERT_EQ(ENOTDIR, errno);
 }
 
@@ -51,7 +51,7 @@
   std::string prefix(td.path);
   std::string path = prefix + "/a/b";
   constexpr mode_t mode = 0755;
-  ASSERT_EQ(0, dirCreateHierarchy(path.c_str(), mode, nullptr, false, nullptr));
+  ASSERT_EQ(0, mkdir_recursively(path, mode, false, nullptr));
 
   // Verify.
   struct stat sb;
@@ -69,7 +69,7 @@
   TemporaryDir td;
   std::string prefix(td.path);
   std::string path = prefix + "/a/b";
-  ASSERT_EQ(0, dirCreateHierarchy(path.c_str(), 0755, nullptr, true, nullptr));
+  ASSERT_EQ(0, mkdir_recursively(path, 0755, true, nullptr));
 
   // Verify that "../a" exists but not "../a/b".
   struct stat sb;
@@ -83,31 +83,21 @@
   ASSERT_EQ(0, rmdir((prefix + "/a").c_str()));
 }
 
-TEST(DirUtilTest, create_mode_and_timestamp) {
+TEST(DirUtilTest, create_mode) {
   TemporaryDir td;
   std::string prefix(td.path);
   std::string path = prefix + "/a/b";
-  // Set the timestamp to 8/1/2008.
-  constexpr struct utimbuf timestamp = { 1217592000, 1217592000 };
   constexpr mode_t mode = 0751;
-  ASSERT_EQ(0, dirCreateHierarchy(path.c_str(), mode, &timestamp, false, nullptr));
+  ASSERT_EQ(0, mkdir_recursively(path, mode, false, nullptr));
 
-  // Verify the mode and timestamp for "../a/b".
+  // Verify the mode for "../a/b".
   struct stat sb;
   ASSERT_EQ(0, stat(path.c_str(), &sb)) << strerror(errno);
   ASSERT_TRUE(S_ISDIR(sb.st_mode));
   constexpr mode_t mask = S_IRWXU | S_IRWXG | S_IRWXO;
   ASSERT_EQ(mode, sb.st_mode & mask);
 
-  timespec time;
-  time.tv_sec = 1217592000;
-  time.tv_nsec = 0;
-
-  ASSERT_EQ(time.tv_sec, static_cast<long>(sb.st_atime));
-  ASSERT_EQ(time.tv_sec, static_cast<long>(sb.st_mtime));
-
-  // Verify the mode for "../a". Note that the timestamp for intermediate directories (e.g. "../a")
-  // may not be 'timestamp' according to the current implementation.
+  // Verify the mode for "../a".
   ASSERT_EQ(0, stat((prefix + "/a").c_str(), &sb)) << strerror(errno);
   ASSERT_TRUE(S_ISDIR(sb.st_mode));
   ASSERT_EQ(mode, sb.st_mode & mask);
@@ -116,35 +106,3 @@
   ASSERT_EQ(0, rmdir((prefix + "/a/b").c_str()));
   ASSERT_EQ(0, rmdir((prefix + "/a").c_str()));
 }
-
-TEST(DirUtilTest, unlink_invalid) {
-  // File doesn't exist.
-  ASSERT_EQ(-1, dirUnlinkHierarchy("doesntexist"));
-
-  // Nonexistent directory.
-  TemporaryDir td;
-  std::string path(td.path);
-  ASSERT_EQ(-1, dirUnlinkHierarchy((path + "/a").c_str()));
-  ASSERT_EQ(ENOENT, errno);
-}
-
-TEST(DirUtilTest, unlink_smoke) {
-  // Unlink a file.
-  TemporaryFile tf;
-  ASSERT_EQ(0, dirUnlinkHierarchy(tf.path));
-  ASSERT_EQ(-1, access(tf.path, F_OK));
-
-  TemporaryDir td;
-  std::string path(td.path);
-  constexpr mode_t mode = 0700;
-  ASSERT_EQ(0, mkdir((path + "/a").c_str(), mode));
-  ASSERT_EQ(0, mkdir((path + "/a/b").c_str(), mode));
-  ASSERT_EQ(0, mkdir((path + "/a/b/c").c_str(), mode));
-  ASSERT_EQ(0, mkdir((path + "/a/d").c_str(), mode));
-
-  // Remove "../a" recursively.
-  ASSERT_EQ(0, dirUnlinkHierarchy((path + "/a").c_str()));
-
-  // Verify it's gone.
-  ASSERT_EQ(-1, access((path + "/a").c_str(), F_OK));
-}
diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp
index 3c6d77e..7ae193e 100644
--- a/tests/unit/rangeset_test.cpp
+++ b/tests/unit/rangeset_test.cpp
@@ -17,11 +17,23 @@
 #include <signal.h>
 #include <sys/types.h>
 
+#include <limits>
 #include <vector>
 
 #include <gtest/gtest.h>
 
-#include "updater/rangeset.h"
+#include "otautil/rangeset.h"
+
+TEST(RangeSetTest, ctor) {
+  RangeSet rs(std::vector<Range>{ Range{ 8, 10 }, Range{ 1, 5 } });
+  ASSERT_TRUE(rs);
+
+  RangeSet rs2(std::vector<Range>{});
+  ASSERT_FALSE(rs2);
+
+  RangeSet rs3(std::vector<Range>{ Range{ 8, 10 }, Range{ 5, 1 } });
+  ASSERT_FALSE(rs3);
+}
 
 TEST(RangeSetTest, Parse_smoke) {
   RangeSet rs = RangeSet::Parse("2,1,10");
@@ -37,27 +49,64 @@
 
   // Leading zeros are fine. But android::base::ParseUint() doesn't like trailing zeros like "10 ".
   ASSERT_EQ(rs, RangeSet::Parse(" 2, 1,   10"));
-  ASSERT_EXIT(RangeSet::Parse("2,1,10 "), ::testing::KilledBySignal(SIGABRT), "");
+  ASSERT_FALSE(RangeSet::Parse("2,1,10 "));
 }
 
 TEST(RangeSetTest, Parse_InvalidCases) {
   // Insufficient number of tokens.
-  ASSERT_EXIT(RangeSet::Parse(""), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("2,1"), ::testing::KilledBySignal(SIGABRT), "");
+  ASSERT_FALSE(RangeSet::Parse(""));
+  ASSERT_FALSE(RangeSet::Parse("2,1"));
 
   // The first token (i.e. the number of following tokens) is invalid.
-  ASSERT_EXIT(RangeSet::Parse("a,1,1"), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("3,1,1"), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("-3,1,1"), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("2,1,2,3"), ::testing::KilledBySignal(SIGABRT), "");
+  ASSERT_FALSE(RangeSet::Parse("a,1,1"));
+  ASSERT_FALSE(RangeSet::Parse("3,1,1"));
+  ASSERT_FALSE(RangeSet::Parse("-3,1,1"));
+  ASSERT_FALSE(RangeSet::Parse("2,1,2,3"));
 
   // Invalid tokens.
-  ASSERT_EXIT(RangeSet::Parse("2,1,10a"), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("2,,10"), ::testing::KilledBySignal(SIGABRT), "");
+  ASSERT_FALSE(RangeSet::Parse("2,1,10a"));
+  ASSERT_FALSE(RangeSet::Parse("2,,10"));
 
   // Empty or negative range.
-  ASSERT_EXIT(RangeSet::Parse("2,2,2"), ::testing::KilledBySignal(SIGABRT), "");
-  ASSERT_EXIT(RangeSet::Parse("2,2,1"), ::testing::KilledBySignal(SIGABRT), "");
+  ASSERT_FALSE(RangeSet::Parse("2,2,2"));
+  ASSERT_FALSE(RangeSet::Parse("2,2,1"));
+}
+
+TEST(RangeSetTest, Clear) {
+  RangeSet rs = RangeSet::Parse("2,1,6");
+  ASSERT_TRUE(rs);
+  rs.Clear();
+  ASSERT_FALSE(rs);
+
+  // No-op to clear an empty RangeSet.
+  rs.Clear();
+  ASSERT_FALSE(rs);
+}
+
+TEST(RangeSetTest, PushBack) {
+  RangeSet rs;
+  ASSERT_FALSE(rs);
+
+  ASSERT_TRUE(rs.PushBack({ 3, 5 }));
+  ASSERT_EQ(RangeSet::Parse("2,3,5"), rs);
+
+  ASSERT_TRUE(rs.PushBack({ 5, 15 }));
+  ASSERT_EQ(RangeSet::Parse("4,3,5,5,15"), rs);
+  ASSERT_EQ(static_cast<size_t>(2), rs.size());
+  ASSERT_EQ(static_cast<size_t>(12), rs.blocks());
+}
+
+TEST(RangeSetTest, PushBack_InvalidInput) {
+  RangeSet rs;
+  ASSERT_FALSE(rs);
+  ASSERT_FALSE(rs.PushBack({ 5, 3 }));
+  ASSERT_FALSE(rs);
+  ASSERT_FALSE(rs.PushBack({ 15, 15 }));
+  ASSERT_FALSE(rs);
+
+  ASSERT_TRUE(rs.PushBack({ 5, 15 }));
+  ASSERT_FALSE(rs.PushBack({ 5, std::numeric_limits<size_t>::max() - 2 }));
+  ASSERT_EQ(RangeSet::Parse("2,5,15"), rs);
 }
 
 TEST(RangeSetTest, Overlaps) {
@@ -74,6 +123,86 @@
   ASSERT_FALSE(RangeSet::Parse("2,5,7").Overlaps(RangeSet::Parse("2,3,5")));
 }
 
+TEST(RangeSetTest, Split) {
+  RangeSet rs1 = RangeSet::Parse("2,1,2");
+  ASSERT_TRUE(rs1);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,1,2") }), rs1.Split(1));
+
+  RangeSet rs2 = RangeSet::Parse("2,5,10");
+  ASSERT_TRUE(rs2);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,5,8"), RangeSet::Parse("2,8,10") }),
+            rs2.Split(2));
+
+  RangeSet rs3 = RangeSet::Parse("4,0,1,5,10");
+  ASSERT_TRUE(rs3);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("4,0,1,5,7"), RangeSet::Parse("2,7,10") }),
+            rs3.Split(2));
+
+  RangeSet rs4 = RangeSet::Parse("6,1,3,3,4,4,5");
+  ASSERT_TRUE(rs4);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,1,3"), RangeSet::Parse("2,3,4"),
+                                    RangeSet::Parse("2,4,5") }),
+            rs4.Split(3));
+
+  RangeSet rs5 = RangeSet::Parse("2,0,10");
+  ASSERT_TRUE(rs5);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,0,3"), RangeSet::Parse("2,3,6"),
+                                    RangeSet::Parse("2,6,8"), RangeSet::Parse("2,8,10") }),
+            rs5.Split(4));
+
+  RangeSet rs6 = RangeSet::Parse(
+      "20,0,268,269,271,286,447,8350,32770,33022,98306,98558,163842,164094,196609,204800,229378,"
+      "229630,294914,295166,457564");
+  ASSERT_TRUE(rs6);
+  size_t rs6_blocks = rs6.blocks();
+  auto splits = rs6.Split(4);
+  ASSERT_EQ(
+      (std::vector<RangeSet>{
+          RangeSet::Parse("12,0,268,269,271,286,447,8350,32770,33022,98306,98558,118472"),
+          RangeSet::Parse("8,118472,163842,164094,196609,204800,229378,229630,237216"),
+          RangeSet::Parse("4,237216,294914,295166,347516"), RangeSet::Parse("2,347516,457564") }),
+      splits);
+  size_t sum = 0;
+  for (const auto& element : splits) {
+    sum += element.blocks();
+  }
+  ASSERT_EQ(rs6_blocks, sum);
+}
+
+TEST(RangeSetTest, Split_EdgeCases) {
+  // Empty RangeSet.
+  RangeSet rs1;
+  ASSERT_FALSE(rs1);
+  ASSERT_EQ((std::vector<RangeSet>{}), rs1.Split(2));
+  ASSERT_FALSE(rs1);
+
+  // Zero group.
+  RangeSet rs2 = RangeSet::Parse("2,1,5");
+  ASSERT_TRUE(rs2);
+  ASSERT_EQ((std::vector<RangeSet>{}), rs2.Split(0));
+
+  // The number of blocks equals to the number of groups.
+  RangeSet rs3 = RangeSet::Parse("2,1,5");
+  ASSERT_TRUE(rs3);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,1,2"), RangeSet::Parse("2,2,3"),
+                                    RangeSet::Parse("2,3,4"), RangeSet::Parse("2,4,5") }),
+            rs3.Split(4));
+
+  // Less blocks than the number of groups.
+  RangeSet rs4 = RangeSet::Parse("2,1,5");
+  ASSERT_TRUE(rs4);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,1,2"), RangeSet::Parse("2,2,3"),
+                                    RangeSet::Parse("2,3,4"), RangeSet::Parse("2,4,5") }),
+            rs4.Split(8));
+
+  // Less blocks than the number of groups.
+  RangeSet rs5 = RangeSet::Parse("2,0,3");
+  ASSERT_TRUE(rs5);
+  ASSERT_EQ((std::vector<RangeSet>{ RangeSet::Parse("2,0,1"), RangeSet::Parse("2,1,2"),
+                                    RangeSet::Parse("2,2,3") }),
+            rs5.Split(4));
+}
+
 TEST(RangeSetTest, GetBlockNumber) {
   RangeSet rs = RangeSet::Parse("2,1,10");
   ASSERT_EQ(static_cast<size_t>(1), rs.GetBlockNumber(0));
@@ -90,7 +219,7 @@
   ASSERT_NE(RangeSet::Parse("2,1,6"), RangeSet::Parse("2,1,7"));
   ASSERT_NE(RangeSet::Parse("2,1,6"), RangeSet::Parse("2,2,7"));
 
-  // The orders of Range's matter. "4,1,5,8,10" != "4,8,10,1,5".
+  // The orders of Range's matter, e.g. "4,1,5,8,10" != "4,8,10,1,5".
   ASSERT_NE(RangeSet::Parse("4,1,5,8,10"), RangeSet::Parse("4,8,10,1,5"));
 }
 
@@ -110,3 +239,51 @@
   }
   ASSERT_EQ((std::vector<Range>{ Range{ 8, 10 }, Range{ 1, 5 } }), ranges);
 }
+
+TEST(RangeSetTest, ToString) {
+  ASSERT_EQ("", RangeSet::Parse("").ToString());
+  ASSERT_EQ("2,1,6", RangeSet::Parse("2,1,6").ToString());
+  ASSERT_EQ("4,1,5,8,10", RangeSet::Parse("4,1,5,8,10").ToString());
+  ASSERT_EQ("6,1,3,4,6,15,22", RangeSet::Parse("6,1,3,4,6,15,22").ToString());
+}
+
+TEST(SortedRangeSetTest, Insert) {
+  SortedRangeSet rs({ { 2, 3 }, { 4, 6 }, { 8, 14 } });
+  rs.Insert({ 1, 2 });
+  ASSERT_EQ(SortedRangeSet({ { 1, 3 }, { 4, 6 }, { 8, 14 } }), rs);
+  ASSERT_EQ(static_cast<size_t>(10), rs.blocks());
+  rs.Insert({ 3, 5 });
+  ASSERT_EQ(SortedRangeSet({ { 1, 6 }, { 8, 14 } }), rs);
+  ASSERT_EQ(static_cast<size_t>(11), rs.blocks());
+
+  SortedRangeSet r1({ { 20, 22 }, { 15, 18 } });
+  rs.Insert(r1);
+  ASSERT_EQ(SortedRangeSet({ { 1, 6 }, { 8, 14 }, { 15, 18 }, { 20, 22 } }), rs);
+  ASSERT_EQ(static_cast<size_t>(16), rs.blocks());
+
+  SortedRangeSet r2({ { 2, 7 }, { 15, 21 }, { 20, 25 } });
+  rs.Insert(r2);
+  ASSERT_EQ(SortedRangeSet({ { 1, 7 }, { 8, 14 }, { 15, 25 } }), rs);
+  ASSERT_EQ(static_cast<size_t>(22), rs.blocks());
+}
+
+TEST(SortedRangeSetTest, file_range) {
+  SortedRangeSet rs;
+  rs.Insert(4096, 4096);
+  ASSERT_EQ(SortedRangeSet({ { 1, 2 } }), rs);
+  // insert block 2-9
+  rs.Insert(4096 * 3 - 1, 4096 * 7);
+  ASSERT_EQ(SortedRangeSet({ { 1, 10 } }), rs);
+  // insert block 15-19
+  rs.Insert(4096 * 15 + 1, 4096 * 4);
+  ASSERT_EQ(SortedRangeSet({ { 1, 10 }, { 15, 20 } }), rs);
+
+  // rs overlaps block 2-2
+  ASSERT_TRUE(rs.Overlaps(4096 * 2 - 1, 10));
+  ASSERT_FALSE(rs.Overlaps(4096 * 10, 4096 * 5));
+
+  ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106));
+  ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10));
+  // block#10 not in range.
+  ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), "");
+}
diff --git a/tools/recovery_l10n/Android.mk b/tools/recovery_l10n/Android.mk
index 937abd1..7197c5c 100644
--- a/tools/recovery_l10n/Android.mk
+++ b/tools/recovery_l10n/Android.mk
@@ -5,6 +5,7 @@
 include $(CLEAR_VARS)
 
 LOCAL_PACKAGE_NAME := RecoveryLocalizer
+LOCAL_SDK_VERSION := current
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/tools/recovery_l10n/res/values-as/strings.xml b/tools/recovery_l10n/res/values-as/strings.xml
new file mode 100644
index 0000000..2624ceb
--- /dev/null
+++ b/tools/recovery_l10n/res/values-as/strings.xml
@@ -0,0 +1,9 @@
+<?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="2013591905463558223">"আপডেইট ইনষ্টল কৰি থকা হৈছে"</string>
+    <string name="recovery_erasing" msgid="7334826894904037088">"মচি থকা হৈছে"</string>
+    <string name="recovery_no_command" msgid="4465476568623024327">"কোনো আদেশ নাই"</string>
+    <string name="recovery_error" msgid="5748178989622716736">"ত্ৰুটি!"</string>
+    <string name="recovery_installing_security" msgid="9184031299717114342">"সুৰক্ষা আপডেইট ইনষ্টল কৰি থকা হৈছে"</string>
+</resources>
diff --git a/tools/recovery_l10n/res/values-or/strings.xml b/tools/recovery_l10n/res/values-or/strings.xml
new file mode 100644
index 0000000..2b0851c
--- /dev/null
+++ b/tools/recovery_l10n/res/values-or/strings.xml
@@ -0,0 +1,9 @@
+<?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="2013591905463558223">"ସିଷ୍ଟମ ଅପଡେଟ ଇନଷ୍ଟଲ କରୁଛି"</string>
+    <string name="recovery_erasing" msgid="7334826894904037088">"ଲିଭାଉଛି"</string>
+    <string name="recovery_no_command" msgid="4465476568623024327">"କୌଣସି କମାଣ୍ଡ ନାହିଁ"</string>
+    <string name="recovery_error" msgid="5748178989622716736">"ତ୍ରୁଟି!"</string>
+    <string name="recovery_installing_security" msgid="9184031299717114342">"ସୁରକ୍ଷା ଅପ୍‌ଡେଟ୍‌ ଇନ୍‌ଷ୍ଟଲ୍‌ କରୁଛି"</string>
+</resources>
diff --git a/ui.cpp b/ui.cpp
index e80d7ed..3c9ded7 100644
--- a/ui.cpp
+++ b/ui.cpp
@@ -48,12 +48,16 @@
 static constexpr int UI_WAIT_KEY_TIMEOUT_SEC = 120;
 static constexpr const char* BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/brightness";
 static constexpr const char* MAX_BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/max_brightness";
+static constexpr const char* BRIGHTNESS_FILE_SDM =
+    "/sys/class/backlight/panel0-backlight/brightness";
+static constexpr const char* MAX_BRIGHTNESS_FILE_SDM =
+    "/sys/class/backlight/panel0-backlight/max_brightness";
 
 RecoveryUI::RecoveryUI()
-    : locale_(""),
-      rtl_locale_(false),
-      brightness_normal_(50),
+    : brightness_normal_(50),
       brightness_dimmed_(25),
+      brightness_file_(BRIGHTNESS_FILE),
+      max_brightness_file_(MAX_BRIGHTNESS_FILE),
       touch_screen_allowed_(false),
       kTouchLowThreshold(RECOVERY_UI_TOUCH_LOW_THRESHOLD),
       kTouchHighThreshold(RECOVERY_UI_TOUCH_HIGH_THRESHOLD),
@@ -103,12 +107,17 @@
   if (brightness_normal_ == 0 || brightness_dimmed_ > brightness_normal_) {
     return false;
   }
-
+  if (access(brightness_file_.c_str(), R_OK | W_OK)) {
+    brightness_file_ = BRIGHTNESS_FILE_SDM;
+  }
+  if (access(max_brightness_file_.c_str(), R_OK)) {
+    max_brightness_file_ = MAX_BRIGHTNESS_FILE_SDM;
+  }
   // Set the initial brightness level based on the max brightness. Note that reading the initial
   // value from BRIGHTNESS_FILE doesn't give the actual brightness value (bullhead, sailfish), so
   // we don't have a good way to query the default value.
   std::string content;
-  if (!android::base::ReadFileToString(MAX_BRIGHTNESS_FILE, &content)) {
+  if (!android::base::ReadFileToString(max_brightness_file_, &content)) {
     PLOG(WARNING) << "Failed to read max brightness";
     return false;
   }
@@ -122,7 +131,7 @@
   brightness_normal_value_ = max_value * brightness_normal_ / 100.0;
   brightness_dimmed_value_ = max_value * brightness_dimmed_ / 100.0;
   if (!android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
-                                        BRIGHTNESS_FILE)) {
+                                        brightness_file_)) {
     PLOG(WARNING) << "Failed to set brightness";
     return false;
   }
@@ -132,10 +141,7 @@
   return true;
 }
 
-bool RecoveryUI::Init(const std::string& locale) {
-  // Set up the locale info.
-  SetLocale(locale);
-
+bool RecoveryUI::Init(const std::string& /* locale */) {
   ev_init(std::bind(&RecoveryUI::OnInputEvent, this, std::placeholders::_1, std::placeholders::_2),
           touch_screen_allowed_);
 
@@ -435,13 +441,13 @@
         // Lower the brightness level: NORMAL -> DIMMED; DIMMED -> OFF.
         if (screensaver_state_ == ScreensaverState::NORMAL) {
           if (android::base::WriteStringToFile(std::to_string(brightness_dimmed_value_),
-                                               BRIGHTNESS_FILE)) {
+                                               brightness_file_)) {
             LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
                       << "%)";
             screensaver_state_ = ScreensaverState::DIMMED;
           }
         } else if (screensaver_state_ == ScreensaverState::DIMMED) {
-          if (android::base::WriteStringToFile("0", BRIGHTNESS_FILE)) {
+          if (android::base::WriteStringToFile("0", brightness_file_)) {
             LOG(INFO) << "Brightness: 0 (off)";
             screensaver_state_ = ScreensaverState::OFF;
           }
@@ -456,7 +462,7 @@
 
         // Reset the brightness to normal.
         if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
-                                             BRIGHTNESS_FILE)) {
+                                             brightness_file_)) {
           screensaver_state_ = ScreensaverState::NORMAL;
           LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
                     << "%)";
@@ -574,23 +580,3 @@
   enable_reboot = enabled;
   pthread_mutex_unlock(&key_queue_mutex);
 }
-
-void RecoveryUI::SetLocale(const std::string& new_locale) {
-  this->locale_ = new_locale;
-  this->rtl_locale_ = false;
-
-  if (!new_locale.empty()) {
-    size_t underscore = new_locale.find('_');
-    // lang has the language prefix prior to '_', or full string if '_' doesn't exist.
-    std::string lang = new_locale.substr(0, underscore);
-
-    // A bit cheesy: keep an explicit list of supported RTL languages.
-    if (lang == "ar" ||  // Arabic
-        lang == "fa" ||  // Persian (Farsi)
-        lang == "he" ||  // Hebrew (new language code)
-        lang == "iw" ||  // Hebrew (old language code)
-        lang == "ur") {  // Urdu
-      rtl_locale_ = true;
-    }
-  }
-}
diff --git a/ui.h b/ui.h
index 3d9afec..4c54d69 100644
--- a/ui.h
+++ b/ui.h
@@ -26,6 +26,27 @@
 // Abstract class for controlling the user interface during recovery.
 class RecoveryUI {
  public:
+  enum Icon {
+    NONE,
+    INSTALLING_UPDATE,
+    ERASING,
+    NO_COMMAND,
+    ERROR
+  };
+
+  enum ProgressType {
+    EMPTY,
+    INDETERMINATE,
+    DETERMINATE
+  };
+
+  enum KeyAction {
+    ENQUEUE,
+    TOGGLE,
+    REBOOT,
+    IGNORE
+  };
+
   RecoveryUI();
 
   virtual ~RecoveryUI() {}
@@ -38,12 +59,10 @@
   virtual void SetStage(int current, int max) = 0;
 
   // Sets the overall recovery state ("background image").
-  enum Icon { NONE, INSTALLING_UPDATE, ERASING, NO_COMMAND, ERROR };
   virtual void SetBackground(Icon icon) = 0;
   virtual void SetSystemUpdateText(bool security_update) = 0;
 
   // --- progress indicator ---
-  enum ProgressType { EMPTY, INDETERMINATE, DETERMINATE };
   virtual void SetProgressType(ProgressType determinate) = 0;
 
   // Shows a progress bar and define the scope of the next operation:
@@ -94,7 +113,6 @@
   // 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
@@ -125,24 +143,47 @@
  protected:
   void EnqueueKey(int key_code);
 
-  // The locale that's used to show the rendered texts.
-  std::string locale_;
-  bool rtl_locale_;
-
   // The normal and dimmed brightness percentages (default: 50 and 25, which means 50% and 25% of
   // the max_brightness). Because the absolute values may vary across devices. These two values can
   // be configured via subclassing. Setting brightness_normal_ to 0 to disable screensaver.
   unsigned int brightness_normal_;
   unsigned int brightness_dimmed_;
+  std::string brightness_file_;
+  std::string max_brightness_file_;
 
   // Whether we should listen for touch inputs (default: false).
   bool touch_screen_allowed_;
 
  private:
+  enum class ScreensaverState {
+    DISABLED,
+    NORMAL,
+    DIMMED,
+    OFF
+  };
+
+  struct key_timer_t {
+    RecoveryUI* ui;
+    int key_code;
+    int count;
+  };
+
   // The sensitivity when detecting a swipe.
   const int kTouchLowThreshold;
   const int kTouchHighThreshold;
 
+  void OnKeyDetected(int key_code);
+  void OnTouchDetected(int dx, int dy);
+  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);
+
+  bool InitScreensaver();
+
   // Key event input queue
   pthread_mutex_t key_queue_mutex;
   pthread_cond_t key_queue_cond;
@@ -172,33 +213,14 @@
   bool touch_swiping_;
   bool is_bootreason_recovery_ui_;
 
-  struct key_timer_t {
-    RecoveryUI* ui;
-    int key_code;
-    int count;
-  };
-
   pthread_t input_thread_;
 
-  void OnKeyDetected(int key_code);
-  void OnTouchDetected(int dx, int dy);
-  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);
-
-  void SetLocale(const std::string&);
-
-  enum class ScreensaverState { DISABLED, NORMAL, DIMMED, OFF };
   ScreensaverState screensaver_state_;
+
   // The following two contain the absolute values computed from brightness_normal_ and
   // brightness_dimmed_ respectively.
   unsigned int brightness_normal_value_;
   unsigned int brightness_dimmed_value_;
-  bool InitScreensaver();
 };
 
 #endif  // RECOVERY_UI_H
diff --git a/uncrypt/Android.bp b/uncrypt/Android.bp
new file mode 100644
index 0000000..aa56d2f
--- /dev/null
+++ b/uncrypt/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.
+
+cc_binary {
+    name: "uncrypt",
+
+    srcs: [
+        "uncrypt.cpp",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+
+    static_libs: [
+        "libbootloader_message",
+        "libotautil",
+        "libfs_mgr",
+        "libbase",
+        "libcutils",
+        "liblog",
+    ],
+
+    init_rc: [
+        "uncrypt.rc",
+    ],
+}
diff --git a/uncrypt/Android.mk b/uncrypt/Android.mk
deleted file mode 100644
index 59084b0..0000000
--- a/uncrypt/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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 := \
-    libbootloader_message \
-    libbase \
-    liblog \
-    libfs_mgr \
-    libcutils
-LOCAL_CFLAGS := -Werror
-LOCAL_INIT_RC := uncrypt.rc
-
-include $(BUILD_EXECUTABLE)
diff --git a/uncrypt/uncrypt.cpp b/uncrypt/uncrypt.cpp
index ad3bdce..bb43c2c 100644
--- a/uncrypt/uncrypt.cpp
+++ b/uncrypt/uncrypt.cpp
@@ -116,7 +116,7 @@
 #include <cutils/sockets.h>
 #include <fs_mgr.h>
 
-#include "error_code.h"
+#include "otautil/error_code.h"
 
 static constexpr int WINDOW_SIZE = 5;
 static constexpr int FIBMAP_RETRY_LIMIT = 3;
@@ -172,10 +172,14 @@
     return fstab;
 }
 
-static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted) {
+static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted, bool *f2fs_fs) {
     // Look for a volume whose mount point is the prefix of path and
     // return its block device.  Set encrypted if it's currently
     // encrypted.
+
+    // ensure f2fs_fs is set to 0 first.
+    if (f2fs_fs)
+        *f2fs_fs = false;
     for (int i = 0; i < fstab->num_entries; ++i) {
         struct fstab_rec* v = &fstab->recs[i];
         if (!v->mount_point) {
@@ -192,6 +196,8 @@
                     *encrypted = true;
                 }
             }
+            if (f2fs_fs && strcmp(v->fs_type, "f2fs") == 0)
+                *f2fs_fs = true;
             return v->blk_device;
         }
     }
@@ -244,7 +250,7 @@
 }
 
 static int produce_block_map(const char* path, const char* map_file, const char* blk_dev,
-                             bool encrypted, int socket) {
+                             bool encrypted, bool f2fs_fs, int socket) {
     std::string err;
     if (!android::base::RemoveFileIfExists(map_file, &err)) {
         LOG(ERROR) << "failed to remove the existing map file " << map_file << ": " << err;
@@ -307,6 +313,17 @@
         }
     }
 
+#ifndef F2FS_IOC_SET_DONTMOVE
+#ifndef F2FS_IOCTL_MAGIC
+#define F2FS_IOCTL_MAGIC		0xf5
+#endif
+#define F2FS_IOC_SET_DONTMOVE		_IO(F2FS_IOCTL_MAGIC, 13)
+#endif
+    if (f2fs_fs && ioctl(fd, F2FS_IOC_SET_DONTMOVE) < 0) {
+        PLOG(ERROR) << "Failed to set non-movable file for f2fs: " << path << " on " << blk_dev;
+        return kUncryptIoctlError;
+    }
+
     off64_t pos = 0;
     int last_progress = 0;
     while (pos < sb.st_size) {
@@ -448,20 +465,21 @@
 static int uncrypt(const char* input_path, const char* map_file, const int socket) {
     LOG(INFO) << "update package is \"" << 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.
+    // 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) {
+    if (realpath(input_path, path) == nullptr) {
         PLOG(ERROR) << "failed to convert \"" << input_path << "\" to absolute path";
-        return 1;
+        return kUncryptRealpathFindError;
     }
 
     bool encryptable;
     bool encrypted;
-    const char* blk_dev = find_block_device(path, &encryptable, &encrypted);
-    if (blk_dev == NULL) {
+    bool f2fs_fs;
+    const char* blk_dev = find_block_device(path, &encryptable, &encrypted, &f2fs_fs);
+    if (blk_dev == nullptr) {
         LOG(ERROR) << "failed to find block device for " << path;
-        return 1;
+        return kUncryptBlockDeviceFindError;
     }
 
     // If the filesystem it's on isn't encrypted, we only produce the
@@ -479,7 +497,7 @@
     // and /sdcard we leave the file alone.
     if (strncmp(path, "/data/", 6) == 0) {
         LOG(INFO) << "writing block map " << map_file;
-        return produce_block_map(path, map_file, blk_dev, encrypted, socket);
+        return produce_block_map(path, map_file, blk_dev, encrypted, f2fs_fs, socket);
     }
 
     return 0;
diff --git a/update_verifier/Android.mk b/update_verifier/Android.mk
index 33c5fe9..0ff8854 100644
--- a/update_verifier/Android.mk
+++ b/update_verifier/Android.mk
@@ -22,6 +22,10 @@
     update_verifier.cpp
 
 LOCAL_MODULE := libupdate_verifier
+
+LOCAL_STATIC_LIBRARIES := \
+    libotautil
+
 LOCAL_SHARED_LIBRARIES := \
     libbase \
     libcutils \
@@ -54,7 +58,9 @@
 
 LOCAL_MODULE := update_verifier
 LOCAL_STATIC_LIBRARIES := \
-    libupdate_verifier
+    libupdate_verifier \
+    libotautil
+
 LOCAL_SHARED_LIBRARIES := \
     libbase \
     libcutils \
diff --git a/update_verifier/update_verifier.cpp b/update_verifier/update_verifier.cpp
index ba7b7ae..92d9313 100644
--- a/update_verifier/update_verifier.cpp
+++ b/update_verifier/update_verifier.cpp
@@ -58,6 +58,8 @@
 #include <android/hardware/boot/1.0/IBootControl.h>
 #include <cutils/android_reboot.h>
 
+#include "otautil/rangeset.h"
+
 using android::sp;
 using android::hardware::boot::V1_0::IBootControl;
 using android::hardware::boot::V1_0::BoolResult;
@@ -72,14 +74,13 @@
 }
 
 static bool read_blocks(const std::string& partition, const std::string& range_str) {
-  if (partition != "system" && partition != "vendor") {
-    LOG(ERROR) << "partition name must be system or vendor: " << partition;
+  if (partition != "system" && partition != "vendor" && partition != "product") {
+    LOG(ERROR) << "Invalid partition name \"" << partition << "\"";
     return false;
   }
-  // Iterate the content of "/sys/block/dm-X/dm/name". If it matches "system"
-  // (or "vendor"), then dm-X is a dm-wrapped system/vendor partition.
-  // Afterwards, update_verifier will read every block on the care_map_file of
-  // "/dev/block/dm-X" to ensure the partition's integrity.
+  // Iterate the content of "/sys/block/dm-X/dm/name". If it matches one of "system", "vendor" or
+  // "product", then dm-X is a dm-wrapped device for that target. We will later read all the
+  // ("cared") blocks from "/dev/block/dm-X" to ensure the target partition's integrity.
   static constexpr auto DM_PATH_PREFIX = "/sys/block/";
   dirent** namelist;
   int n = scandir(DM_PATH_PREFIX, &namelist, dm_name_filter, alphasort);
@@ -129,42 +130,33 @@
   // followed by 'count' number comma separated integers. Every two integers reprensent a
   // block range with the first number included in range but second number not included.
   // For example '4,64536,65343,74149,74150' represents: [64536,65343) and [74149,74150).
-  std::vector<std::string> ranges = android::base::Split(range_str, ",");
-  size_t range_count;
-  bool status = android::base::ParseUint(ranges[0], &range_count);
-  if (!status || (range_count == 0) || (range_count % 2 != 0) ||
-      (range_count != ranges.size() - 1)) {
-    LOG(ERROR) << "Error in parsing range string.";
+  RangeSet ranges = RangeSet::Parse(range_str);
+  if (!ranges) {
+    LOG(ERROR) << "Error parsing RangeSet string " << range_str;
     return false;
   }
-  range_count /= 2;
+
+  // RangeSet::Split() splits the ranges into multiple groups with same number of blocks (except for
+  // the last group).
+  size_t thread_num = std::thread::hardware_concurrency() ?: 4;
+  std::vector<RangeSet> groups = ranges.Split(thread_num);
 
   std::vector<std::future<bool>> threads;
-  size_t thread_num = std::thread::hardware_concurrency() ?: 4;
-  thread_num = std::min(thread_num, range_count);
-  size_t group_range_count = (range_count + thread_num - 1) / thread_num;
-
-  for (size_t t = 0; t < thread_num; t++) {
-    auto thread_func = [t, group_range_count, &dm_block_device, &ranges, &partition]() {
-      size_t blk_count = 0;
-      static constexpr size_t kBlockSize = 4096;
-      std::vector<uint8_t> buf(1024 * kBlockSize);
+  for (const auto& group : groups) {
+    auto thread_func = [&group, &dm_block_device, &partition]() {
       android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(dm_block_device.c_str(), O_RDONLY)));
       if (fd.get() == -1) {
         PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition;
         return false;
       }
 
-      for (size_t i = group_range_count * 2 * t + 1;
-           i < std::min(group_range_count * 2 * (t + 1) + 1, ranges.size()); i += 2) {
-        unsigned int range_start, range_end;
-        bool parse_status = android::base::ParseUint(ranges[i], &range_start);
-        parse_status = parse_status && android::base::ParseUint(ranges[i + 1], &range_end);
-        if (!parse_status || range_start >= range_end) {
-          LOG(ERROR) << "Invalid range pair " << ranges[i] << ", " << ranges[i + 1];
-          return false;
-        }
+      static constexpr size_t kBlockSize = 4096;
+      std::vector<uint8_t> buf(1024 * kBlockSize);
 
+      size_t block_count = 0;
+      for (const auto& range : group) {
+        size_t range_start = range.first;
+        size_t range_end = range.second;
         if (lseek64(fd.get(), static_cast<off64_t>(range_start) * kBlockSize, SEEK_SET) == -1) {
           PLOG(ERROR) << "lseek to " << range_start << " failed";
           return false;
@@ -179,9 +171,9 @@
           }
           remain -= to_read;
         }
-        blk_count += (range_end - range_start);
+        block_count += (range_end - range_start);
       }
-      LOG(INFO) << "Finished reading " << blk_count << " blocks on " << dm_block_device;
+      LOG(INFO) << "Finished reading " << block_count << " blocks on " << dm_block_device;
       return true;
     };
 
@@ -213,10 +205,9 @@
     PLOG(WARNING) << "Failed to open " << care_map_name;
     return true;
   }
-  // Care map file has four lines (two lines if vendor partition is not present):
-  // First line has the block partition name (system/vendor).
-  // Second line holds all ranges of blocks to verify.
-  // The next two lines have the same format but for vendor partition.
+  // care_map file has up to six lines, where every two lines make a pair. Within each pair, the
+  // first line has the partition name (e.g. "system"), while the second line holds the ranges of
+  // all the blocks to verify.
   std::string file_content;
   if (!android::base::ReadFdToString(care_map_fd.get(), &file_content)) {
     LOG(ERROR) << "Error reading care map contents to string.";
@@ -225,9 +216,9 @@
 
   std::vector<std::string> lines;
   lines = android::base::Split(android::base::Trim(file_content), "\n");
-  if (lines.size() != 2 && lines.size() != 4) {
+  if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
     LOG(ERROR) << "Invalid lines in care_map: found " << lines.size()
-               << " lines, expecting 2 or 4 lines.";
+               << " lines, expecting 2 or 4 or 6 lines.";
     return false;
   }
 
diff --git a/update_verifier/update_verifier_main.cpp b/update_verifier/update_verifier_main.cpp
index 46e8bbb..a86203b 100644
--- a/update_verifier/update_verifier_main.cpp
+++ b/update_verifier/update_verifier_main.cpp
@@ -16,8 +16,23 @@
 
 // See the comments in update_verifier.cpp.
 
+#include <string>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+
 #include "update_verifier/update_verifier.h"
 
 int main(int argc, char** argv) {
+  std::string s = android::base::GetProperty("ro.boot.slot_suffix", "");
+
+  if (s.empty()) {
+    return 0;  // non-A/B update device, so we quit
+  }
+
+  // Set up update_verifier logging to be written to kmsg; because we may not have Logd during
+  // boot time.
+  android::base::InitLogging(argv, &android::base::KernelLogger);
+
   return update_verifier(argc, argv);
 }
diff --git a/updater/Android.mk b/updater/Android.mk
index 86dc48e..6f334ee 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -66,7 +66,7 @@
     external/e2fsprogs/misc
 
 LOCAL_CFLAGS := \
-    -Wno-unused-parameter \
+    -Wall \
     -Werror
 
 LOCAL_EXPORT_C_INCLUDE_DIRS := \
@@ -91,7 +91,7 @@
     $(LOCAL_PATH)/include
 
 LOCAL_CFLAGS := \
-    -Wno-unused-parameter \
+    -Wall \
     -Werror
 
 LOCAL_STATIC_LIBRARIES := \
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index a0b9ad2..e93196b 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -34,11 +34,13 @@
 #include <fec/io.h>
 
 #include <functional>
+#include <limits>
 #include <memory>
 #include <string>
 #include <unordered_map>
 #include <vector>
 
+#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/strings.h>
@@ -50,11 +52,12 @@
 #include <ziparchive/zip_archive.h>
 
 #include "edify/expr.h"
-#include "error_code.h"
-#include "ota_io.h"
-#include "print_sha1.h"
+#include "otafault/ota_io.h"
+#include "otautil/cache_location.h"
+#include "otautil/error_code.h"
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
 #include "updater/install.h"
-#include "updater/rangeset.h"
 #include "updater/updater.h"
 
 // Set this to 0 to interpret 'erase' transfers to mean do a
@@ -63,7 +66,6 @@
 #define DEBUG_ERASE  0
 
 static constexpr size_t BLOCKSIZE = 4096;
-static constexpr const char* STASH_DIRECTORY_BASE = "/cache/recovery";
 static constexpr mode_t STASH_DIRECTORY_MODE = 0700;
 static constexpr mode_t STASH_FILE_MODE = 0600;
 
@@ -71,6 +73,93 @@
 static bool is_retry = false;
 static std::unordered_map<std::string, RangeSet> stash_map;
 
+static void DeleteLastCommandFile() {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+  if (unlink(last_command_file.c_str()) == -1 && errno != ENOENT) {
+    PLOG(ERROR) << "Failed to unlink: " << last_command_file;
+  }
+}
+
+// Parse the last command index of the last update and save the result to |last_command_index|.
+// Return true if we successfully read the index.
+static bool ParseLastCommandFile(int* last_command_index) {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+  android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(last_command_file.c_str(), O_RDONLY)));
+  if (fd == -1) {
+    if (errno != ENOENT) {
+      PLOG(ERROR) << "Failed to open " << last_command_file;
+      return false;
+    }
+
+    LOG(INFO) << last_command_file << " doesn't exist.";
+    return false;
+  }
+
+  // Now that the last_command file exists, parse the last command index of previous update.
+  std::string content;
+  if (!android::base::ReadFdToString(fd.get(), &content)) {
+    LOG(ERROR) << "Failed to read: " << last_command_file;
+    return false;
+  }
+
+  std::vector<std::string> lines = android::base::Split(android::base::Trim(content), "\n");
+  if (lines.size() != 2) {
+    LOG(ERROR) << "Unexpected line counts in last command file: " << content;
+    return false;
+  }
+
+  if (!android::base::ParseInt(lines[0], last_command_index)) {
+    LOG(ERROR) << "Failed to parse integer in: " << lines[0];
+    return false;
+  }
+
+  return true;
+}
+
+// Update the last command index in the last_command_file if the current command writes to the
+// stash either explicitly or implicitly.
+static bool UpdateLastCommandIndex(int command_index, const std::string& command_string) {
+  std::string last_command_file = CacheLocation::location().last_command_file();
+  std::string last_command_tmp = last_command_file + ".tmp";
+  std::string content = std::to_string(command_index) + "\n" + command_string;
+  android::base::unique_fd wfd(
+      TEMP_FAILURE_RETRY(open(last_command_tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0660)));
+  if (wfd == -1 || !android::base::WriteStringToFd(content, wfd)) {
+    PLOG(ERROR) << "Failed to update last command";
+    return false;
+  }
+
+  if (fsync(wfd) == -1) {
+    PLOG(ERROR) << "Failed to fsync " << last_command_tmp;
+    return false;
+  }
+
+  if (chown(last_command_tmp.c_str(), AID_SYSTEM, AID_SYSTEM) == -1) {
+    PLOG(ERROR) << "Failed to change owner for " << last_command_tmp;
+    return false;
+  }
+
+  if (rename(last_command_tmp.c_str(), last_command_file.c_str()) == -1) {
+    PLOG(ERROR) << "Failed to rename" << last_command_tmp;
+    return false;
+  }
+
+  std::string last_command_dir = android::base::Dirname(last_command_file);
+  android::base::unique_fd dfd(
+      TEMP_FAILURE_RETRY(ota_open(last_command_dir.c_str(), O_RDONLY | O_DIRECTORY)));
+  if (dfd == -1) {
+    PLOG(ERROR) << "Failed to open " << last_command_dir;
+    return false;
+  }
+
+  if (fsync(dfd) == -1) {
+    PLOG(ERROR) << "Failed to fsync " << last_command_dir;
+    return false;
+  }
+
+  return true;
+}
+
 static int read_all(int fd, uint8_t* data, size_t size) {
     size_t so_far = 0;
     while (so_far < size) {
@@ -281,6 +370,11 @@
     // Wait for nti->writer to be non-null, indicating some of this data is wanted.
     pthread_mutex_lock(&nti->mu);
     while (nti->writer == nullptr) {
+      // End the new data receiver if we encounter an error when performing block image update.
+      if (!nti->receiver_available) {
+        pthread_mutex_unlock(&nti->mu);
+        return false;
+      }
       pthread_cond_wait(&nti->cv, &nti->mu);
     }
     pthread_mutex_unlock(&nti->mu);
@@ -316,6 +410,11 @@
     // Wait for nti->writer to be non-null, indicating some of this data is wanted.
     pthread_mutex_lock(&nti->mu);
     while (nti->writer == nullptr) {
+      // End the receiver if we encounter an error when performing block image update.
+      if (!nti->receiver_available) {
+        pthread_mutex_unlock(&nti->mu);
+        return false;
+      }
       pthread_cond_wait(&nti->cv, &nti->mu);
     }
     pthread_mutex_unlock(&nti->mu);
@@ -429,6 +528,7 @@
 struct CommandParameters {
     std::vector<std::string> tokens;
     size_t cpos;
+    int cmdindex;
     const char* cmdname;
     const char* cmdline;
     std::string freestash;
@@ -445,6 +545,7 @@
     pthread_t thread;
     std::vector<uint8_t> buffer;
     uint8_t* patch_start;
+    bool target_verified;  // The target blocks have expected contents already.
 };
 
 // Print the hash in hex for corrupted source blocks (excluding the stashed blocks which is
@@ -482,6 +583,10 @@
   }
 
   RangeSet src = RangeSet::Parse(params.tokens[pos++]);
+  if (!src) {
+    LOG(ERROR) << "Failed to parse range in " << params.cmdline;
+    return;
+  }
 
   RangeSet locs;
   // If there's no stashed blocks, content in the buffer is consecutive and has the same
@@ -572,7 +677,7 @@
         return "";
     }
 
-    std::string fn(STASH_DIRECTORY_BASE);
+    std::string fn(CacheLocation::location().stash_directory_base());
     fn += "/" + base + "/" + id + postfix;
 
     return fn;
@@ -803,7 +908,7 @@
   size_t max_stash_size = maxblocks * BLOCKSIZE;
 
   if (res == -1 && errno != ENOENT) {
-    ErrorAbort(state, kStashCreationFailure, "stat \"%s\" failed: %s\n", dirname.c_str(),
+    ErrorAbort(state, kStashCreationFailure, "stat \"%s\" failed: %s", dirname.c_str(),
                strerror(errno));
     return -1;
   } else if (res != 0) {
@@ -811,19 +916,19 @@
     res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE);
 
     if (res != 0) {
-      ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s\n", dirname.c_str(),
+      ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s", dirname.c_str(),
                  strerror(errno));
       return -1;
     }
 
     if (chown(dirname.c_str(), AID_SYSTEM, AID_SYSTEM) != 0) {  // system user
-      ErrorAbort(state, kStashCreationFailure, "chown \"%s\" failed: %s\n", dirname.c_str(),
+      ErrorAbort(state, kStashCreationFailure, "chown \"%s\" failed: %s", dirname.c_str(),
                  strerror(errno));
       return -1;
     }
 
     if (CacheSizeCheck(max_stash_size) != 0) {
-      ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu needed)\n",
+      ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu needed)",
                  max_stash_size);
       return -1;
     }
@@ -855,7 +960,7 @@
   if (max_stash_size > existing) {
     size_t needed = max_stash_size - existing;
     if (CacheSizeCheck(needed) != 0) {
-      ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu more needed)\n",
+      ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu more needed)",
                  needed);
       return -1;
     }
@@ -926,6 +1031,7 @@
     params.cpos++;
   } else {
     RangeSet src = RangeSet::Parse(params.tokens[params.cpos++]);
+    CHECK(static_cast<bool>(src));
     *overlap = src.Overlaps(tgt);
 
     if (ReadBlocks(src, params.buffer, params.fd) == -1) {
@@ -938,6 +1044,7 @@
     }
 
     RangeSet locs = RangeSet::Parse(params.tokens[params.cpos++]);
+    CHECK(static_cast<bool>(locs));
     MoveRange(params.buffer, locs, params.buffer);
   }
 
@@ -960,6 +1067,7 @@
     }
 
     RangeSet locs = RangeSet::Parse(tokens[1]);
+    CHECK(static_cast<bool>(locs));
     MoveRange(params.buffer, locs, stash);
   }
 
@@ -1024,6 +1132,7 @@
 
   // <tgt_range>
   tgt = RangeSet::Parse(params.tokens[params.cpos++]);
+  CHECK(static_cast<bool>(tgt));
 
   std::vector<uint8_t> tgtbuffer(tgt.blocks() * BLOCKSIZE);
   if (ReadBlocks(tgt, tgtbuffer, params.fd) == -1) {
@@ -1054,6 +1163,10 @@
         return -1;
       }
 
+      if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) {
+        LOG(WARNING) << "Failed to update the last command file.";
+      }
+
       params.stashed += *src_blocks;
       // Can be deleted when the write has completed.
       if (!stash_exists) {
@@ -1094,8 +1207,11 @@
 
   if (status == 0) {
     params.foundwrites = true;
-  } else if (params.foundwrites) {
-    LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]";
+  } else {
+    params.target_verified = true;
+    if (params.foundwrites) {
+      LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]";
+    }
   }
 
   if (params.canwrite) {
@@ -1136,6 +1252,7 @@
   }
 
   RangeSet src = RangeSet::Parse(params.tokens[params.cpos++]);
+  CHECK(static_cast<bool>(src));
 
   allocate(src.blocks() * BLOCKSIZE, params.buffer);
   if (ReadBlocks(src, params.buffer, params.fd) == -1) {
@@ -1158,8 +1275,15 @@
   }
 
   LOG(INFO) << "stashing " << blocks << " blocks to " << id;
-  params.stashed += blocks;
-  return WriteStash(params.stashbase, id, blocks, params.buffer, false, nullptr);
+  int result = WriteStash(params.stashbase, id, blocks, params.buffer, false, nullptr);
+  if (result == 0) {
+    if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) {
+      LOG(WARNING) << "Failed to update the last command file.";
+    }
+
+    params.stashed += blocks;
+  }
+  return result;
 }
 
 static int PerformCommandFree(CommandParameters& params) {
@@ -1186,6 +1310,7 @@
   }
 
   RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]);
+  CHECK(static_cast<bool>(tgt));
 
   LOG(INFO) << "  zeroing " << tgt.blocks() << " blocks";
 
@@ -1228,6 +1353,7 @@
   }
 
   RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]);
+  CHECK(static_cast<bool>(tgt));
 
   if (params.canwrite) {
     LOG(INFO) << " writing " << tgt.blocks() << " blocks of new data";
@@ -1285,8 +1411,11 @@
 
   if (status == 0) {
     params.foundwrites = true;
-  } else if (params.foundwrites) {
-    LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]";
+  } else {
+    params.target_verified = true;
+    if (params.foundwrites) {
+      LOG(WARNING) << "warning: commands executed out of order [" << params.cmdname << "]";
+    }
   }
 
   if (params.canwrite) {
@@ -1297,7 +1426,7 @@
 
       RangeSinkWriter writer(params.fd, tgt);
       if (params.cmdname[0] == 'i') {  // imgdiff
-        if (ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, &patch_value,
+        if (ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value,
                             std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
                                       std::placeholders::_2),
                             nullptr, nullptr) != 0) {
@@ -1306,7 +1435,7 @@
           return -1;
         }
       } else {
-        if (ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, &patch_value, 0,
+        if (ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value, 0,
                              std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
                                        std::placeholders::_2),
                              nullptr) != 0) {
@@ -1358,6 +1487,7 @@
   }
 
   RangeSet tgt = RangeSet::Parse(params.tokens[params.cpos++]);
+  CHECK(static_cast<bool>(tgt));
 
   if (params.canwrite) {
     LOG(INFO) << " erasing " << tgt.blocks() << " blocks";
@@ -1495,7 +1625,7 @@
 
   std::vector<std::string> lines = android::base::Split(transfer_list_value->data, "\n");
   if (lines.size() < 2) {
-    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zd]\n",
+    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zd]",
                lines.size());
     return StringValue("");
   }
@@ -1511,7 +1641,7 @@
   // Second line in transfer list is the total number of blocks we expect to write.
   size_t total_blocks;
   if (!android::base::ParseUint(lines[1], &total_blocks)) {
-    ErrorAbort(state, kArgsParsingFailure, "unexpected block count [%s]\n", lines[1].c_str());
+    ErrorAbort(state, kArgsParsingFailure, "unexpected block count [%s]", lines[1].c_str());
     return StringValue("");
   }
 
@@ -1521,7 +1651,7 @@
 
   size_t start = 2;
   if (lines.size() < 4) {
-    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]\n",
+    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]",
                lines.size());
     return StringValue("");
   }
@@ -1532,7 +1662,7 @@
   // Fourth line is the maximum number of blocks that will be stashed simultaneously
   size_t stash_max_blocks;
   if (!android::base::ParseUint(lines[3], &stash_max_blocks)) {
-    ErrorAbort(state, kArgsParsingFailure, "unexpected maximum stash blocks [%s]\n",
+    ErrorAbort(state, kArgsParsingFailure, "unexpected maximum stash blocks [%s]",
                lines[3].c_str());
     return StringValue("");
   }
@@ -1544,6 +1674,23 @@
 
   params.createdstash = res;
 
+  // When performing an update, save the index and cmdline of the current command into
+  // the last_command_file if this command writes to the stash either explicitly of implicitly.
+  // Upon resuming an update, read the saved index first; then
+  //   1. In verification mode, check if the 'move' or 'diff' commands before the saved index has
+  //      the expected target blocks already. If not, these commands cannot be skipped and we need
+  //      to attempt to execute them again. Therefore, we will delete the last_command_file so that
+  //      the update will resume from the start of the transfer list.
+  //   2. In update mode, skip all commands before the saved index. Therefore, we can avoid deleting
+  //      stashes with duplicate id unintentionally (b/69858743); and also speed up the update.
+  // If an update succeeds or is unresumable, delete the last_command_file.
+  int saved_last_command_index;
+  if (!ParseLastCommandFile(&saved_last_command_index)) {
+    DeleteLastCommandFile();
+    // We failed to parse the last command, set it explicitly to -1.
+    saved_last_command_index = -1;
+  }
+
   start += 2;
 
   // Build a map of the available commands
@@ -1559,14 +1706,20 @@
   int rc = -1;
 
   // Subsequent lines are all individual transfer commands
-  for (auto it = lines.cbegin() + start; it != lines.cend(); it++) {
-    const std::string& line(*it);
+  for (size_t i = start; i < lines.size(); i++) {
+    const std::string& line = lines[i];
     if (line.empty()) continue;
 
     params.tokens = android::base::Split(line, " ");
     params.cpos = 0;
+    if (i - start > std::numeric_limits<int>::max()) {
+      params.cmdindex = -1;
+    } else {
+      params.cmdindex = i - start;
+    }
     params.cmdname = params.tokens[params.cpos++].c_str();
     params.cmdline = line.c_str();
+    params.target_verified = false;
 
     if (cmd_map.find(params.cmdname) == cmd_map.end()) {
       LOG(ERROR) << "unexpected command [" << params.cmdname << "]";
@@ -1575,11 +1728,40 @@
 
     const Command* cmd = cmd_map[params.cmdname];
 
-    if (cmd->f != nullptr && cmd->f(params) == -1) {
+    // Skip the command if we explicitly set the corresponding function pointer to nullptr, e.g.
+    // "erase" during block_image_verify.
+    if (cmd->f == nullptr) {
+      LOG(DEBUG) << "skip executing command [" << line << "]";
+      continue;
+    }
+
+    // Skip all commands before the saved last command index when resuming an update.
+    if (params.canwrite && params.cmdindex != -1 && params.cmdindex <= saved_last_command_index) {
+      LOG(INFO) << "Skipping already executed command: " << params.cmdindex
+                << ", last executed command for previous update: " << saved_last_command_index;
+      continue;
+    }
+
+    if (cmd->f(params) == -1) {
       LOG(ERROR) << "failed to execute command [" << line << "]";
       goto pbiudone;
     }
 
+    // In verify mode, check if the commands before the saved last_command_index have been
+    // executed correctly. If some target blocks have unexpected contents, delete the last command
+    // file so that we will resume the update from the first command in the transfer list.
+    if (!params.canwrite && saved_last_command_index != -1 && params.cmdindex != -1 &&
+        params.cmdindex <= saved_last_command_index) {
+      // TODO(xunchang) check that the cmdline of the saved index is correct.
+      std::string cmdname = std::string(params.cmdname);
+      if ((cmdname == "move" || cmdname == "bsdiff" || cmdname == "imgdiff") &&
+          !params.target_verified) {
+        LOG(WARNING) << "Previously executed command " << saved_last_command_index << ": "
+                     << params.cmdline << " doesn't produce expected target blocks.";
+        saved_last_command_index = -1;
+        DeleteLastCommandFile();
+      }
+    }
     if (params.canwrite) {
       if (ota_fsync(params.fd) == -1) {
         failure_type = kFsyncFailure;
@@ -1591,29 +1773,45 @@
     }
   }
 
-  if (params.canwrite) {
-    pthread_join(params.thread, nullptr);
-
-    LOG(INFO) << "wrote " << params.written << " blocks; expected " << total_blocks;
-    LOG(INFO) << "stashed " << params.stashed << " blocks";
-    LOG(INFO) << "max alloc needed was " << params.buffer.size();
-
-    const char* partition = strrchr(blockdev_filename->data.c_str(), '/');
-    if (partition != nullptr && *(partition + 1) != 0) {
-      fprintf(cmd_pipe, "log bytes_written_%s: %zu\n", partition + 1, params.written * BLOCKSIZE);
-      fprintf(cmd_pipe, "log bytes_stashed_%s: %zu\n", partition + 1, params.stashed * BLOCKSIZE);
-      fflush(cmd_pipe);
-    }
-    // Delete stash only after successfully completing the update, as it may contain blocks needed
-    // to complete the update later.
-    DeleteStash(params.stashbase);
-  } else {
-    LOG(INFO) << "verified partition contents; update may be resumed";
-  }
-
   rc = 0;
 
 pbiudone:
+  if (params.canwrite) {
+    pthread_mutex_lock(&params.nti.mu);
+    if (params.nti.receiver_available) {
+      LOG(WARNING) << "new data receiver is still available after executing all commands.";
+    }
+    params.nti.receiver_available = false;
+    pthread_cond_broadcast(&params.nti.cv);
+    pthread_mutex_unlock(&params.nti.mu);
+    int ret = pthread_join(params.thread, nullptr);
+    if (ret != 0) {
+      LOG(WARNING) << "pthread join returned with " << strerror(ret);
+    }
+
+    if (rc == 0) {
+      LOG(INFO) << "wrote " << params.written << " blocks; expected " << total_blocks;
+      LOG(INFO) << "stashed " << params.stashed << " blocks";
+      LOG(INFO) << "max alloc needed was " << params.buffer.size();
+
+      const char* partition = strrchr(blockdev_filename->data.c_str(), '/');
+      if (partition != nullptr && *(partition + 1) != 0) {
+        fprintf(cmd_pipe, "log bytes_written_%s: %zu\n", partition + 1, params.written * BLOCKSIZE);
+        fprintf(cmd_pipe, "log bytes_stashed_%s: %zu\n", partition + 1, params.stashed * BLOCKSIZE);
+        fflush(cmd_pipe);
+      }
+      // Delete stash only after successfully completing the update, as it may contain blocks needed
+      // to complete the update later.
+      DeleteStash(params.stashbase);
+      DeleteLastCommandFile();
+    }
+
+    pthread_mutex_destroy(&params.nti.mu);
+    pthread_cond_destroy(&params.nti.cv);
+  } else if (rc == 0) {
+    LOG(INFO) << "verified partition contents; update may be resumed";
+  }
+
   if (ota_fsync(params.fd) == -1) {
     failure_type = kFsyncFailure;
     PLOG(ERROR) << "fsync failed";
@@ -1624,6 +1822,11 @@
     BrotliDecoderDestroyInstance(params.nti.brotli_decoder_state);
   }
 
+  // Delete the last command file if the update cannot be resumed.
+  if (params.isunresumable) {
+    DeleteLastCommandFile();
+  }
+
   // 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)) {
@@ -1748,6 +1951,7 @@
   }
 
   RangeSet rs = RangeSet::Parse(ranges->data);
+  CHECK(static_cast<bool>(rs));
 
   SHA_CTX ctx;
   SHA1_Init(&ctx);
@@ -1859,6 +2063,11 @@
     ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name);
     return StringValue("");
   }
+  RangeSet rs = RangeSet::Parse(ranges->data);
+  if (!rs) {
+    ErrorAbort(state, kArgsParsingFailure, "failed to parse ranges: %s", ranges->data.c_str());
+    return StringValue("");
+  }
 
   // Output notice to log when recover is attempted
   LOG(INFO) << filename->data << " image corrupted, attempting to recover...";
@@ -1884,7 +2093,7 @@
   }
 
   uint8_t buffer[BLOCKSIZE];
-  for (const auto& range : RangeSet::Parse(ranges->data)) {
+  for (const auto& range : rs) {
     for (size_t j = range.first; j < range.second; ++j) {
       // Stay within the data area, libfec validates and corrects metadata
       if (status.data_size <= static_cast<uint64_t>(j) * BLOCKSIZE) {
diff --git a/updater/include/updater/blockimg.h b/updater/include/updater/blockimg.h
index 2f4ad3c..71733b3 100644
--- a/updater/include/updater/blockimg.h
+++ b/updater/include/updater/blockimg.h
@@ -17,6 +17,8 @@
 #ifndef _UPDATER_BLOCKIMG_H_
 #define _UPDATER_BLOCKIMG_H_
 
+#include <string>
+
 void RegisterBlockImageFunctions();
 
 #endif
diff --git a/updater/include/updater/rangeset.h b/updater/include/updater/rangeset.h
deleted file mode 100644
index fad0380..0000000
--- a/updater/include/updater/rangeset.h
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2017 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.
- */
-
-#pragma once
-
-#include <stddef.h>
-
-#include <string>
-#include <utility>
-#include <vector>
-
-#include <android-base/logging.h>
-#include <android-base/parseint.h>
-#include <android-base/strings.h>
-
-using Range = std::pair<size_t, size_t>;
-
-class RangeSet {
- public:
-  RangeSet() : blocks_(0) {}
-
-  explicit RangeSet(std::vector<Range>&& pairs) {
-    CHECK_NE(pairs.size(), static_cast<size_t>(0)) << "Invalid number of tokens";
-
-    // Sanity check the input.
-    size_t result = 0;
-    for (const auto& range : pairs) {
-      CHECK_LT(range.first, range.second)
-          << "Empty or negative range: " << range.first << ", " << range.second;
-      size_t sz = range.second - range.first;
-      CHECK_LE(result, SIZE_MAX - sz) << "RangeSet size overflow";
-      result += sz;
-    }
-
-    ranges_ = pairs;
-    blocks_ = result;
-  }
-
-  static RangeSet Parse(const std::string& range_text) {
-    std::vector<std::string> pieces = android::base::Split(range_text, ",");
-    CHECK_GE(pieces.size(), static_cast<size_t>(3)) << "Invalid range text: " << range_text;
-
-    size_t num;
-    CHECK(android::base::ParseUint(pieces[0], &num, static_cast<size_t>(INT_MAX)))
-        << "Failed to parse the number of tokens: " << range_text;
-
-    CHECK_NE(num, static_cast<size_t>(0)) << "Invalid number of tokens: " << range_text;
-    CHECK_EQ(num % 2, static_cast<size_t>(0)) << "Number of tokens must be even: " << range_text;
-    CHECK_EQ(num, pieces.size() - 1) << "Mismatching number of tokens: " << range_text;
-
-    std::vector<Range> pairs;
-    for (size_t i = 0; i < num; i += 2) {
-      size_t first;
-      CHECK(android::base::ParseUint(pieces[i + 1], &first, static_cast<size_t>(INT_MAX)));
-      size_t second;
-      CHECK(android::base::ParseUint(pieces[i + 2], &second, static_cast<size_t>(INT_MAX)));
-
-      pairs.emplace_back(first, second);
-    }
-
-    return RangeSet(std::move(pairs));
-  }
-
-  // Get the block number for the i-th (starting from 0) block in the RangeSet.
-  size_t GetBlockNumber(size_t idx) const {
-    CHECK_LT(idx, blocks_) << "Out of bound index " << idx << " (total blocks: " << blocks_ << ")";
-
-    for (const auto& range : ranges_) {
-      if (idx < range.second - range.first) {
-        return range.first + idx;
-      }
-      idx -= (range.second - range.first);
-    }
-
-    CHECK(false) << "Failed to find block number for index " << idx;
-    return 0;  // Unreachable, but to make compiler happy.
-  }
-
-  // RangeSet has half-closed half-open bounds. For example, "3,5" contains blocks 3 and 4. So "3,5"
-  // and "5,7" are not overlapped.
-  bool Overlaps(const RangeSet& other) const {
-    for (const auto& range : ranges_) {
-      size_t start = range.first;
-      size_t end = range.second;
-      for (const auto& other_range : other.ranges_) {
-        size_t other_start = other_range.first;
-        size_t other_end = other_range.second;
-        // [start, end) vs [other_start, other_end)
-        if (!(other_start >= end || start >= other_end)) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  // size() gives the number of Range's in this RangeSet.
-  size_t size() const {
-    return ranges_.size();
-  }
-
-  // blocks() gives the number of all blocks in this RangeSet.
-  size_t blocks() const {
-    return blocks_;
-  }
-
-  // We provide const iterators only.
-  std::vector<Range>::const_iterator cbegin() const {
-    return ranges_.cbegin();
-  }
-
-  std::vector<Range>::const_iterator cend() const {
-    return ranges_.cend();
-  }
-
-  // Need to provide begin()/end() since range-based loop expects begin()/end().
-  std::vector<Range>::const_iterator begin() const {
-    return ranges_.cbegin();
-  }
-
-  std::vector<Range>::const_iterator end() const {
-    return ranges_.cend();
-  }
-
-  // Reverse const iterators for MoveRange().
-  std::vector<Range>::const_reverse_iterator crbegin() const {
-    return ranges_.crbegin();
-  }
-
-  std::vector<Range>::const_reverse_iterator crend() const {
-    return ranges_.crend();
-  }
-
-  const Range& operator[](size_t i) const {
-    return ranges_[i];
-  }
-
-  bool operator==(const RangeSet& other) const {
-    // The orders of Range's matter. "4,1,5,8,10" != "4,8,10,1,5".
-    return (ranges_ == other.ranges_);
-  }
-
-  bool operator!=(const RangeSet& other) const {
-    return ranges_ != other.ranges_;
-  }
-
- private:
-  // Actual limit for each value and the total number are both INT_MAX.
-  std::vector<Range> ranges_;
-  size_t blocks_;
-};
diff --git a/updater/install.cpp b/updater/install.cpp
index bfe91e7..9be7645 100644
--- a/updater/install.cpp
+++ b/updater/install.cpp
@@ -49,20 +49,19 @@
 #include <applypatch/applypatch.h>
 #include <bootloader_message/bootloader_message.h>
 #include <cutils/android_reboot.h>
-#include <ext4_utils/make_ext4fs.h>
 #include <ext4_utils/wipe.h>
 #include <openssl/sha.h>
 #include <selinux/label.h>
 #include <selinux/selinux.h>
+#include <tune2fs.h>
 #include <ziparchive/zip_archive.h>
 
 #include "edify/expr.h"
-#include "error_code.h"
 #include "mounts.h"
-#include "ota_io.h"
+#include "otafault/ota_io.h"
 #include "otautil/DirUtil.h"
-#include "print_sha1.h"
-#include "tune2fs.h"
+#include "otautil/error_code.h"
+#include "otautil/print_sha1.h"
 #include "updater/updater.h"
 
 // Send over the buffer to recovery though the command pipe.
@@ -95,32 +94,242 @@
   uiPrint(state, error_msg);
 }
 
-static bool is_dir(const std::string& dirpath) {
-  struct stat st;
-  return stat(dirpath.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
+// 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, const std::vector<std::unique_ptr<Expr>>& argv) {
+  std::vector<std::string> args;
+  if (!ReadArgs(state, argv, &args)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
+  }
+
+  std::string buffer = android::base::Join(args, "");
+  uiPrint(state, buffer);
+  return StringValue(buffer);
 }
 
-// Create all parent directories of name, if necessary.
-static bool make_parents(const std::string& name) {
-  size_t prev_end = 0;
-  while (prev_end < name.size()) {
-    size_t next_end = name.find('/', prev_end + 1);
-    if (next_end == std::string::npos) {
-      break;
-    }
-    std::string dir_path = name.substr(0, next_end);
-    if (!is_dir(dir_path)) {
-      int result = mkdir(dir_path.c_str(), 0700);
-      if (result != 0) {
-        PLOG(ERROR) << "failed to mkdir " << dir_path << " when make parents for " << name;
-        return false;
-      }
-
-      LOG(INFO) << "created [" << dir_path << "]";
-    }
-    prev_end = next_end;
+// package_extract_file(package_file[, dest_file])
+//   Extracts a single package_file from the update package and writes it to dest_file,
+//   overwriting existing files if necessary. Without the dest_file argument, returns the
+//   contents of the package file as a binary blob.
+Value* PackageExtractFileFn(const char* name, State* state,
+                            const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() < 1 || argv.size() > 2) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 or 2 args, got %zu", name,
+                      argv.size());
   }
-  return true;
+
+  if (argv.size() == 2) {
+    // The two-argument version extracts to a file.
+
+    std::vector<std::string> args;
+    if (!ReadArgs(state, argv, &args)) {
+      return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name,
+                        argv.size());
+    }
+    const std::string& zip_path = args[0];
+    const std::string& dest_path = args[1];
+
+    ZipArchiveHandle za = static_cast<UpdaterInfo*>(state->cookie)->package_zip;
+    ZipString zip_string_path(zip_path.c_str());
+    ZipEntry entry;
+    if (FindEntry(za, zip_string_path, &entry) != 0) {
+      LOG(ERROR) << name << ": no " << zip_path << " in package";
+      return StringValue("");
+    }
+
+    unique_fd fd(TEMP_FAILURE_RETRY(
+        ota_open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)));
+    if (fd == -1) {
+      PLOG(ERROR) << name << ": can't open " << dest_path << " for write";
+      return StringValue("");
+    }
+
+    bool success = true;
+    int32_t ret = ExtractEntryToFile(za, &entry, fd);
+    if (ret != 0) {
+      LOG(ERROR) << name << ": Failed to extract entry \"" << zip_path << "\" ("
+                 << entry.uncompressed_length << " bytes) to \"" << dest_path
+                 << "\": " << ErrorCodeString(ret);
+      success = false;
+    }
+    if (ota_fsync(fd) == -1) {
+      PLOG(ERROR) << "fsync of \"" << dest_path << "\" failed";
+      success = false;
+    }
+    if (ota_close(fd) == -1) {
+      PLOG(ERROR) << "close of \"" << dest_path << "\" failed";
+      success = false;
+    }
+
+    return StringValue(success ? "t" : "");
+  } else {
+    // The one-argument version returns the contents of the file as the result.
+
+    std::vector<std::string> args;
+    if (!ReadArgs(state, argv, &args)) {
+      return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name,
+                        argv.size());
+    }
+    const std::string& zip_path = args[0];
+
+    ZipArchiveHandle za = static_cast<UpdaterInfo*>(state->cookie)->package_zip;
+    ZipString zip_string_path(zip_path.c_str());
+    ZipEntry entry;
+    if (FindEntry(za, zip_string_path, &entry) != 0) {
+      return ErrorAbort(state, kPackageExtractFileFailure, "%s(): no %s in package", name,
+                        zip_path.c_str());
+    }
+
+    std::string buffer;
+    buffer.resize(entry.uncompressed_length);
+
+    int32_t ret =
+        ExtractToMemory(za, &entry, reinterpret_cast<uint8_t*>(&buffer[0]), buffer.size());
+    if (ret != 0) {
+      return ErrorAbort(state, kPackageExtractFileFailure,
+                        "%s: Failed to extract entry \"%s\" (%zu bytes) to memory: %s", name,
+                        zip_path.c_str(), buffer.size(), ErrorCodeString(ret));
+    }
+
+    return new Value(VAL_BLOB, buffer);
+  }
+}
+
+// apply_patch(src_file, tgt_file, tgt_sha1, tgt_size, patch1_sha1, patch1_blob, [...])
+//   Applies a binary patch to the src_file to produce the tgt_file. If the desired target is the
+//   same as the source, pass "-" for tgt_file. tgt_sha1 and tgt_size are the expected final SHA1
+//   hash and size of the target file. The remaining arguments must come in pairs: a SHA1 hash (a
+//   40-character hex string) and a blob. The blob is the patch to be applied when the source
+//   file's current contents have the given SHA1.
+//
+//   The patching is done in a safe manner that guarantees the target file either has the desired
+//   SHA1 hash and size, or it is untouched -- it will not be left in an unrecoverable intermediate
+//   state. If the process is interrupted during patching, the target file may be in an intermediate
+//   state; a copy exists in the cache partition so restarting the update can successfully update
+//   the file.
+Value* ApplyPatchFn(const char* name, State* state,
+                    const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() < 6 || (argv.size() % 2) == 1) {
+    return ErrorAbort(state, kArgsParsingFailure,
+                      "%s(): expected at least 6 args and an "
+                      "even number, got %zu",
+                      name, argv.size());
+  }
+
+  std::vector<std::string> args;
+  if (!ReadArgs(state, argv, &args, 0, 4)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
+  }
+  const std::string& source_filename = args[0];
+  const std::string& target_filename = args[1];
+  const std::string& target_sha1 = args[2];
+  const std::string& target_size_str = args[3];
+
+  size_t target_size;
+  if (!android::base::ParseUint(target_size_str.c_str(), &target_size)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count", name,
+                      target_size_str.c_str());
+  }
+
+  int patchcount = (argv.size() - 4) / 2;
+  std::vector<std::unique_ptr<Value>> arg_values;
+  if (!ReadValueArgs(state, argv, &arg_values, 4, argv.size() - 4)) {
+    return nullptr;
+  }
+
+  for (int i = 0; i < patchcount; ++i) {
+    if (arg_values[i * 2]->type != VAL_STRING) {
+      return ErrorAbort(state, kArgsParsingFailure, "%s(): sha-1 #%d is not string", name, i * 2);
+    }
+    if (arg_values[i * 2 + 1]->type != VAL_BLOB) {
+      return ErrorAbort(state, kArgsParsingFailure, "%s(): patch #%d is not blob", name, i * 2 + 1);
+    }
+  }
+
+  std::vector<std::string> patch_sha_str;
+  std::vector<std::unique_ptr<Value>> patches;
+  for (int i = 0; i < patchcount; ++i) {
+    patch_sha_str.push_back(arg_values[i * 2]->data);
+    patches.push_back(std::move(arg_values[i * 2 + 1]));
+  }
+
+  int result = applypatch(source_filename.c_str(), target_filename.c_str(), target_sha1.c_str(),
+                          target_size, patch_sha_str, patches, nullptr);
+
+  return StringValue(result == 0 ? "t" : "");
+}
+
+// apply_patch_check(filename, [sha1, ...])
+//   Returns true if the contents of filename or the temporary copy in the cache partition (if
+//   present) have a SHA-1 checksum equal to one of the given sha1 values. sha1 values are
+//   specified as 40 hex digits. This function differs from sha1_check(read_file(filename),
+//   sha1 [, ...]) in that it knows to check the cache partition copy, so apply_patch_check() will
+//   succeed even if the file was corrupted by an interrupted apply_patch() update.
+Value* ApplyPatchCheckFn(const char* name, State* state,
+                         const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() < 1) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): expected at least 1 arg, got %zu", name,
+                      argv.size());
+  }
+
+  std::vector<std::string> args;
+  if (!ReadArgs(state, argv, &args, 0, 1)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
+  }
+  const std::string& filename = args[0];
+
+  std::vector<std::string> sha1s;
+  if (argv.size() > 1 && !ReadArgs(state, argv, &sha1s, 1, argv.size() - 1)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
+  }
+  int result = applypatch_check(filename.c_str(), sha1s);
+
+  return StringValue(result == 0 ? "t" : "");
+}
+
+// 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, const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() < 1) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() expects at least 1 arg", name);
+  }
+
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return nullptr;
+  }
+
+  if (args[0]->type == VAL_INVALID) {
+    return StringValue("");
+  }
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(reinterpret_cast<const uint8_t*>(args[0]->data.c_str()), args[0]->data.size(), digest);
+
+  if (argv.size() == 1) {
+    return StringValue(print_sha1(digest));
+  }
+
+  for (size_t i = 1; i < argv.size(); ++i) {
+    uint8_t arg_digest[SHA_DIGEST_LENGTH];
+    if (args[i]->type != VAL_STRING) {
+      LOG(ERROR) << name << "(): arg " << i << " is not a string; skipping";
+    } else if (ParseSha1(args[i]->data.c_str(), arg_digest) != 0) {
+      // Warn about bad args and skip them.
+      LOG(ERROR) << name << "(): error parsing \"" << args[i]->data << "\" as sha-1; skipping";
+    } else if (memcmp(digest, arg_digest, SHA_DIGEST_LENGTH) == 0) {
+      // Found a match.
+      return args[i].release();
+    }
+  }
+
+  // Didn't match any of the hex strings; return false.
+  return StringValue("");
 }
 
 // mount(fs_type, partition_type, location, mount_point)
@@ -297,7 +506,7 @@
 
   int64_t size;
   if (!android::base::ParseInt(fs_size, &size)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s\n", name,
+    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s", name,
                       fs_size.c_str());
   }
 
@@ -312,14 +521,8 @@
 
     int status = exec_cmd(mke2fs_argv[0], const_cast<char**>(mke2fs_argv));
     if (status != 0) {
-      LOG(WARNING) << name << ": mke2fs failed (" << status << ") on " << location
-                   << ", falling back to make_ext4fs";
-      status = make_ext4fs(location.c_str(), size, mount_point.c_str(), sehandle);
-      if (status != 0) {
-        LOG(ERROR) << name << ": make_ext4fs failed (" << status << ") on " << location;
-        return StringValue("");
-      }
-      return StringValue(location);
+      LOG(ERROR) << name << ": mke2fs failed (" << status << ") on " << location;
+      return StringValue("");
     }
 
     const char* e2fsdroid_argv[] = { "/sbin/e2fsdroid_static", "-e",   "-a", mount_point.c_str(),
@@ -338,15 +541,30 @@
     std::string num_sectors = std::to_string(size / 512);
 
     const char* f2fs_path = "/sbin/mkfs.f2fs";
-    const char* f2fs_argv[] = {
-      "mkfs.f2fs", "-t", "-d1", location.c_str(), (size < 512) ? nullptr : num_sectors.c_str(),
-      nullptr
-    };
+    const char* f2fs_argv[] = { "mkfs.f2fs",
+                                "-d1",
+                                "-f",
+                                "-O", "encrypt",
+                                "-O", "quota",
+                                "-O", "verity",
+                                "-w", "512",
+                                location.c_str(),
+                                (size < 512) ? nullptr : num_sectors.c_str(),
+                                nullptr };
     int status = exec_cmd(f2fs_path, const_cast<char**>(f2fs_argv));
     if (status != 0) {
       LOG(ERROR) << name << ": mkfs.f2fs failed (" << status << ") on " << location;
       return StringValue("");
     }
+
+    const char* sload_argv[] = { "/sbin/sload.f2fs", "-t", mount_point.c_str(), location.c_str(),
+                                 nullptr };
+    status = exec_cmd(sload_argv[0], const_cast<char**>(sload_argv));
+    if (status != 0) {
+      LOG(ERROR) << name << ": sload.f2fs failed (" << status << ") on " << location;
+      return StringValue("");
+    }
+
     return StringValue(location);
   } else {
     LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \""
@@ -372,12 +590,12 @@
 
   double frac;
   if (!android::base::ParseDouble(frac_str.c_str(), &frac)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s\n", name,
+    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s", name,
                       frac_str.c_str());
   }
   int sec;
   if (!android::base::ParseInt(sec_str.c_str(), &sec)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s\n", name,
+    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse int in %s", name,
                       sec_str.c_str());
   }
 
@@ -387,7 +605,8 @@
   return StringValue(frac_str);
 }
 
-Value* SetProgressFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
+Value* SetProgressFn(const char* name, State* state,
+                     const std::vector<std::unique_ptr<Expr>>& argv) {
   if (argv.size() != 1) {
     return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size());
   }
@@ -400,7 +619,7 @@
 
   double frac;
   if (!android::base::ParseDouble(frac_str.c_str(), &frac)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s\n", name,
+    return ErrorAbort(state, kArgsParsingFailure, "%s: failed to parse double in %s", name,
                       frac_str.c_str());
   }
 
@@ -410,93 +629,6 @@
   return StringValue(frac_str);
 }
 
-// package_extract_file(package_file[, dest_file])
-//   Extracts a single package_file from the update package and writes it to dest_file,
-//   overwriting existing files if necessary. Without the dest_file argument, returns the
-//   contents of the package file as a binary blob.
-Value* PackageExtractFileFn(const char* name, State* state,
-                            const std::vector<std::unique_ptr<Expr>>& argv) {
-  if (argv.size() < 1 || argv.size() > 2) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 or 2 args, got %zu", name,
-                      argv.size());
-  }
-
-  if (argv.size() == 2) {
-    // The two-argument version extracts to a file.
-
-    std::vector<std::string> args;
-    if (!ReadArgs(state, argv, &args)) {
-      return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name,
-                        argv.size());
-    }
-    const std::string& zip_path = args[0];
-    const std::string& dest_path = args[1];
-
-    ZipArchiveHandle za = static_cast<UpdaterInfo*>(state->cookie)->package_zip;
-    ZipString zip_string_path(zip_path.c_str());
-    ZipEntry entry;
-    if (FindEntry(za, zip_string_path, &entry) != 0) {
-      LOG(ERROR) << name << ": no " << zip_path << " in package";
-      return StringValue("");
-    }
-
-    unique_fd fd(TEMP_FAILURE_RETRY(
-        ota_open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)));
-    if (fd == -1) {
-      PLOG(ERROR) << name << ": can't open " << dest_path << " for write";
-      return StringValue("");
-    }
-
-    bool success = true;
-    int32_t ret = ExtractEntryToFile(za, &entry, fd);
-    if (ret != 0) {
-      LOG(ERROR) << name << ": Failed to extract entry \"" << zip_path << "\" ("
-                 << entry.uncompressed_length << " bytes) to \"" << dest_path
-                 << "\": " << ErrorCodeString(ret);
-      success = false;
-    }
-    if (ota_fsync(fd) == -1) {
-      PLOG(ERROR) << "fsync of \"" << dest_path << "\" failed";
-      success = false;
-    }
-    if (ota_close(fd) == -1) {
-      PLOG(ERROR) << "close of \"" << dest_path << "\" failed";
-      success = false;
-    }
-
-    return StringValue(success ? "t" : "");
-  } else {
-    // The one-argument version returns the contents of the file as the result.
-
-    std::vector<std::string> args;
-    if (!ReadArgs(state, argv, &args)) {
-      return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse %zu args", name,
-                        argv.size());
-    }
-    const std::string& zip_path = args[0];
-
-    ZipArchiveHandle za = static_cast<UpdaterInfo*>(state->cookie)->package_zip;
-    ZipString zip_string_path(zip_path.c_str());
-    ZipEntry entry;
-    if (FindEntry(za, zip_string_path, &entry) != 0) {
-      return ErrorAbort(state, kPackageExtractFileFailure, "%s(): no %s in package", name,
-                        zip_path.c_str());
-    }
-
-    std::string buffer;
-    buffer.resize(entry.uncompressed_length);
-
-    int32_t ret = ExtractToMemory(za, &entry, reinterpret_cast<uint8_t*>(&buffer[0]), buffer.size());
-    if (ret != 0) {
-      return ErrorAbort(state, kPackageExtractFileFailure,
-                        "%s: Failed to extract entry \"%s\" (%zu bytes) to memory: %s", name,
-                        zip_path.c_str(), buffer.size(), ErrorCodeString(ret));
-    }
-
-    return new Value(VAL_BLOB, buffer);
-  }
-}
-
 Value* GetPropFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
   if (argv.size() != 1) {
     return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size());
@@ -515,7 +647,8 @@
 //   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, const std::vector<std::unique_ptr<Expr>>& argv) {
+Value* FileGetPropFn(const char* name, State* state,
+                     const std::vector<std::unique_ptr<Expr>>& argv) {
   if (argv.size() != 2) {
     return ErrorAbort(state, kArgsParsingFailure, "%s() expects 2 args, got %zu", name,
                       argv.size());
@@ -581,7 +714,8 @@
 }
 
 // apply_patch_space(bytes)
-Value* ApplyPatchSpaceFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
+Value* ApplyPatchSpaceFn(const char* name, State* state,
+                         const std::vector<std::unique_ptr<Expr>>& argv) {
   if (argv.size() != 1) {
     return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 args, got %zu", name,
                       argv.size());
@@ -594,115 +728,15 @@
 
   size_t bytes;
   if (!android::base::ParseUint(bytes_str.c_str(), &bytes)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count\n\n",
-                      name, bytes_str.c_str());
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count", name,
+                      bytes_str.c_str());
   }
 
-  return StringValue(CacheSizeCheck(bytes) ? "" : "t");
-}
-
-// apply_patch(src_file, tgt_file, tgt_sha1, tgt_size, patch1_sha1, patch1_blob, [...])
-//   Applies a binary patch to the src_file to produce the tgt_file. If the desired target is the
-//   same as the source, pass "-" for tgt_file. tgt_sha1 and tgt_size are the expected final SHA1
-//   hash and size of the target file. The remaining arguments must come in pairs: a SHA1 hash (a
-//   40-character hex string) and a blob. The blob is the patch to be applied when the source
-//   file's current contents have the given SHA1.
-//
-//   The patching is done in a safe manner that guarantees the target file either has the desired
-//   SHA1 hash and size, or it is untouched -- it will not be left in an unrecoverable intermediate
-//   state. If the process is interrupted during patching, the target file may be in an intermediate
-//   state; a copy exists in the cache partition so restarting the update can successfully update
-//   the file.
-Value* ApplyPatchFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
-    if (argv.size() < 6 || (argv.size() % 2) == 1) {
-        return ErrorAbort(state, kArgsParsingFailure, "%s(): expected at least 6 args and an "
-                          "even number, got %zu", name, argv.size());
-    }
-
-    std::vector<std::string> args;
-    if (!ReadArgs(state, argv, &args, 0, 4)) {
-        return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
-    }
-    const std::string& source_filename = args[0];
-    const std::string& target_filename = args[1];
-    const std::string& target_sha1 = args[2];
-    const std::string& target_size_str = args[3];
-
-    size_t target_size;
-    if (!android::base::ParseUint(target_size_str.c_str(), &target_size)) {
-        return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count",
-                          name, target_size_str.c_str());
-    }
-
-    int patchcount = (argv.size()-4) / 2;
-    std::vector<std::unique_ptr<Value>> arg_values;
-    if (!ReadValueArgs(state, argv, &arg_values, 4, argv.size() - 4)) {
-        return nullptr;
-    }
-
-    for (int i = 0; i < patchcount; ++i) {
-        if (arg_values[i * 2]->type != VAL_STRING) {
-            return ErrorAbort(state, kArgsParsingFailure, "%s(): sha-1 #%d is not string", name,
-                              i * 2);
-        }
-        if (arg_values[i * 2 + 1]->type != VAL_BLOB) {
-            return ErrorAbort(state, kArgsParsingFailure, "%s(): patch #%d is not blob", name,
-                              i * 2 + 1);
-        }
-    }
-
-    std::vector<std::string> patch_sha_str;
-    std::vector<std::unique_ptr<Value>> patches;
-    for (int i = 0; i < patchcount; ++i) {
-        patch_sha_str.push_back(arg_values[i * 2]->data);
-        patches.push_back(std::move(arg_values[i * 2 + 1]));
-    }
-
-    int result = applypatch(source_filename.c_str(), target_filename.c_str(),
-                            target_sha1.c_str(), target_size,
-                            patch_sha_str, patches, nullptr);
-
-    return StringValue(result == 0 ? "t" : "");
-}
-
-// apply_patch_check(filename, [sha1, ...])
-//   Returns true if the contents of filename or the temporary copy in the cache partition (if
-//   present) have a SHA-1 checksum equal to one of the given sha1 values. sha1 values are
-//   specified as 40 hex digits. This function differs from sha1_check(read_file(filename),
-//   sha1 [, ...]) in that it knows to check the cache partition copy, so apply_patch_check() will
-//   succeed even if the file was corrupted by an interrupted apply_patch() update.
-Value* ApplyPatchCheckFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
-  if (argv.size() < 1) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s(): expected at least 1 arg, got %zu", name,
-                      argv.size());
+  // Skip the cache size check if the update is a retry.
+  if (state->is_retry || CacheSizeCheck(bytes) == 0) {
+    return StringValue("t");
   }
-
-  std::vector<std::string> args;
-  if (!ReadArgs(state, argv, &args, 0, 1)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
-  }
-  const std::string& filename = args[0];
-
-  std::vector<std::string> sha1s;
-  if (argv.size() > 1 && !ReadArgs(state, argv, &sha1s, 1, argv.size() - 1)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
-  }
-  int result = applypatch_check(filename.c_str(), sha1s);
-
-  return StringValue(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, const std::vector<std::unique_ptr<Expr>>& argv) {
-  std::vector<std::string> args;
-  if (!ReadArgs(state, argv, &args)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
-  }
-
-  std::string buffer = android::base::Join(args, "");
-  uiPrint(state, buffer);
-  return StringValue(buffer);
+  return StringValue("");
 }
 
 Value* WipeCacheFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
@@ -752,51 +786,6 @@
   return StringValue(std::to_string(status));
 }
 
-// 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, const std::vector<std::unique_ptr<Expr>>& argv) {
-  if (argv.size() < 1) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s() expects at least 1 arg", name);
-  }
-
-  std::vector<std::unique_ptr<Value>> args;
-  if (!ReadValueArgs(state, argv, &args)) {
-    return nullptr;
-  }
-
-  if (args[0]->type == VAL_INVALID) {
-    return StringValue("");
-  }
-  uint8_t digest[SHA_DIGEST_LENGTH];
-  SHA1(reinterpret_cast<const uint8_t*>(args[0]->data.c_str()), args[0]->data.size(), digest);
-
-  if (argv.size() == 1) {
-    return StringValue(print_sha1(digest));
-  }
-
-  for (size_t i = 1; i < argv.size(); ++i) {
-    uint8_t arg_digest[SHA_DIGEST_LENGTH];
-    if (args[i]->type != VAL_STRING) {
-      LOG(ERROR) << name << "(): arg " << i << " is not a string; skipping";
-    } else if (ParseSha1(args[i]->data.c_str(), arg_digest) != 0) {
-      // Warn about bad args and skip them.
-      LOG(ERROR) << name << "(): error parsing \"" << args[i]->data << "\" as sha-1; skipping";
-    } else if (memcmp(digest, arg_digest, SHA_DIGEST_LENGTH) == 0) {
-      // Found a match.
-      return args[i].release();
-    }
-  }
-
-  // Didn't match any of the hex strings; return false.
-  return StringValue("");
-}
-
 // Read a local file and return its contents (the Value* returned
 // is actually a FileContents*).
 Value* ReadFileFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
diff --git a/updater/updater.cpp b/updater/updater.cpp
index 1d8fa8e..1d6b172 100644
--- a/updater/updater.cpp
+++ b/updater/updater.cpp
@@ -30,10 +30,12 @@
 #include <selinux/selinux.h>
 #include <ziparchive/zip_archive.h>
 
-#include "config.h"
 #include "edify/expr.h"
+#include "otafault/config.h"
 #include "otautil/DirUtil.h"
 #include "otautil/SysUtil.h"
+#include "otautil/cache_location.h"
+#include "otautil/error_code.h"
 #include "updater/blockimg.h"
 #include "updater/install.h"
 
diff --git a/verifier.cpp b/verifier.cpp
index 18437fb..283e043 100644
--- a/verifier.cpp
+++ b/verifier.cpp
@@ -32,7 +32,7 @@
 #include <openssl/obj_mac.h>
 
 #include "asn1_decoder.h"
-#include "print_sha1.h"
+#include "otautil/print_sha1.h"
 
 static constexpr size_t MiB = 1024 * 1024;
 
diff --git a/vr_ui.cpp b/vr_ui.cpp
index 1251672..a58c99e 100644
--- a/vr_ui.cpp
+++ b/vr_ui.cpp
@@ -20,16 +20,46 @@
 
 VrRecoveryUI::VrRecoveryUI() : kStereoOffset(RECOVERY_UI_VR_STEREO_OFFSET) {}
 
-bool VrRecoveryUI::InitTextParams() {
-  if (!ScreenRecoveryUI::InitTextParams()) return false;
-  int mid_divide = gr_fb_width() / 2;
-  text_cols_ = (mid_divide - kMarginWidth - kStereoOffset) / char_width_;
-  return true;
+int VrRecoveryUI::ScreenWidth() const {
+  return gr_fb_width() / 2;
+}
+
+int VrRecoveryUI::ScreenHeight() const {
+  return gr_fb_height();
+}
+
+void VrRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                               int dy) const {
+  gr_blit(surface, sx, sy, w, h, dx + kStereoOffset, dy);
+  gr_blit(surface, sx, sy, w, h, dx - kStereoOffset + ScreenWidth(), dy);
+}
+
+void VrRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
+  gr_texticon(x + kStereoOffset, y, surface);
+  gr_texticon(x - kStereoOffset + ScreenWidth(), y, surface);
 }
 
 int VrRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
-  int mid_divide = gr_fb_width() / 2;
   gr_text(gr_sys_font(), x + kStereoOffset, y, line, bold);
-  gr_text(gr_sys_font(), x - kStereoOffset + mid_divide, y, line, bold);
+  gr_text(gr_sys_font(), x - kStereoOffset + ScreenWidth(), y, line, bold);
   return char_height_ + 4;
 }
+
+int VrRecoveryUI::DrawHorizontalRule(int y) const {
+  y += 4;
+  gr_fill(kMarginWidth + kStereoOffset, y, ScreenWidth() - kMarginWidth + kStereoOffset, y + 2);
+  gr_fill(ScreenWidth() + kMarginWidth - kStereoOffset, y,
+          gr_fb_width() - kMarginWidth - kStereoOffset, y + 2);
+  return y + 4;
+}
+
+void VrRecoveryUI::DrawHighlightBar(int /* x */, int y, int /* width */, int height) const {
+  gr_fill(kMarginWidth + kStereoOffset, y, ScreenWidth() - kMarginWidth + kStereoOffset, y + height);
+  gr_fill(ScreenWidth() + kMarginWidth - kStereoOffset, y,
+          gr_fb_width() - kMarginWidth - kStereoOffset, y + height);
+}
+
+void VrRecoveryUI::DrawFill(int x, int y, int w, int h) const {
+  gr_fill(x + kStereoOffset, y, w, h);
+  gr_fill(x - kStereoOffset + ScreenWidth(), y, w, h);
+}
diff --git a/vr_ui.h b/vr_ui.h
index d996c14..eeb4589 100644
--- a/vr_ui.h
+++ b/vr_ui.h
@@ -28,8 +28,14 @@
   // Can vary per device depending on screen size and lens distortion.
   const int kStereoOffset;
 
-  bool InitTextParams() override;
+  int ScreenWidth() const override;
+  int ScreenHeight() const override;
 
+  void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const override;
+  int DrawHorizontalRule(int y) const override;
+  void DrawHighlightBar(int x, int y, int width, int height) const override;
+  void DrawFill(int x, int y, int w, int h) const override;
+  void DrawTextIcon(int x, int y, GRSurface* surface) const override;
   int DrawTextLine(int x, int y, const char* line, bool bold) const override;
 };
 
diff --git a/wear_ui.cpp b/wear_ui.cpp
index 624116c..ca6b1b1 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -16,40 +16,16 @@
 
 #include "wear_ui.h"
 
-#include <errno.h>
-#include <fcntl.h>
-#include <stdarg.h>
-#include <stdlib.h>
+#include <pthread.h>
+#include <stdio.h>  // TODO: Remove after killing the call to sprintf().
 #include <string.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-#include <sys/types.h>
-#include <time.h>
-#include <unistd.h>
 
 #include <string>
-#include <vector>
 
 #include <android-base/properties.h>
 #include <android-base/strings.h>
-#include <android-base/stringprintf.h>
 #include <minui/minui.h>
 
-#include "common.h"
-#include "device.h"
-
-// 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()
     : kProgressBarBaseline(RECOVERY_UI_PROGRESS_BAR_BASELINE),
       kMenuUnusableRows(RECOVERY_UI_MENU_UNUSABLE_ROWS) {
@@ -61,10 +37,6 @@
   loop_frames = 60;
 
   touch_screen_allowed_ = true;
-
-  for (size_t i = 0; i < 5; i++) backgroundIcon[i] = NULL;
-
-  self = this;
 }
 
 int WearRecoveryUI::GetProgressBaseline() const {
@@ -80,24 +52,12 @@
   gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
   if (currentIcon != NONE) {
-    GRSurface* surface;
-    if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
-      if (!intro_done) {
-        surface = introFrames[current_frame];
-      } else {
-        surface = loopFrames[current_frame];
-      }
-    } else {
-      surface = backgroundIcon[currentIcon];
-    }
-
-    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);
+    GRSurface* frame = GetCurrentFrame();
+    int frame_width = gr_get_width(frame);
+    int frame_height = gr_get_height(frame);
+    int frame_x = (gr_fb_width() - frame_width) / 2;
+    int frame_y = (gr_fb_height() - frame_height) / 2;
+    gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
   }
 }
 
@@ -173,8 +133,7 @@
     // 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_;
+    int row = text_row_;
     size_t count = 0;
     for (int ty = gr_fb_height() - char_height_ - kMarginHeight; ty > y + 2 && count < text_rows_;
          ty -= char_height_, ++count) {
@@ -191,64 +150,7 @@
   gr_flip();
 }
 
-bool WearRecoveryUI::InitTextParams() {
-  if (!ScreenRecoveryUI::InitTextParams()) {
-    return false;
-  }
-
-  text_cols_ = (gr_fb_width() - (kMarginWidth * 2)) / char_width_;
-
-  if (text_rows_ > kMaxRows) text_rows_ = kMaxRows;
-  if (text_cols_ > kMaxCols) text_cols_ = kMaxCols;
-
-  visible_text_rows = (gr_fb_height() - (kMarginHeight * 2)) / char_height_;
-  return true;
-}
-
-bool WearRecoveryUI::Init(const std::string& locale) {
-  if (!ScreenRecoveryUI::Init(locale)) {
-    return false;
-  }
-
-  LoadBitmap("icon_error", &backgroundIcon[ERROR]);
-  backgroundIcon[NO_COMMAND] = backgroundIcon[ERROR];
-
-  // This leaves backgroundIcon[INSTALLING_UPDATE] and backgroundIcon[ERASING]
-  // as NULL which is fine since draw_background_locked() doesn't use them.
-
-  return true;
-}
-
-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::SetStage(int /* current */, int /* max */) {}
 
 void WearRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
                                int initial_selection) {
@@ -268,7 +170,7 @@
     show_menu = true;
     menu_sel = initial_selection;
     menu_start = 0;
-    menu_end = visible_text_rows - 1 - kMenuUnusableRows;
+    menu_end = text_rows_ - 1 - kMenuUnusableRows;
     if (menu_items <= menu_end) menu_end = menu_items;
     update_screen_locked();
   }
@@ -296,106 +198,3 @@
   pthread_mutex_unlock(&updateMutex);
   return sel;
 }
-
-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::PrintOnScreenOnly(const char *fmt, ...) {
-  va_list ap;
-  va_start(ap, fmt);
-  PrintV(fmt, false, ap);
-  va_end(ap);
-}
-
-void WearRecoveryUI::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);
-}
diff --git a/wear_ui.h b/wear_ui.h
index 3bd90b6..739b4cb 100644
--- a/wear_ui.h
+++ b/wear_ui.h
@@ -19,22 +19,12 @@
 
 #include "screen_ui.h"
 
-#include <string>
-
 class WearRecoveryUI : public ScreenRecoveryUI {
  public:
   WearRecoveryUI();
 
-  bool Init(const std::string& locale) override;
-
   void SetStage(int current, int max) override;
 
-  // printing messages
-  void Print(const char* fmt, ...) override;
-  void PrintOnScreenOnly(const char* fmt, ...) override __printflike(2, 3);
-  void ShowFile(const char* filename) override;
-  void ShowFile(FILE* fp) override;
-
   // menu display
   void StartMenu(const char* const* headers, const char* const* items,
                  int initial_selection) override;
@@ -50,30 +40,13 @@
 
   int GetProgressBaseline() const override;
 
-  bool InitTextParams() override;
-
   void update_progress_locked() override;
 
-  void PrintV(const char*, bool, va_list) override;
-
  private:
-  GRSurface* backgroundIcon[5];
-
-  static const int kMaxCols = 96;
-  static const int kMaxRows = 96;
-
-  // Number of text rows seen on screen
-  int visible_text_rows;
-
-  const char* const* menu_headers_;
-  int menu_start, menu_end;
-
-  pthread_t progress_t;
-
   void draw_background_locked() override;
   void draw_screen_locked() override;
 
-  void PutChar(char);
+  int menu_start, menu_end;
 };
 
 #endif  // RECOVERY_WEAR_UI_H